diff --git a/dto/auth_dto.go b/dto/auth_dto.go index b308725..4c5e799 100644 --- a/dto/auth_dto.go +++ b/dto/auth_dto.go @@ -1,5 +1,10 @@ package dto +import ( + "regexp" + "strings" +) + type LoginDTO struct { Identifier string `json:"identifier" validate:"required"` Password string `json:"password" validate:"required,min=6"` @@ -9,3 +14,85 @@ type UserResponseWithToken struct { UserID string `json:"user_id"` Token string `json:"token"` } + +type RegisterDTO struct { + Username string `json:"username"` + Name string `json:"name"` + Phone string `json:"phone"` + Email string `json:"email"` + Password string `json:"password"` + ConfirmPassword string `json:"confirm_password"` +} + +type UserResponseDTO struct { + ID string `json:"id"` + Username string `json:"username"` + Name string `json:"name"` + Phone string `json:"phone"` + Email string `json:"email"` + EmailVerified bool `json:"emailVerified"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +func (r *RegisterDTO) Validate() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.Username) == "" { + errors["username"] = append(errors["username"], "Username is required") + } + 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 strings.TrimSpace(r.Password) == "" { + errors["password"] = append(errors["password"], "Password is required") + } else if !IsValidPassword(r.Password) { + errors["password"] = append(errors["password"], "Password must be at least 8 characters long and contain at least one number") + } + + if strings.TrimSpace(r.ConfirmPassword) == "" { + errors["confirm_password"] = append(errors["confirm_password"], "Confirm password is required") + } else if r.Password != r.ConfirmPassword { + errors["confirm_password"] = append(errors["confirm_password"], "Password and confirm password do not match") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} + +func IsValidPhoneNumber(phone string) bool { + + re := regexp.MustCompile(`^\+62\d{9,13}$`) + return re.MatchString(phone) +} + +func IsValidEmail(email string) bool { + + re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + return re.MatchString(email) +} + +func IsValidPassword(password string) bool { + if len(password) < 8 { + return false + } + + re := regexp.MustCompile(`\d`) + return re.MatchString(password) +} diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index b9617ab..8ad1bd3 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -39,6 +39,48 @@ func (h *UserHandler) Login(c *fiber.Ctx) error { return utils.LogResponse(c, user, "Login successful") } +func (h *UserHandler) Register(c *fiber.Ctx) error { + var registerDTO dto.RegisterDTO + if err := c.BodyParser(®isterDTO); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) + } + + errors, valid := registerDTO.Validate() + + if !valid { + + return utils.ValidationErrorResponse(c, errors) + } + + user, err := h.UserService.Register(registerDTO) + if err != nil { + return utils.ErrorResponse(c, err.Error()) + } + + createdAt, err := utils.FormatDateToIndonesianFormat(user.CreatedAt) + if err != nil { + return utils.InternalServerErrorResponse(c, "Error formatting created date") + } + + updatedAt, err := utils.FormatDateToIndonesianFormat(user.UpdatedAt) + if err != nil { + return utils.InternalServerErrorResponse(c, "Error formatting updated date") + } + + userResponse := dto.UserResponseDTO{ + ID: user.ID, + Username: user.Username, + Name: user.Name, + Phone: user.Phone, + Email: user.Email, + EmailVerified: user.EmailVerified, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + return utils.LogResponse(c, userResponse, "Registration successful") +} + func (h *UserHandler) Logout(c *fiber.Ctx) error { token := c.Get("Authorization") diff --git a/internal/repositories/auth_repo.go b/internal/repositories/auth_repo.go index 5035d32..c5ef993 100644 --- a/internal/repositories/auth_repo.go +++ b/internal/repositories/auth_repo.go @@ -7,6 +7,7 @@ import ( type UserRepository interface { FindByEmailOrUsernameOrPhone(identifier string) (*model.User, error) + Create(user *model.User) error } type userRepository struct { @@ -25,3 +26,11 @@ func (r *userRepository) FindByEmailOrUsernameOrPhone(identifier string) (*model } return &user, nil } + +func (r *userRepository) Create(user *model.User) error { + err := r.DB.Create(user).Error + if err != nil { + return err + } + return nil +} diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 9bce0d1..96c9a69 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -14,6 +14,7 @@ import ( type UserService interface { Login(credentials dto.LoginDTO) (*dto.UserResponseWithToken, error) + Register(user dto.RegisterDTO) (*model.User, error) } type userService struct { @@ -73,4 +74,31 @@ func (s *userService) generateJWT(user *model.User) (string, error) { func CheckPasswordHash(password, hashedPassword string) bool { err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) return err == nil -} \ No newline at end of file +} + +func (s *userService) Register(user dto.RegisterDTO) (*model.User, error) { + + if user.Password != user.ConfirmPassword { + return nil, fmt.Errorf("password and confirm password do not match") + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) + if err != nil { + return nil, fmt.Errorf("failed to hash password: %v", err) + } + + newUser := model.User{ + Username: user.Username, + Name: user.Name, + Phone: user.Phone, + Email: user.Email, + Password: string(hashedPassword), + } + + err = s.UserRepo.Create(&newUser) + if err != nil { + return nil, fmt.Errorf("failed to create user: %v", err) + } + + return &newUser, nil +} diff --git a/presentation/auth_route.go b/presentation/auth_route.go index a6d4dab..563e07c 100644 --- a/presentation/auth_route.go +++ b/presentation/auth_route.go @@ -23,5 +23,6 @@ func AuthRouter(app *fiber.App) { userHandler := handler.NewUserHandler(userService) api.Post("/login", userHandler.Login) + api.Post("/register", userHandler.Register) api.Post("/logout", userHandler.Logout) }