From 2d20f892e736d8b89829396af1a5a0f7a8dbe202 Mon Sep 17 00:00:00 2001 From: Habib Fatkhul Rohman Date: Wed, 15 Oct 2025 21:10:13 +0700 Subject: [PATCH] Refactor user module to support ClientID and enhance user management features --- modules/user/controller/user_controller.go | 26 +++- modules/user/dto/user_dto.go | 62 ++++++---- modules/user/query/user_query.go | 26 ++-- modules/user/repository/user_repository.go | 38 +++--- modules/user/routes.go | 9 +- modules/user/service/user_service.go | 135 +++++++++++++-------- 6 files changed, 186 insertions(+), 110 deletions(-) diff --git a/modules/user/controller/user_controller.go b/modules/user/controller/user_controller.go index 6c2c45e..09b2cae 100644 --- a/modules/user/controller/user_controller.go +++ b/modules/user/controller/user_controller.go @@ -12,6 +12,7 @@ import ( "github.com/Caknoooo/go-pagination" "github.com/gin-gonic/gin" "github.com/samber/do" + "github.com/sirupsen/logrus" "gorm.io/gorm" ) @@ -20,6 +21,7 @@ type ( Register(ctx *gin.Context) Login(ctx *gin.Context) Me(ctx *gin.Context) + GetUserById(ctx *gin.Context) Refresh(ctx *gin.Context) GetAllUser(ctx *gin.Context) SendVerificationEmail(ctx *gin.Context) @@ -62,16 +64,18 @@ func (c *userController) Register(ctx *gin.Context) { } func (c *userController) GetAllUser(ctx *gin.Context) { - tenantId := ctx.MustGet("tenant_id").(string) + clientId := ctx.MustGet("client_id").(string) + logrus.Info("Client ID: ", clientId) var filter = &query.UserFilter{ - TenantID: tenantId, - Name: ctx.Query("name"), // ambil parameter name dari query string + ClientID: clientId, + Name: ctx.Query("name"), // example additional filter } + logrus.Info("Filter: ", filter) filter.BindPagination(ctx) ctx.ShouldBindQuery(filter) - users, total, err := pagination.PaginatedQueryWithIncludable[query.User](c.db, filter) + users, total, err := pagination.PaginatedQueryWithIncludable[query.M_User](c.db, filter) if err != nil { res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_USER, err.Error(), nil) ctx.JSON(http.StatusBadRequest, res) @@ -97,6 +101,20 @@ func (c *userController) Me(ctx *gin.Context) { ctx.JSON(http.StatusOK, res) } +func (c *userController) GetUserById(ctx *gin.Context) { + userId := ctx.Param("id") + + result, err := c.userService.GetUserById(ctx.Request.Context(), userId) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_USER, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_USER, result) + ctx.JSON(http.StatusOK, res) +} + func (c *userController) Login(ctx *gin.Context) { var req dto.UserLoginRequest if err := ctx.ShouldBind(&req); err != nil { diff --git a/modules/user/dto/user_dto.go b/modules/user/dto/user_dto.go index a5bebf4..aec66a9 100644 --- a/modules/user/dto/user_dto.go +++ b/modules/user/dto/user_dto.go @@ -2,10 +2,10 @@ package dto import ( "errors" - "mime/multipart" "github.com/Caknoooo/go-gin-clean-starter/database/entities" "github.com/Caknoooo/go-gin-clean-starter/pkg/dto" + "github.com/google/uuid" ) const ( @@ -50,21 +50,29 @@ var ( type ( UserCreateRequest struct { - Name string `json:"name" form:"name" binding:"required,min=2,max=100"` - TelpNumber string `json:"telp_number" form:"telp_number" binding:"omitempty,min=8,max=20"` - Email string `json:"email" form:"email" binding:"required,email"` - Password string `json:"password" form:"password" binding:"required,min=8"` - Image *multipart.FileHeader `json:"image" form:"image"` + Name string `json:"name" form:"name" binding:"required,min=2,max=100"` + Username string `json:"username" form:"username" binding:"required,min=2,max=100"` + Password string `json:"password" form:"password" binding:"required,min=8"` + Gender string `json:"gender" form:"gender" binding:"omitempty,max=10"` + Address string `json:"address" form:"address" binding:"omitempty"` + Phone string `json:"phone" form:"phone" binding:"omitempty,min=8,max=20"` + Email string `json:"email" form:"email" binding:"required,email"` + PhotoUrl string `json:"photo_url" form:"photo_url" binding:"omitempty"` + ClientID uuid.UUID `json:"client_id" form:"client_id" binding:"required,uuid4"` + MaintenanceGroupUserID uuid.UUID `json:"maintenance_group_user_id" form:"maintenance_group_user_id" binding:"omitempty,uuid4"` + LocationID uuid.UUID `json:"location_id" form:"location_id" binding:"omitempty,uuid4"` } UserResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Email string `json:"email"` - TelpNumber string `json:"telp_number"` - Role string `json:"role"` - ImageUrl string `json:"image_url"` - IsVerified bool `json:"is_verified"` + ID string `json:"id"` + Name string `json:"name"` + Username string `json:"username"` + Password string `json:"password,omitempty"` + Gender string `json:"gender"` + Address string `json:"address"` + Phone string `json:"phone"` + Email string `json:"email"` + PhotoUrl string `json:"photo_url"` } UserPaginationResponse struct { @@ -78,18 +86,28 @@ type ( } UserUpdateRequest struct { - Name string `json:"name" form:"name" binding:"omitempty,min=2,max=100"` - TelpNumber string `json:"telp_number" form:"telp_number" binding:"omitempty,min=8,max=20"` - Email string `json:"email" form:"email" binding:"omitempty,email"` + Name string `json:"name" form:"name" binding:"omitempty,min=2,max=100"` + Username string `json:"username" form:"username" binding:"omitempty,min=2,max=100"` + Password string `json:"password" form:"password" binding:"omitempty,min=8"` + Gender string `json:"gender" form:"gender" binding:"omitempty,max=10"` + Address string `json:"address" form:"address" binding:"omitempty"` + Phone string `json:"phone" form:"phone" binding:"omitempty,min=8,max=20"` + Email string `json:"email" form:"email" binding:"omitempty,email"` + PhotoUrl string `json:"photo_url" form:"photo_url" binding:"omitempty"` + ClientID uuid.UUID `json:"client_id" form:"client_id" binding:"omitempty,uuid4"` + MaintenanceGroupUserID uuid.UUID `json:"maintenance_group_user_id" form:"maintenance_group_user_id" binding:"omitempty,uuid4"` + LocationID uuid.UUID `json:"location_id" form:"location_id" binding:"omitempty,uuid4"` } UserUpdateResponse struct { - ID string `json:"id"` - Name string `json:"name"` - TelpNumber string `json:"telp_number"` - Role string `json:"role"` - Email string `json:"email"` - IsVerified bool `json:"is_verified"` + ID string `json:"id"` + Name string `json:"name"` + Username string `json:"username"` + Gender string `json:"gender"` + Address string `json:"address"` + Phone string `json:"phone"` + Email string `json:"email"` + PhotoUrl string `json:"photo_url"` } SendVerificationEmailRequest struct { diff --git a/modules/user/query/user_query.go b/modules/user/query/user_query.go index 7911516..0d19680 100644 --- a/modules/user/query/user_query.go +++ b/modules/user/query/user_query.go @@ -5,20 +5,22 @@ import ( "gorm.io/gorm" ) -type User struct { - ID string `json:"id"` - Name string `json:"name"` - Email string `json:"email"` - TelpNumber string `json:"telp_number"` - Role string `json:"role"` - ImageUrl string `json:"image_url"` - IsVerified bool `json:"is_verified"` +type M_User struct { + ID string `json:"id"` + Name string `json:"name"` + Username string `json:"username"` + Password string `json:"password"` + Gender string `json:"gender"` + Address string `json:"address"` + Phone string `json:"phone"` + Email string `json:"email"` + PhotoUrl string `json:"photo_url"` } type UserFilter struct { pagination.BaseFilter Name string `form:"name"` // tambahkan ini - TenantID string `form:"tenant_id"` // tambahkan ini + ClientID string `form:"client_id"` // tambahkan ini } func (f *UserFilter) ApplyFilters(query *gorm.DB) *gorm.DB { @@ -26,14 +28,14 @@ func (f *UserFilter) ApplyFilters(query *gorm.DB) *gorm.DB { if f.Name != "" { query = query.Where("name ILIKE ?", "%"+f.Name+"%") } - if f.TenantID != "" { - query = query.Where("tenant_id = ?", f.TenantID) + if f.ClientID != "" { + query = query.Where("client_id = ?", f.ClientID) } return query } func (f *UserFilter) GetTableName() string { - return "users" + return "m_users" } func (f *UserFilter) GetSearchFields() []string { diff --git a/modules/user/repository/user_repository.go b/modules/user/repository/user_repository.go index 92c5ab6..aaaca4f 100644 --- a/modules/user/repository/user_repository.go +++ b/modules/user/repository/user_repository.go @@ -9,11 +9,11 @@ import ( type ( UserRepository interface { - Register(ctx context.Context, tx *gorm.DB, user entities.User) (entities.User, error) - GetUserById(ctx context.Context, tx *gorm.DB, userId string) (entities.User, error) - GetUserByEmail(ctx context.Context, tx *gorm.DB, email string) (entities.User, error) - CheckEmail(ctx context.Context, tx *gorm.DB, email string) (entities.User, bool, error) - Update(ctx context.Context, tx *gorm.DB, user entities.User) (entities.User, error) + Register(ctx context.Context, tx *gorm.DB, user entities.M_User) (entities.M_User, error) + GetUserById(ctx context.Context, tx *gorm.DB, userId string) (entities.M_User, error) + GetUserByEmail(ctx context.Context, tx *gorm.DB, email string) (entities.M_User, error) + CheckEmail(ctx context.Context, tx *gorm.DB, email string) (entities.M_User, bool, error) + Update(ctx context.Context, tx *gorm.DB, user entities.M_User) (entities.M_User, error) Delete(ctx context.Context, tx *gorm.DB, userId string) error } @@ -28,64 +28,64 @@ func NewUserRepository(db *gorm.DB) UserRepository { } } -func (r *userRepository) Register(ctx context.Context, tx *gorm.DB, user entities.User) (entities.User, error) { +func (r *userRepository) Register(ctx context.Context, tx *gorm.DB, user entities.M_User) (entities.M_User, error) { if tx == nil { tx = r.db } if err := tx.WithContext(ctx).Create(&user).Error; err != nil { - return entities.User{}, err + return entities.M_User{}, err } return user, nil } -func (r *userRepository) GetUserById(ctx context.Context, tx *gorm.DB, userId string) (entities.User, error) { +func (r *userRepository) GetUserById(ctx context.Context, tx *gorm.DB, userId string) (entities.M_User, error) { if tx == nil { tx = r.db } - var user entities.User + var user entities.M_User if err := tx.WithContext(ctx).Where("id = ?", userId).Take(&user).Error; err != nil { - return entities.User{}, err + return entities.M_User{}, err } return user, nil } -func (r *userRepository) GetUserByEmail(ctx context.Context, tx *gorm.DB, email string) (entities.User, error) { +func (r *userRepository) GetUserByEmail(ctx context.Context, tx *gorm.DB, email string) (entities.M_User, error) { if tx == nil { tx = r.db } - var user entities.User + var user entities.M_User if err := tx.WithContext(ctx).Where("email = ?", email).Take(&user).Error; err != nil { - return entities.User{}, err + return entities.M_User{}, err } return user, nil } -func (r *userRepository) CheckEmail(ctx context.Context, tx *gorm.DB, email string) (entities.User, bool, error) { +func (r *userRepository) CheckEmail(ctx context.Context, tx *gorm.DB, email string) (entities.M_User, bool, error) { if tx == nil { tx = r.db } - var user entities.User + var user entities.M_User if err := tx.WithContext(ctx).Where("email = ?", email).Take(&user).Error; err != nil { - return entities.User{}, false, err + return entities.M_User{}, false, err } return user, true, nil } -func (r *userRepository) Update(ctx context.Context, tx *gorm.DB, user entities.User) (entities.User, error) { +func (r *userRepository) Update(ctx context.Context, tx *gorm.DB, user entities.M_User) (entities.M_User, error) { if tx == nil { tx = r.db } if err := tx.WithContext(ctx).Updates(&user).Error; err != nil { - return entities.User{}, err + return entities.M_User{}, err } return user, nil @@ -96,7 +96,7 @@ func (r *userRepository) Delete(ctx context.Context, tx *gorm.DB, userId string) tx = r.db } - if err := tx.WithContext(ctx).Delete(&entities.User{}, "id = ?", userId).Error; err != nil { + if err := tx.WithContext(ctx).Delete(&entities.M_User{}, "id = ?", userId).Error; err != nil { return err } diff --git a/modules/user/routes.go b/modules/user/routes.go index 7b38164..beabe97 100644 --- a/modules/user/routes.go +++ b/modules/user/routes.go @@ -13,16 +13,17 @@ func RegisterRoutes(server *gin.Engine, injector *do.Injector) { userController := do.MustInvoke[controller.UserController](injector) jwtService := do.MustInvokeNamed[service.JWTService](injector, constants.JWTService) - userRoutes := server.Group("/api/user") + userRoutes := server.Group("/api/v1/user") { - userRoutes.POST("", userController.Register) - userRoutes.POST("/login", userController.Login) + // userRoutes.POST("", userController.Register) + // userRoutes.POST("/login", userController.Login) + // userRoutes.POST("/verify-email", userController.VerifyEmail) userRoutes.GET("", middlewares.Authenticate(jwtService), userController.GetAllUser) userRoutes.GET("/me", middlewares.Authenticate(jwtService), userController.Me) + userRoutes.GET("/:id", middlewares.Authenticate(jwtService), userController.GetUserById) userRoutes.PUT("/:id", middlewares.Authenticate(jwtService), userController.Update) userRoutes.DELETE("/:id", middlewares.Authenticate(jwtService), userController.Delete) userRoutes.POST("/send-verification-email", userController.SendVerificationEmail) - userRoutes.POST("/verify-email", userController.VerifyEmail) userRoutes.POST("/refresh", middlewares.Authenticate(jwtService), userController.Refresh) } } diff --git a/modules/user/service/user_service.go b/modules/user/service/user_service.go index d2be3f6..58f5d72 100644 --- a/modules/user/service/user_service.go +++ b/modules/user/service/user_service.go @@ -2,6 +2,7 @@ package service import ( "context" + "fmt" "github.com/Caknoooo/go-gin-clean-starter/database/entities" authDto "github.com/Caknoooo/go-gin-clean-starter/modules/auth/dto" @@ -9,8 +10,6 @@ import ( authService "github.com/Caknoooo/go-gin-clean-starter/modules/auth/service" "github.com/Caknoooo/go-gin-clean-starter/modules/user/dto" "github.com/Caknoooo/go-gin-clean-starter/modules/user/repository" - "github.com/Caknoooo/go-gin-clean-starter/pkg/constants" - "github.com/Caknoooo/go-gin-clean-starter/pkg/helpers" "github.com/Caknoooo/go-gin-clean-starter/pkg/utils" "github.com/google/uuid" "gorm.io/gorm" @@ -57,29 +56,36 @@ func (s *userService) Register(ctx context.Context, req dto.UserCreateRequest) ( return dto.UserResponse{}, dto.ErrEmailAlreadyExists } - user := entities.User{ - ID: uuid.New(), - Name: req.Name, - Email: req.Email, - TelpNumber: req.TelpNumber, - Password: req.Password, - Role: constants.ENUM_ROLE_USER, - IsVerified: false, + enryptPassword, err := utils.HashPassword(req.Password) + if err != nil { + return dto.UserResponse{}, err + } + + user := entities.M_User{ + Name: req.Name, + Username: req.Username, + Email: req.Email, + Password: enryptPassword, + Gender: req.Gender, + Address: req.Address, + Phone: req.Phone, + PhotoUrl: req.PhotoUrl, + ClientID: req.ClientID, } createdUser, err := s.userRepository.Register(ctx, s.db, user) if err != nil { return dto.UserResponse{}, err } - return dto.UserResponse{ - ID: createdUser.ID.String(), - Name: createdUser.Name, - Email: createdUser.Email, - TelpNumber: createdUser.TelpNumber, - Role: createdUser.Role, - ImageUrl: createdUser.ImageUrl, - IsVerified: createdUser.IsVerified, + ID: createdUser.ID.String(), + Name: createdUser.Name, + Username: createdUser.Username, + Email: createdUser.Email, + Phone: createdUser.Phone, + Gender: createdUser.Gender, + Address: createdUser.Address, + PhotoUrl: createdUser.PhotoUrl, }, nil } @@ -90,13 +96,14 @@ func (s *userService) GetUserById(ctx context.Context, userId string) (dto.UserR } return dto.UserResponse{ - ID: user.ID.String(), - Name: user.Name, - Email: user.Email, - TelpNumber: user.TelpNumber, - Role: user.Role, - ImageUrl: user.ImageUrl, - IsVerified: user.IsVerified, + ID: user.ID.String(), + Name: user.Name, + Username: user.Username, + Email: user.Email, + Gender: user.Gender, + Address: user.Address, + Phone: user.Phone, + PhotoUrl: user.PhotoUrl, }, nil } @@ -106,18 +113,20 @@ func (s *userService) Verify(ctx context.Context, req dto.UserLoginRequest) (aut return authDto.TokenResponse{}, dto.ErrEmailNotFound } - isValid, err := helpers.CheckPassword(user.Password, []byte(req.Password)) - if err != nil || !isValid { + isValid := utils.CheckPasswordHash(req.Password, user.Password) + if !isValid { + fmt.Println("Password validation error:", err) return authDto.TokenResponse{}, dto.ErrUserNotFound } - accessToken := s.jwtService.GenerateAccessToken(user.TenantID.String(), user.ID.String(), user.Role) + accessToken := s.jwtService.GenerateAccessToken(user.ClientID.String(), user.ID.String()) refreshTokenString, expiresAt := s.jwtService.GenerateRefreshToken() refreshToken := entities.RefreshToken{ - ID: uuid.New(), - UserID: user.ID, - TenantID: user.TenantID, + ID: uuid.New(), + UserID: user.ID, + // TenantID: user.TenantID, + ClientID: user.ClientID, Token: refreshTokenString, ExpiresAt: expiresAt, } @@ -130,7 +139,6 @@ func (s *userService) Verify(ctx context.Context, req dto.UserLoginRequest) (aut return authDto.TokenResponse{ AccessToken: accessToken, RefreshToken: refreshTokenString, - Role: user.Role, }, nil } @@ -140,11 +148,11 @@ func (s *userService) SendVerificationEmail(ctx context.Context, req dto.SendVer return dto.ErrEmailNotFound } - if user.IsVerified { - return dto.ErrAccountAlreadyVerified - } + // if user.IsVerified { + // return dto.ErrAccountAlreadyVerified + // } - verificationToken := s.jwtService.GenerateAccessToken(user.TenantID.String(), user.ID.String(), "verification") + verificationToken := s.jwtService.GenerateAccessToken(user.ClientID.String(), user.ID.String()) subject := "Email Verification" body := "Please verify your email using this token: " + verificationToken @@ -168,15 +176,15 @@ func (s *userService) VerifyEmail(ctx context.Context, req dto.VerifyEmailReques return dto.VerifyEmailResponse{}, dto.ErrUserNotFound } - user.IsVerified = true + // user.IsVerified = true updatedUser, err := s.userRepository.Update(ctx, s.db, user) if err != nil { return dto.VerifyEmailResponse{}, err } return dto.VerifyEmailResponse{ - Email: updatedUser.Email, - IsVerified: updatedUser.IsVerified, + Email: updatedUser.Email, + // IsVerified: updatedUser.IsVerified, }, nil } @@ -192,8 +200,36 @@ func (s *userService) Update(ctx context.Context, req dto.UserUpdateRequest, use if req.Email != "" { user.Email = req.Email } - if req.TelpNumber != "" { - user.TelpNumber = req.TelpNumber + if req.Username != "" { + user.Username = req.Username + } + if req.Password != "" { + enryptPassword, err := utils.HashPassword(req.Password) + if err != nil { + return dto.UserUpdateResponse{}, err + } + user.Password = enryptPassword + } + if req.Gender != "" { + user.Gender = req.Gender + } + if req.Address != "" { + user.Address = req.Address + } + if req.Phone != "" { + user.Phone = req.Phone + } + if req.PhotoUrl != "" { + user.PhotoUrl = req.PhotoUrl + } + if req.ClientID != uuid.Nil { + user.ClientID = req.ClientID + } + if req.MaintenanceGroupUserID != uuid.Nil { + user.MaintenanceGroupUserID = req.MaintenanceGroupUserID + } + if req.LocationID != uuid.Nil { + user.LocationID = req.LocationID } updatedUser, err := s.userRepository.Update(ctx, s.db, user) @@ -202,12 +238,14 @@ func (s *userService) Update(ctx context.Context, req dto.UserUpdateRequest, use } return dto.UserUpdateResponse{ - ID: updatedUser.ID.String(), - Name: updatedUser.Name, - TelpNumber: updatedUser.TelpNumber, - Role: updatedUser.Role, - Email: updatedUser.Email, - IsVerified: updatedUser.IsVerified, + ID: updatedUser.ID.String(), + Name: updatedUser.Name, + Username: updatedUser.Username, + Phone: updatedUser.Phone, + Email: updatedUser.Email, + Gender: updatedUser.Gender, + Address: updatedUser.Address, + PhotoUrl: updatedUser.PhotoUrl, }, nil } @@ -221,7 +259,7 @@ func (s *userService) RefreshToken(ctx context.Context, req authDto.RefreshToken return authDto.TokenResponse{}, err } - accessToken := s.jwtService.GenerateAccessToken(refreshToken.TenantID.String(), refreshToken.UserID.String(), refreshToken.User.Role) + accessToken := s.jwtService.GenerateAccessToken(refreshToken.ClientID.String(), refreshToken.UserID.String()) newRefreshTokenString, expiresAt := s.jwtService.GenerateRefreshToken() err = s.refreshTokenRepository.DeleteByToken(ctx, s.db, req.RefreshToken) @@ -244,6 +282,5 @@ func (s *userService) RefreshToken(ctx context.Context, req authDto.RefreshToken return authDto.TokenResponse{ AccessToken: accessToken, RefreshToken: newRefreshTokenString, - Role: refreshToken.User.Role, }, nil }