api change user password

This commit is contained in:
Efril 2026-01-14 14:08:27 +07:00
parent 057cccfef9
commit c8ea680e05
11 changed files with 89 additions and 23 deletions

7
go.mod
View File

@ -15,7 +15,6 @@ require (
github.com/bytedance/sonic v1.10.2 // indirect github.com/bytedance/sonic v1.10.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // 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/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // 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/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // 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/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pkg/errors v0.9.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/rogpeppe/go-internal v1.11.0 // indirect
github.com/spf13/afero v1.9.5 // indirect github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // 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/subosito/gotenv v1.4.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
@ -65,8 +62,8 @@ require (
require ( require (
github.com/aws/aws-sdk-go v1.55.7 github.com/aws/aws-sdk-go v1.55.7
github.com/golang-jwt/jwt/v5 v5.2.3 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/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.10.0
go.uber.org/zap v1.21.0 go.uber.org/zap v1.21.0
golang.org/x/crypto v0.28.0 golang.org/x/crypto v0.28.0
gorm.io/driver/postgres v1.5.0 gorm.io/driver/postgres v1.5.0

2
go.sum
View File

@ -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.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.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.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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=

View File

@ -3,11 +3,11 @@ package contract
import "time" import "time"
const ( const (
SettingIncomingLetterPrefix = "INCOMING_LETTER_PREFIX" SettingIncomingLetterPrefix = "INCOMING_LETTER_PREFIX"
SettingIncomingLetterSequence = "INCOMING_LETTER_SEQUENCE" SettingIncomingLetterSequence = "INCOMING_LETTER_SEQUENCE"
SettingIncomingLetterDepartmentRecipients = "INCOMING_LETTER_DEPARTMENT_RECIPIENTS" SettingIncomingLetterDepartmentRecipients = "INCOMING_LETTER_DEPARTMENT_RECIPIENTS"
SettingOutgoingLetterPrefix = "OUTGOING_LETTER_PREFIX" SettingOutgoingLetterPrefix = "OUTGOING_LETTER_PREFIX"
SettingOutgoingLetterSequence = "OUTGOING_LETTER_SEQUENCE" SettingOutgoingLetterSequence = "OUTGOING_LETTER_SEQUENCE"
) )
type ErrorResponse struct { type ErrorResponse struct {
@ -29,6 +29,12 @@ type SuccessResponse struct {
Data interface{} `json:"data,omitempty"` Data interface{} `json:"data,omitempty"`
} }
type NewSuccessResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
type PaginationRequest struct { type PaginationRequest struct {
Page int `json:"page" validate:"min=1"` Page int `json:"page" validate:"min=1"`
Limit int `json:"limit" validate:"min=1,max=100"` Limit int `json:"limit" validate:"min=1,max=100"`

View File

@ -7,17 +7,17 @@ import (
) )
type CreateUserRequest struct { type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=1,max=255"` Name string `json:"name" validate:"required,min=1,max=255"`
Email string `json:"email" validate:"required,email"` Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=6"` Password string `json:"password" validate:"required,min=6"`
RoleID *uuid.UUID `json:"role_id,omitempty" validate:"required"` RoleID *uuid.UUID `json:"role_id,omitempty" validate:"required"`
DepartmentIDs []uuid.UUID `json:"department_ids,omitempty"` DepartmentIDs []uuid.UUID `json:"department_ids,omitempty"`
} }
type UpdateUserRequest struct { type UpdateUserRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"` Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
Email *string `json:"email,omitempty" validate:"omitempty,email"` 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"` IsActive *bool `json:"is_active,omitempty"`
Permissions *map[string]interface{} `json:"permissions,omitempty"` Permissions *map[string]interface{} `json:"permissions,omitempty"`
DepartmentIDs *[]uuid.UUID `json:"department_ids,omitempty"` DepartmentIDs *[]uuid.UUID `json:"department_ids,omitempty"`
@ -28,6 +28,10 @@ type ChangePasswordRequest struct {
NewPassword string `json:"new_password" validate:"required,min=6"` NewPassword string `json:"new_password" validate:"required,min=6"`
} }
type ChangeUserPasswordRequest struct {
NewPassword string `json:"new_password" validate:"required,min=6"`
}
type UpdateUserOutletRequest struct { type UpdateUserOutletRequest struct {
OutletID uuid.UUID `json:"outlet_id" validate:"required"` OutletID uuid.UUID `json:"outlet_id" validate:"required"`
} }
@ -105,9 +109,9 @@ type CreateDepartmentRequest struct {
} }
type UpdateDepartmentRequest struct { type UpdateDepartmentRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"` Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
Code *string `json:"code,omitempty" validate:"omitempty,min=1,max=50"` Code *string `json:"code,omitempty" validate:"omitempty,min=1,max=50"`
ParentID *uuid.UUID `json:"parent_id,omitempty"` ParentID *uuid.UUID `json:"parent_id,omitempty"`
} }
type GetDepartmentResponse struct { type GetDepartmentResponse struct {

View File

@ -249,6 +249,40 @@ func (h *UserHandler) ChangePassword(c *gin.Context) {
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "Password changed successfully"}) 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) { func (h *UserHandler) GetProfile(c *gin.Context) {
appCtx := appcontext.FromGinContext(c.Request.Context()) appCtx := appcontext.FromGinContext(c.Request.Context())
if appCtx.UserID == uuid.Nil { if appCtx.UserID == uuid.Nil {

View File

@ -15,6 +15,7 @@ type UserService interface {
GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error)
ListUsers(ctx context.Context, req *contract.ListUsersRequest) (*contract.ListUsersResponse, error) ListUsers(ctx context.Context, req *contract.ListUsersRequest) (*contract.ListUsersResponse, error)
ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) 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) GetProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error)
UpdateProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error) UpdateProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error)

View File

@ -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) 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) profile, err := p.profileRepo.GetByUserID(ctx, updated.ID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get user profile: %w", err) 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 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 { func (p *UserProcessorImpl) ActivateUser(ctx context.Context, userID uuid.UUID) error {
_, err := p.userRepo.GetByID(ctx, userID) _, err := p.userRepo.GetByID(ctx, userID)
if err != nil { if err != nil {

View File

@ -15,6 +15,7 @@ type UserHandler interface {
GetUserProfile(c *gin.Context) GetUserProfile(c *gin.Context)
UpdateProfile(c *gin.Context) UpdateProfile(c *gin.Context)
ChangePassword(c *gin.Context) ChangePassword(c *gin.Context)
ChangeUserPassword(c *gin.Context)
ListTitles(c *gin.Context) ListTitles(c *gin.Context)
GetActiveUsersForMention(c *gin.Context) GetActiveUsersForMention(c *gin.Context)
} }

View File

@ -103,6 +103,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
users.GET("/:id/profile", r.userHandler.GetUserProfile) users.GET("/:id/profile", r.userHandler.GetUserProfile)
users.PUT("/profile", r.userHandler.UpdateProfile) users.PUT("/profile", r.userHandler.UpdateProfile)
users.PUT("/:id/password", r.userHandler.ChangePassword) users.PUT("/:id/password", r.userHandler.ChangePassword)
users.PUT("/:id/user-password", r.userHandler.ChangeUserPassword)
users.GET("/titles", r.userHandler.ListTitles) users.GET("/titles", r.userHandler.ListTitles)
users.GET("/mention", r.userHandler.GetActiveUsersForMention) users.GET("/mention", r.userHandler.GetActiveUsersForMention)
users.POST("/profile/avatar", r.fileHandler.UploadProfileAvatar) users.POST("/profile/avatar", r.fileHandler.UploadProfileAvatar)

View File

@ -17,6 +17,7 @@ type UserProcessor interface {
GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error)
GetUserEntityByEmail(ctx context.Context, email string) (*entities.User, error) GetUserEntityByEmail(ctx context.Context, email string) (*entities.User, error)
ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) 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) GetUserRoles(ctx context.Context, userID uuid.UUID) ([]contract.RoleResponse, error)
GetUserPermissionCodes(ctx context.Context, userID uuid.UUID) ([]string, error) GetUserPermissionCodes(ctx context.Context, userID uuid.UUID) ([]string, error)

