From c8ea680e05989f38b6ab53f4fbace68547c3cba6 Mon Sep 17 00:00:00 2001 From: Efril Date: Wed, 14 Jan 2026 14:08:27 +0700 Subject: [PATCH] api change user password --- go.mod | 7 ++---- go.sum | 2 -- internal/contract/common.go | 14 ++++++++---- internal/contract/user_contract.go | 20 +++++++++------- internal/handler/user_handler.go | 34 ++++++++++++++++++++++++++++ internal/handler/user_service.go | 1 + internal/processor/user_processor.go | 21 ++++++++++++++++- internal/router/health_handler.go | 1 + internal/router/router.go | 1 + internal/service/user_processor.go | 1 + internal/service/user_service.go | 10 +++++--- 11 files changed, 89 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index 3419c97..b5c7dd5 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,6 @@ require ( github.com/bytedance/sonic v1.10.2 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -38,16 +37,14 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/novuhq/go-novu v0.1.2 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect @@ -65,8 +62,8 @@ require ( require ( github.com/aws/aws-sdk-go v1.55.7 github.com/golang-jwt/jwt/v5 v5.2.3 + github.com/novuhq/go-novu v0.1.2 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.10.0 go.uber.org/zap v1.21.0 golang.org/x/crypto v0.28.0 gorm.io/driver/postgres v1.5.0 diff --git a/go.sum b/go.sum index 1ecfcd2..8d13098 100644 --- a/go.sum +++ b/go.sum @@ -239,8 +239,6 @@ github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1Fof github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= diff --git a/internal/contract/common.go b/internal/contract/common.go index 8067286..0da6ac2 100644 --- a/internal/contract/common.go +++ b/internal/contract/common.go @@ -3,11 +3,11 @@ package contract import "time" const ( - SettingIncomingLetterPrefix = "INCOMING_LETTER_PREFIX" - SettingIncomingLetterSequence = "INCOMING_LETTER_SEQUENCE" + SettingIncomingLetterPrefix = "INCOMING_LETTER_PREFIX" + SettingIncomingLetterSequence = "INCOMING_LETTER_SEQUENCE" SettingIncomingLetterDepartmentRecipients = "INCOMING_LETTER_DEPARTMENT_RECIPIENTS" - SettingOutgoingLetterPrefix = "OUTGOING_LETTER_PREFIX" - SettingOutgoingLetterSequence = "OUTGOING_LETTER_SEQUENCE" + SettingOutgoingLetterPrefix = "OUTGOING_LETTER_PREFIX" + SettingOutgoingLetterSequence = "OUTGOING_LETTER_SEQUENCE" ) type ErrorResponse struct { @@ -29,6 +29,12 @@ type SuccessResponse struct { Data interface{} `json:"data,omitempty"` } +type NewSuccessResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + type PaginationRequest struct { Page int `json:"page" validate:"min=1"` Limit int `json:"limit" validate:"min=1,max=100"` diff --git a/internal/contract/user_contract.go b/internal/contract/user_contract.go index 6141307..c73c788 100644 --- a/internal/contract/user_contract.go +++ b/internal/contract/user_contract.go @@ -7,17 +7,17 @@ import ( ) type CreateUserRequest struct { - Name string `json:"name" validate:"required,min=1,max=255"` - Email string `json:"email" validate:"required,email"` - Password string `json:"password" validate:"required,min=6"` - RoleID *uuid.UUID `json:"role_id,omitempty" validate:"required"` + Name string `json:"name" validate:"required,min=1,max=255"` + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required,min=6"` + RoleID *uuid.UUID `json:"role_id,omitempty" validate:"required"` DepartmentIDs []uuid.UUID `json:"department_ids,omitempty"` } type UpdateUserRequest struct { Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"` Email *string `json:"email,omitempty" validate:"omitempty,email"` - Role *uuid.UUID `json:"role,omitempty"` + Role *uuid.UUID `json:"role,omitempty"` IsActive *bool `json:"is_active,omitempty"` Permissions *map[string]interface{} `json:"permissions,omitempty"` DepartmentIDs *[]uuid.UUID `json:"department_ids,omitempty"` @@ -28,6 +28,10 @@ type ChangePasswordRequest struct { NewPassword string `json:"new_password" validate:"required,min=6"` } +type ChangeUserPasswordRequest struct { + NewPassword string `json:"new_password" validate:"required,min=6"` +} + type UpdateUserOutletRequest struct { OutletID uuid.UUID `json:"outlet_id" validate:"required"` } @@ -105,9 +109,9 @@ type CreateDepartmentRequest struct { } type UpdateDepartmentRequest struct { - Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"` - Code *string `json:"code,omitempty" validate:"omitempty,min=1,max=50"` - ParentID *uuid.UUID `json:"parent_id,omitempty"` + Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"` + Code *string `json:"code,omitempty" validate:"omitempty,min=1,max=50"` + ParentID *uuid.UUID `json:"parent_id,omitempty"` } type GetDepartmentResponse struct { diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go index e5c8752..a963a5b 100644 --- a/internal/handler/user_handler.go +++ b/internal/handler/user_handler.go @@ -249,6 +249,40 @@ func (h *UserHandler) ChangePassword(c *gin.Context) { c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "Password changed successfully"}) } +func (h *UserHandler) ChangeUserPassword(c *gin.Context) { + userIDStr := c.Param("id") + userID, err := uuid.Parse(userIDStr) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::ChangeUserPassword -> Invalid user ID") + h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode) + return + } + + validationError, validationErrorCode := h.userValidator.ValidateUserID(userID) + if validationError != nil { + logger.FromContext(c).WithError(validationError).Error("UserHandler::ChangeUserPassword -> user ID validation failed") + h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode) + return + } + + var req contract.ChangeUserPasswordRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::ChangeUserPassword -> request binding failed") + h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode) + return + } + + err = h.userService.ChangeUserPassword(c.Request.Context(), userID, &req) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::ChangeUserPassword -> Failed to change password from service") + h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError) + return + } + + logger.FromContext(c).Info("UserHandler::ChangeUserPassword -> Successfully changed password") + c.JSON(http.StatusOK, &contract.NewSuccessResponse{Success: true, Message: "Password changed successfully"}) +} + func (h *UserHandler) GetProfile(c *gin.Context) { appCtx := appcontext.FromGinContext(c.Request.Context()) if appCtx.UserID == uuid.Nil { diff --git a/internal/handler/user_service.go b/internal/handler/user_service.go index 9734879..d692a41 100644 --- a/internal/handler/user_service.go +++ b/internal/handler/user_service.go @@ -15,6 +15,7 @@ type UserService interface { GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) ListUsers(ctx context.Context, req *contract.ListUsersRequest) (*contract.ListUsersResponse, error) ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error + ChangeUserPassword(ctx context.Context, userID uuid.UUID, req *contract.ChangeUserPasswordRequest) error GetProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error) UpdateProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error) diff --git a/internal/processor/user_processor.go b/internal/processor/user_processor.go index 21772d4..1353074 100644 --- a/internal/processor/user_processor.go +++ b/internal/processor/user_processor.go @@ -125,7 +125,7 @@ func (p *UserProcessorImpl) UpdateUser(ctx context.Context, id uuid.UUID, req *c return nil, fmt.Errorf("failed to update user: %w", err) } - if(req.Name != nil) { + if req.Name != nil { profile, err := p.profileRepo.GetByUserID(ctx, updated.ID) if err != nil { return nil, fmt.Errorf("failed to get user profile: %w", err) @@ -293,6 +293,25 @@ func (p *UserProcessorImpl) ChangePassword(ctx context.Context, userID uuid.UUID return nil } +func (p *UserProcessorImpl) ChangeUserPassword(ctx context.Context, userID uuid.UUID, req *contract.ChangeUserPasswordRequest) error { + _, err := p.userRepo.GetByID(ctx, userID) + if err != nil { + return fmt.Errorf("user not found: %w", err) + } + + newPasswordHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) + if err != nil { + return fmt.Errorf("failed to hash new password: %w", err) + } + + err = p.userRepo.UpdatePassword(ctx, userID, string(newPasswordHash)) + if err != nil { + return fmt.Errorf("failed to update password: %w", err) + } + + return nil +} + func (p *UserProcessorImpl) ActivateUser(ctx context.Context, userID uuid.UUID) error { _, err := p.userRepo.GetByID(ctx, userID) if err != nil { diff --git a/internal/router/health_handler.go b/internal/router/health_handler.go index a9262fc..a4187b3 100644 --- a/internal/router/health_handler.go +++ b/internal/router/health_handler.go @@ -15,6 +15,7 @@ type UserHandler interface { GetUserProfile(c *gin.Context) UpdateProfile(c *gin.Context) ChangePassword(c *gin.Context) + ChangeUserPassword(c *gin.Context) ListTitles(c *gin.Context) GetActiveUsersForMention(c *gin.Context) } diff --git a/internal/router/router.go b/internal/router/router.go index 009cdb5..9d41732 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -103,6 +103,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { users.GET("/:id/profile", r.userHandler.GetUserProfile) users.PUT("/profile", r.userHandler.UpdateProfile) users.PUT("/:id/password", r.userHandler.ChangePassword) + users.PUT("/:id/user-password", r.userHandler.ChangeUserPassword) users.GET("/titles", r.userHandler.ListTitles) users.GET("/mention", r.userHandler.GetActiveUsersForMention) users.POST("/profile/avatar", r.fileHandler.UploadProfileAvatar) diff --git a/internal/service/user_processor.go b/internal/service/user_processor.go index d48f528..c4f5773 100644 --- a/internal/service/user_processor.go +++ b/internal/service/user_processor.go @@ -17,6 +17,7 @@ type UserProcessor interface { GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) GetUserEntityByEmail(ctx context.Context, email string) (*entities.User, error) ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error + ChangeUserPassword(ctx context.Context, userID uuid.UUID, req *contract.ChangeUserPasswordRequest) error GetUserRoles(ctx context.Context, userID uuid.UUID) ([]contract.RoleResponse, error) GetUserPermissionCodes(ctx context.Context, userID uuid.UUID) ([]string, error) diff --git a/internal/service/user_service.go b/internal/service/user_service.go index a8209a8..5199e1e 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -52,7 +52,7 @@ func (s *UserServiceImpl) ListUsers(ctx context.Context, req *contract.ListUsers if page <= 0 { page = 1 } - + limit := req.Limit if limit <= 0 { limit = 10 @@ -60,7 +60,7 @@ func (s *UserServiceImpl) ListUsers(ctx context.Context, req *contract.ListUsers if limit > 100 { limit = 100 // Max limit to prevent performance issues } - + offset := (page - 1) * limit // Pass calculated offset and limit to processor @@ -79,6 +79,10 @@ func (s *UserServiceImpl) ChangePassword(ctx context.Context, userID uuid.UUID, return s.userProcessor.ChangePassword(ctx, userID, req) } +func (s *UserServiceImpl) ChangeUserPassword(ctx context.Context, userID uuid.UUID, req *contract.ChangeUserPasswordRequest) error { + return s.userProcessor.ChangeUserPassword(ctx, userID, req) +} + func (s *UserServiceImpl) GetProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error) { prof, err := s.userProcessor.GetUserProfile(ctx, userID) if err != nil { @@ -114,6 +118,6 @@ func (s *UserServiceImpl) GetActiveUsersForMention(ctx context.Context, search * if limit > 100 { limit = 100 // Max limit to prevent performance issues } - + return s.userProcessor.GetActiveUsersForMention(ctx, search, limit) }