diff --git a/cmd/main.go b/cmd/main.go index e3276e2..fa89a95 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,12 +6,13 @@ import ( "github.com/Caknoooo/go-gin-clean-starter/middlewares" "github.com/Caknoooo/go-gin-clean-starter/modules/auth" + "github.com/Caknoooo/go-gin-clean-starter/modules/tenant" "github.com/Caknoooo/go-gin-clean-starter/modules/user" "github.com/Caknoooo/go-gin-clean-starter/providers" "github.com/Caknoooo/go-gin-clean-starter/script" "github.com/samber/do" - "github.com/common-nighthawk/go-figure" + // "github.com/common-nighthawk/go-figure" "github.com/gin-gonic/gin" ) @@ -39,8 +40,8 @@ func run(server *gin.Engine) { serve = ":" + port } - myFigure := figure.NewColorFigure("Caknoo", "", "green", true) - myFigure.Print() + // myFigure := figure.NewColorFigure("Hafaro", "", "green", true) + // myFigure.Print() if err := server.Run(serve); err != nil { log.Fatalf("error running server: %v", err) @@ -64,6 +65,7 @@ func main() { // Register module routes user.RegisterRoutes(server, injector) auth.RegisterRoutes(server, injector) + tenant.RegisterRoutes(server, injector) run(server) } diff --git a/modules/tenant/controller/tenant_controller.go b/modules/tenant/controller/tenant_controller.go new file mode 100644 index 0000000..4018432 --- /dev/null +++ b/modules/tenant/controller/tenant_controller.go @@ -0,0 +1,122 @@ +package controller + +import ( + "net/http" + + "github.com/Caknoooo/go-gin-clean-starter/modules/tenant/dto" + "github.com/Caknoooo/go-gin-clean-starter/modules/tenant/query" + "github.com/Caknoooo/go-gin-clean-starter/modules/tenant/service" + "github.com/Caknoooo/go-gin-clean-starter/pkg/constants" + "github.com/Caknoooo/go-gin-clean-starter/pkg/utils" + "github.com/Caknoooo/go-pagination" + "github.com/gin-gonic/gin" + "github.com/samber/do" + "gorm.io/gorm" +) + +type ( + TenantController interface { + Create(ctx *gin.Context) + GetAll(ctx *gin.Context) + GetById(ctx *gin.Context) + Update(ctx *gin.Context) + Delete(ctx *gin.Context) + } + + tenantController struct { + tenantService service.TenantService + db *gorm.DB + } +) + +func NewTenantController(injector *do.Injector, ts service.TenantService) TenantController { + db := do.MustInvokeNamed[*gorm.DB](injector, constants.DB) + return &tenantController{ + tenantService: ts, + db: db, + } +} + +func (c *tenantController) Create(ctx *gin.Context) { + var tenant dto.TenantCreateRequest + if err := ctx.ShouldBind(&tenant); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil) + ctx.AbortWithStatusJSON(http.StatusBadRequest, res) + return + } + + result, err := c.tenantService.Create(ctx.Request.Context(), tenant) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_REGISTER_TENANT, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_REGISTER_TENANT, result) + ctx.JSON(http.StatusOK, res) +} + +func (c *tenantController) GetById(ctx *gin.Context) { + tenantId := ctx.Param("id") + + result, err := c.tenantService.GetTenantById(ctx.Request.Context(), tenantId) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_TENANT, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_TENANT, result) + ctx.JSON(http.StatusOK, res) +} + +func (c *tenantController) GetAll(ctx *gin.Context) { + var filter = &query.TenantFilter{} + filter.BindPagination(ctx) + + ctx.ShouldBindQuery(filter) + + tenants, total, err := pagination.PaginatedQueryWithIncludable[query.Tenant](c.db, filter) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_TENANT, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + + paginationResponse := pagination.CalculatePagination(filter.Pagination, total) + response := pagination.NewPaginatedResponse(http.StatusOK, dto.MESSAGE_SUCCESS_GET_LIST_TENANT, tenants, paginationResponse) + ctx.JSON(http.StatusOK, response) +} + +func (c *tenantController) Update(ctx *gin.Context) { + tenantId := ctx.Param("id") + var tenant dto.TenantUpdateRequest + if err := ctx.ShouldBind(&tenant); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil) + ctx.AbortWithStatusJSON(http.StatusBadRequest, res) + return + } + + result, err := c.tenantService.Update(ctx.Request.Context(), tenant, tenantId) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_UPDATE_TENANT, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_UPDATE_TENANT, result) + ctx.JSON(http.StatusOK, res) +} + +func (c *tenantController) Delete(ctx *gin.Context) { + tenantId := ctx.Param("id") + + if err := c.tenantService.Delete(ctx.Request.Context(), tenantId); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_DELETE_TENANT, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_DELETE_TENANT, nil) + ctx.JSON(http.StatusOK, res) +} diff --git a/modules/tenant/dto/tenant_dto.go b/modules/tenant/dto/tenant_dto.go new file mode 100644 index 0000000..46ea4b2 --- /dev/null +++ b/modules/tenant/dto/tenant_dto.go @@ -0,0 +1,83 @@ +package dto + +import ( + "errors" + + "github.com/Caknoooo/go-gin-clean-starter/database/entities" + "github.com/Caknoooo/go-gin-clean-starter/pkg/dto" +) + +const ( + // Failed + MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body" + MESSAGE_FAILED_REGISTER_TENANT = "failed create tenant" + MESSAGE_FAILED_GET_LIST_TENANT = "failed get list tenant" + MESSAGE_FAILED_TOKEN_NOT_VALID = "token not valid" + MESSAGE_FAILED_TOKEN_NOT_FOUND = "token not found" + MESSAGE_FAILED_GET_TENANT = "failed get tenant" + MESSAGE_FAILED_LOGIN = "failed login" + MESSAGE_FAILED_UPDATE_TENANT = "failed update tenant" + MESSAGE_FAILED_DELETE_TENANT = "failed delete tenant" + MESSAGE_FAILED_PROSES_REQUEST = "failed proses request" + MESSAGE_FAILED_DENIED_ACCESS = "denied access" + MESSAGE_FAILED_VERIFY_EMAIL = "failed verify email" + + // Success + MESSAGE_SUCCESS_REGISTER_TENANT = "success create tenant" + MESSAGE_SUCCESS_GET_LIST_TENANT = "success get list tenant" + MESSAGE_SUCCESS_GET_TENANT = "success get tenant" + MESSAGE_SUCCESS_LOGIN = "success login" + MESSAGE_SUCCESS_UPDATE_TENANT = "success update tenant" + MESSAGE_SUCCESS_DELETE_TENANT = "success delete tenant" + MESSAGE_SEND_VERIFICATION_EMAIL_SUCCESS = "success send verification email" + MESSAGE_SUCCESS_VERIFY_EMAIL = "success verify email" +) + +var ( + ErrCreateTenant = errors.New("failed to create tenant") + ErrGetTenantById = errors.New("failed to get tenant by id") + ErrGetTenantByEmail = errors.New("failed to get tenant by email") + ErrEmailAlreadyExists = errors.New("email already exist") + ErrUpdateTenant = errors.New("failed to update tenant") + ErrTenantNotFound = errors.New("tenant not found") + ErrEmailNotFound = errors.New("email not found") + ErrDeleteTenant = errors.New("failed to delete tenant") + ErrTokenInvalid = errors.New("token invalid") + ErrTokenExpired = errors.New("token expired") + ErrAccountAlreadyVerified = errors.New("account already verified") + ErrNameAlreadyExists = errors.New("name already exists") +) + +type ( + TenantCreateRequest struct { + Name string `json:"name" form:"name" binding:"required,min=2,max=100"` + } + + TenantResponse struct { + ID string `json:"id"` + Name string `json:"name"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + } + + TenantPaginationResponse struct { + Data []TenantResponse `json:"data"` + dto.PaginationResponse + } + + GetAllTenantRepositoryResponse struct { + Tenants []entities.Tenant `json:"tenants"` + dto.PaginationResponse + } + + TenantUpdateRequest struct { + Name string `json:"name" form:"name" binding:"omitempty,min=2,max=100"` + } + + TenantUpdateResponse struct { + ID string `json:"id"` + Name string `json:"name"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + } +) diff --git a/modules/tenant/query/tenant_query.go b/modules/tenant/query/tenant_query.go new file mode 100644 index 0000000..4ecd204 --- /dev/null +++ b/modules/tenant/query/tenant_query.go @@ -0,0 +1,60 @@ +package query + +import ( + "github.com/Caknoooo/go-pagination" + "gorm.io/gorm" +) + +type Tenant struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type TenantFilter struct { + pagination.BaseFilter + Name string `form:"name"` // tambahkan ini + +} + +func (f *TenantFilter) ApplyFilters(query *gorm.DB) *gorm.DB { + // Apply your filters here + if f.Name != "" { + query = query.Where("name ILIKE ?", "%"+f.Name+"%") + } + return query +} + +func (f *TenantFilter) GetTableName() string { + return "tenants" +} + +func (f *TenantFilter) GetSearchFields() []string { + return []string{"name"} +} + +func (f *TenantFilter) GetDefaultSort() string { + return "id asc" +} + +func (f *TenantFilter) GetIncludes() []string { + return f.Includes +} + +func (f *TenantFilter) GetPagination() pagination.PaginationRequest { + return f.Pagination +} + +func (f *TenantFilter) Validate() { + var validIncludes []string + allowedIncludes := f.GetAllowedIncludes() + for _, include := range f.Includes { + if allowedIncludes[include] { + validIncludes = append(validIncludes, include) + } + } + f.Includes = validIncludes +} + +func (f *TenantFilter) GetAllowedIncludes() map[string]bool { + return map[string]bool{} +} diff --git a/modules/tenant/repository/tenant_repository.go b/modules/tenant/repository/tenant_repository.go new file mode 100644 index 0000000..9e1b99f --- /dev/null +++ b/modules/tenant/repository/tenant_repository.go @@ -0,0 +1,109 @@ +package repository + +import ( + "context" + "errors" + + "github.com/Caknoooo/go-gin-clean-starter/database/entities" + "gorm.io/gorm" +) + +type ( + TenantRepository interface { + Create(ctx context.Context, tx *gorm.DB, tenant entities.Tenant) (entities.Tenant, error) + GetById(ctx context.Context, tx *gorm.DB, tenantId string) (entities.Tenant, error) + GetByName(ctx context.Context, tx *gorm.DB, name string) (entities.Tenant, error) + // CheckEmail(ctx context.Context, tx *gorm.DB, email string) (entities.Tenant, bool, error) + CheckName(ctx context.Context, tx *gorm.DB, name string) (entities.Tenant, bool, error) + Update(ctx context.Context, tx *gorm.DB, tenant entities.Tenant) (entities.Tenant, error) + Delete(ctx context.Context, tx *gorm.DB, tenantId string) error + } + + tenantRepository struct { + db *gorm.DB + } +) + +func NewTenantRepository(db *gorm.DB) TenantRepository { + return &tenantRepository{ + db: db, + } +} + +func (r *tenantRepository) Create(ctx context.Context, tx *gorm.DB, tenant entities.Tenant) (entities.Tenant, error) { + if tx == nil { + tx = r.db + } + + if err := tx.WithContext(ctx).Create(&tenant).Error; err != nil { + return entities.Tenant{}, err + } + + return tenant, nil +} + +func (r *tenantRepository) GetById(ctx context.Context, tx *gorm.DB, tenantId string) (entities.Tenant, error) { + if tx == nil { + tx = r.db + } + + var tenant entities.Tenant + if err := tx.WithContext(ctx).Where("id = ?", tenantId).Take(&tenant).Error; err != nil { + return entities.Tenant{}, err + } + + return tenant, nil +} + +func (r *tenantRepository) GetByName(ctx context.Context, tx *gorm.DB, name string) (entities.Tenant, error) { + if tx == nil { + tx = r.db + } + + var tenant entities.Tenant + if err := tx.WithContext(ctx).Where("name = ?", name).Take(&tenant).Error; err != nil { + return entities.Tenant{}, err + } + + return tenant, nil +} + +func (r *tenantRepository) Update(ctx context.Context, tx *gorm.DB, tenant entities.Tenant) (entities.Tenant, error) { + if tx == nil { + tx = r.db + } + + if err := tx.WithContext(ctx).Updates(&tenant).Error; err != nil { + return entities.Tenant{}, err + } + + return tenant, nil +} + +func (r *tenantRepository) Delete(ctx context.Context, tx *gorm.DB, tenantId string) error { + if tx == nil { + tx = r.db + } + + if err := tx.WithContext(ctx).Delete(&entities.Tenant{}, "id = ?", tenantId).Error; err != nil { + return err + } + + return nil +} + +func (r *tenantRepository) CheckName(ctx context.Context, tx *gorm.DB, name string) (entities.Tenant, bool, error) { + if tx == nil { + tx = r.db + } + + var tenant entities.Tenant + if err := tx.WithContext(ctx).Where("name = ?", name).Take(&tenant).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return entities.Tenant{}, false, nil + } + return entities.Tenant{}, false, err + } + + return tenant, true, nil +} diff --git a/modules/tenant/routes.go b/modules/tenant/routes.go new file mode 100644 index 0000000..49b9545 --- /dev/null +++ b/modules/tenant/routes.go @@ -0,0 +1,24 @@ +package tenant + +import ( + "github.com/Caknoooo/go-gin-clean-starter/middlewares" + "github.com/Caknoooo/go-gin-clean-starter/modules/auth/service" + "github.com/Caknoooo/go-gin-clean-starter/modules/tenant/controller" + "github.com/Caknoooo/go-gin-clean-starter/pkg/constants" + "github.com/gin-gonic/gin" + "github.com/samber/do" +) + +func RegisterRoutes(server *gin.Engine, injector *do.Injector) { + tenantController := do.MustInvoke[controller.TenantController](injector) + jwtService := do.MustInvokeNamed[service.JWTService](injector, constants.JWTService) + + userRoutes := server.Group("/api/tenant") + { + userRoutes.POST("", tenantController.Create) + userRoutes.GET("", tenantController.GetAll) + userRoutes.GET("/:id", tenantController.GetById) + userRoutes.PUT("/:id", middlewares.Authenticate(jwtService), tenantController.Update) + userRoutes.DELETE("/:id", middlewares.Authenticate(jwtService), tenantController.Delete) + } +} diff --git a/modules/tenant/service/tenant_service.go b/modules/tenant/service/tenant_service.go new file mode 100644 index 0000000..12b7112 --- /dev/null +++ b/modules/tenant/service/tenant_service.go @@ -0,0 +1,102 @@ +package service + +import ( + "context" + "time" + + "github.com/Caknoooo/go-gin-clean-starter/database/entities" + "github.com/Caknoooo/go-gin-clean-starter/modules/tenant/dto" + "github.com/Caknoooo/go-gin-clean-starter/modules/tenant/repository" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type TenantService interface { + Create(ctx context.Context, req dto.TenantCreateRequest) (dto.TenantResponse, error) + GetTenantById(ctx context.Context, tenantId string) (dto.TenantResponse, error) + Update(ctx context.Context, req dto.TenantUpdateRequest, tenantId string) (dto.TenantUpdateResponse, error) + Delete(ctx context.Context, tenantId string) error +} + +type tenantService struct { + tenantRepository repository.TenantRepository + db *gorm.DB +} + +func NewTenantService( + tenantRepo repository.TenantRepository, + db *gorm.DB, +) TenantService { + return &tenantService{ + tenantRepository: tenantRepo, + db: db, + } +} + +func (s *tenantService) Create(ctx context.Context, req dto.TenantCreateRequest) (dto.TenantResponse, error) { + _, exists, err := s.tenantRepository.CheckName(ctx, s.db, req.Name) + if err != nil && err != gorm.ErrRecordNotFound { + return dto.TenantResponse{}, err + } + if exists { + return dto.TenantResponse{}, dto.ErrNameAlreadyExists + } + + tenant := entities.Tenant{ + ID: uuid.New(), + Name: req.Name, + } + + createdTenant, err := s.tenantRepository.Create(ctx, s.db, tenant) + if err != nil { + return dto.TenantResponse{}, err + } + + return dto.TenantResponse{ + ID: createdTenant.ID.String(), + Name: createdTenant.Name, + CreatedAt: createdTenant.CreatedAt.Format(time.RFC3339), + UpdatedAt: createdTenant.UpdatedAt.Format(time.RFC3339), + }, nil +} + +func (s *tenantService) GetTenantById(ctx context.Context, tenantId string) (dto.TenantResponse, error) { + tenant, err := s.tenantRepository.GetById(ctx, s.db, tenantId) + if err != nil { + return dto.TenantResponse{}, err + } + + return dto.TenantResponse{ + ID: tenant.ID.String(), + Name: tenant.Name, + CreatedAt: tenant.CreatedAt.Format(time.RFC3339), + UpdatedAt: tenant.UpdatedAt.Format(time.RFC3339), + }, nil +} + +func (s *tenantService) Update(ctx context.Context, req dto.TenantUpdateRequest, tenantId string) (dto.TenantUpdateResponse, error) { + tenant, err := s.tenantRepository.GetById(ctx, s.db, tenantId) + if err != nil { + return dto.TenantUpdateResponse{}, dto.ErrTenantNotFound + } + + if req.Name != "" { + tenant.Name = req.Name + } + + updatedTenant, err := s.tenantRepository.Update(ctx, s.db, tenant) + if err != nil { + return dto.TenantUpdateResponse{}, err + } + + return dto.TenantUpdateResponse{ + ID: updatedTenant.ID.String(), + Name: updatedTenant.Name, + CreatedAt: updatedTenant.CreatedAt.Format(time.RFC3339), + UpdatedAt: updatedTenant.UpdatedAt.Format(time.RFC3339), + }, nil +} + +func (s *tenantService) Delete(ctx context.Context, tenantId string) error { + return s.tenantRepository.Delete(ctx, s.db, tenantId) +} diff --git a/providers/core.go b/providers/core.go index 9306a21..d6c7d1c 100644 --- a/providers/core.go +++ b/providers/core.go @@ -3,10 +3,13 @@ package providers import ( "github.com/Caknoooo/go-gin-clean-starter/config" authRepo "github.com/Caknoooo/go-gin-clean-starter/modules/auth/repository" - userService "github.com/Caknoooo/go-gin-clean-starter/modules/user/service" "github.com/Caknoooo/go-gin-clean-starter/modules/auth/service" + tenantController "github.com/Caknoooo/go-gin-clean-starter/modules/tenant/controller" + tenantRepo "github.com/Caknoooo/go-gin-clean-starter/modules/tenant/repository" + tenantService "github.com/Caknoooo/go-gin-clean-starter/modules/tenant/service" "github.com/Caknoooo/go-gin-clean-starter/modules/user/controller" "github.com/Caknoooo/go-gin-clean-starter/modules/user/repository" + userService "github.com/Caknoooo/go-gin-clean-starter/modules/user/service" "github.com/Caknoooo/go-gin-clean-starter/pkg/constants" "github.com/samber/do" "gorm.io/gorm" @@ -31,10 +34,12 @@ func RegisterDependencies(injector *do.Injector) { // Repository userRepository := repository.NewUserRepository(db) + tenantRepository := tenantRepo.NewTenantRepository(db) refreshTokenRepository := authRepo.NewRefreshTokenRepository(db) // Service userService := userService.NewUserService(userRepository, refreshTokenRepository, jwtService, db) + tenantService := tenantService.NewTenantService(tenantRepository, db) // Controller do.Provide( @@ -42,4 +47,9 @@ func RegisterDependencies(injector *do.Injector) { return controller.NewUserController(i, userService), nil }, ) + do.Provide( + injector, func(i *do.Injector) (tenantController.TenantController, error) { + return tenantController.NewTenantController(i, tenantService), nil + }, + ) }