View File

@ -52,7 +52,7 @@ func (s *UserServiceImpl) ListUsers(ctx context.Context, req *contract.ListUsers
if page <= 0 { if page <= 0 {
page = 1 page = 1
} }
limit := req.Limit limit := req.Limit
if limit <= 0 { if limit <= 0 {
limit = 10 limit = 10
@ -60,7 +60,7 @@ func (s *UserServiceImpl) ListUsers(ctx context.Context, req *contract.ListUsers
if limit > 100 { if limit > 100 {
limit = 100 // Max limit to prevent performance issues limit = 100 // Max limit to prevent performance issues
} }
offset := (page - 1) * limit offset := (page - 1) * limit
// Pass calculated offset and limit to processor // 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) 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) { func (s *UserServiceImpl) GetProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error) {
prof, err := s.userProcessor.GetUserProfile(ctx, userID) prof, err := s.userProcessor.GetUserProfile(ctx, userID)
if err != nil { if err != nil {
@ -114,6 +118,6 @@ func (s *UserServiceImpl) GetActiveUsersForMention(ctx context.Context, search *
if limit > 100 { if limit > 100 {
limit = 100 // Max limit to prevent performance issues limit = 100 // Max limit to prevent performance issues
} }
return s.userProcessor.GetActiveUsersForMention(ctx, search, limit) return s.userProcessor.GetActiveUsersForMention(ctx, search, limit)
} }