diff --git a/dto/user_dto.go b/dto/user_dto.go index 783e473..784a232 100644 --- a/dto/user_dto.go +++ b/dto/user_dto.go @@ -1,5 +1,10 @@ package dto +import ( + "regexp" + "strings" +) + type UserResponseDTO struct { ID string `json:"id"` Username string `json:"username"` @@ -11,3 +16,79 @@ type UserResponseDTO struct { CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } + +type UpdateUserDTO struct { + Name string `json:"name"` + Phone string `json:"phone"` + Email string `json:"email"` +} + +func (r *UpdateUserDTO) Validate() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.Name) == "" { + errors["name"] = append(errors["name"], "Name is required") + } + + if strings.TrimSpace(r.Phone) == "" { + errors["phone"] = append(errors["phone"], "Phone number is required") + } else if !IsValidPhoneNumber(r.Phone) { + errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits") + } + + if strings.TrimSpace(r.Email) == "" { + errors["email"] = append(errors["email"], "Email is required") + } else if !IsValidEmail(r.Email) { + errors["email"] = append(errors["email"], "Invalid email format") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} + +func IsUpdateValidPhoneNumber(phone string) bool { + + re := regexp.MustCompile(`^\+62\d{9,13}$`) + return re.MatchString(phone) +} + +func IsUPdateValidEmail(email string) bool { + + re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + return re.MatchString(email) +} + +type UpdatePasswordDTO struct { + OldPassword string `json:"old_password"` + NewPassword string `json:"new_password"` + ConfirmNewPassword string `json:"confirm_new_password"` +} + +func (u *UpdatePasswordDTO) Validate() (map[string][]string, bool) { + errors := make(map[string][]string) + + if u.OldPassword == "" { + errors["old_password"] = append(errors["old_password"], "Old password is required") + } + + if u.NewPassword == "" { + errors["new_password"] = append(errors["new_password"], "New password is required") + } else if len(u.NewPassword) < 8 { + errors["new_password"] = append(errors["new_password"], "Password must be at least 8 characters long") + } + + if u.ConfirmNewPassword == "" { + errors["confirm_new_password"] = append(errors["confirm_new_password"], "Confirm new password is required") + } else if u.NewPassword != u.ConfirmNewPassword { + errors["confirm_new_password"] = append(errors["confirm_new_password"], "Passwords do not match") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index edb98ac..d6622f2 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -49,7 +49,7 @@ func (h *UserHandler) Register(c *fiber.Ctx) error { user, err := h.UserService.Register(registerDTO) if err != nil { - return utils.ErrorResponse(c, err.Error()) + return utils.GenericErrorResponse(c, fiber.StatusConflict, err.Error()) } createdAt, _ := utils.FormatDateToIndonesianFormat(user.CreatedAt) diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go index cbe60d3..7b54f2b 100644 --- a/internal/handler/user_handler.go +++ b/internal/handler/user_handler.go @@ -2,6 +2,7 @@ package handler import ( "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/dto" "github.com/pahmiudahgede/senggoldong/internal/services" "github.com/pahmiudahgede/senggoldong/utils" ) @@ -28,3 +29,51 @@ func (h *UserProfileHandler) GetUserProfile(c *fiber.Ctx) error { return utils.LogResponse(c, userProfile, "User profile retrieved successfully") } + +func (h *UserProfileHandler) UpdateUserProfile(c *fiber.Ctx) error { + var updateData dto.UpdateUserDTO + if err := c.BodyParser(&updateData); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) + } + + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") + } + + errors, valid := updateData.Validate() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + userResponse, err := h.UserProfileService.UpdateUserProfile(userID, updateData) + if err != nil { + return utils.GenericErrorResponse(c, fiber.StatusConflict, err.Error()) + } + + return utils.LogResponse(c, userResponse, "User profile updated successfully") +} + +func (h *UserProfileHandler) UpdateUserPassword(c *fiber.Ctx) error { + var passwordData dto.UpdatePasswordDTO + if err := c.BodyParser(&passwordData); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) + } + + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") + } + + errors, valid := passwordData.Validate() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + userResponse, err := h.UserProfileService.UpdateUserPassword(userID, passwordData) + if err != nil { + return utils.GenericErrorResponse(c, fiber.StatusBadRequest, err.Error()) + } + + return utils.LogResponse(c, userResponse, "Password updated successfully") +} diff --git a/internal/repositories/user_repo.go b/internal/repositories/user_repo.go index f01595c..4f80f4d 100644 --- a/internal/repositories/user_repo.go +++ b/internal/repositories/user_repo.go @@ -7,6 +7,7 @@ import ( type UserProfileRepository interface { FindByID(userID string) (*model.User, error) + Update(user *model.User) error } type userProfileRepository struct { @@ -25,3 +26,11 @@ func (r *userProfileRepository) FindByID(userID string) (*model.User, error) { } return &user, nil } + +func (r *userProfileRepository) Update(user *model.User) error { + err := r.DB.Save(user).Error + if err != nil { + return err + } + return nil +} diff --git a/internal/services/user_service.go b/internal/services/user_service.go index 214e77a..5ed3ff4 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -9,13 +9,18 @@ import ( "github.com/pahmiudahgede/senggoldong/dto" "github.com/pahmiudahgede/senggoldong/internal/repositories" "github.com/pahmiudahgede/senggoldong/utils" + "golang.org/x/crypto/bcrypt" ) type UserProfileService interface { GetUserProfile(userID string) (*dto.UserResponseDTO, error) + UpdateUserProfile(userID string, updateData dto.UpdateUserDTO) (*dto.UserResponseDTO, error) + UpdateUserPassword(userID string, passwordData dto.UpdatePasswordDTO) (*dto.UserResponseDTO, error) } type userProfileService struct { + UserRepo repositories.UserRepository + RoleRepo repositories.RoleRepository UserProfileRepo repositories.UserProfileRepository } @@ -71,3 +76,115 @@ func (s *userProfileService) GetUserProfile(userID string) (*dto.UserResponseDTO return userResponse, nil } + +func (s *userProfileService) UpdateUserProfile(userID string, updateData dto.UpdateUserDTO) (*dto.UserResponseDTO, error) { + + user, err := s.UserProfileRepo.FindByID(userID) + if err != nil { + return nil, errors.New("user not found") + } + + validationErrors, valid := updateData.Validate() + if !valid { + return nil, fmt.Errorf("validation failed: %v", validationErrors) + } + + if updateData.Name != "" { + user.Name = updateData.Name + } + + if updateData.Phone != "" && updateData.Phone != user.Phone { + + existingPhone, _ := s.UserRepo.FindByPhoneAndRole(updateData.Phone, user.RoleID) + if existingPhone != nil { + return nil, fmt.Errorf("phone number is already used for this role") + } + user.Phone = updateData.Phone + } + + if updateData.Email != "" && updateData.Email != user.Email { + + existingEmail, _ := s.UserRepo.FindByEmailAndRole(updateData.Email, user.RoleID) + if existingEmail != nil { + return nil, fmt.Errorf("email is already used for this role") + } + user.Email = updateData.Email + } + + err = s.UserProfileRepo.Update(user) + if err != nil { + return nil, fmt.Errorf("failed to update user: %v", err) + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(user.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(user.UpdatedAt) + + userResponse := &dto.UserResponseDTO{ + ID: user.ID, + Username: user.Username, + Name: user.Name, + Phone: user.Phone, + Email: user.Email, + EmailVerified: user.EmailVerified, + RoleName: user.Role.RoleName, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + cacheKey := fmt.Sprintf("userProfile:%s", userID) + cacheData := map[string]interface{}{ + "data": userResponse, + } + err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) + if err != nil { + fmt.Printf("Error updating cached user profile in Redis: %v\n", err) + } + + return userResponse, nil +} + +func (s *userProfileService) UpdateUserPassword(userID string, passwordData dto.UpdatePasswordDTO) (*dto.UserResponseDTO, error) { + + validationErrors, valid := passwordData.Validate() + if !valid { + return nil, fmt.Errorf("validation failed: %v", validationErrors) + } + + user, err := s.UserProfileRepo.FindByID(userID) + if err != nil { + return nil, errors.New("user not found") + } + + if !CheckPasswordHash(passwordData.OldPassword, user.Password) { + return nil, errors.New("old password is incorrect") + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(passwordData.NewPassword), bcrypt.DefaultCost) + if err != nil { + return nil, fmt.Errorf("failed to hash new password: %v", err) + } + + user.Password = string(hashedPassword) + + err = s.UserProfileRepo.Update(user) + if err != nil { + return nil, fmt.Errorf("failed to update password: %v", err) + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(user.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(user.UpdatedAt) + + userResponse := &dto.UserResponseDTO{ + ID: user.ID, + Username: user.Username, + Name: user.Name, + Phone: user.Phone, + Email: user.Email, + EmailVerified: user.EmailVerified, + RoleName: user.Role.RoleName, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + return userResponse, nil +} diff --git a/presentation/user_route.go b/presentation/user_route.go index 14deec6..c4fc7ac 100644 --- a/presentation/user_route.go +++ b/presentation/user_route.go @@ -15,4 +15,6 @@ func UserProfileRouter(api fiber.Router) { userProfileHandler := handler.NewUserProfileHandler(userProfileService) api.Get("/user", middleware.AuthMiddleware, userProfileHandler.GetUserProfile) + api.Put("/user/update-user", middleware.AuthMiddleware, userProfileHandler.UpdateUserProfile) + api.Post("/user/update-user-password", middleware.AuthMiddleware, userProfileHandler.UpdateUserPassword) }