feat: add feature register with custom validation
This commit is contained in:
parent
3e882bc61f
commit
12f18c529b
|
@ -1,5 +1,10 @@
|
||||||
package dto
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type LoginDTO struct {
|
type LoginDTO struct {
|
||||||
Identifier string `json:"identifier" validate:"required"`
|
Identifier string `json:"identifier" validate:"required"`
|
||||||
Password string `json:"password" validate:"required,min=6"`
|
Password string `json:"password" validate:"required,min=6"`
|
||||||
|
@ -9,3 +14,85 @@ type UserResponseWithToken struct {
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
Token string `json:"token"`
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -39,6 +39,48 @@ func (h *UserHandler) Login(c *fiber.Ctx) error {
|
||||||
return utils.LogResponse(c, user, "Login successful")
|
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 {
|
func (h *UserHandler) Logout(c *fiber.Ctx) error {
|
||||||
|
|
||||||
token := c.Get("Authorization")
|
token := c.Get("Authorization")
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
type UserRepository interface {
|
type UserRepository interface {
|
||||||
FindByEmailOrUsernameOrPhone(identifier string) (*model.User, error)
|
FindByEmailOrUsernameOrPhone(identifier string) (*model.User, error)
|
||||||
|
Create(user *model.User) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type userRepository struct {
|
type userRepository struct {
|
||||||
|
@ -25,3 +26,11 @@ func (r *userRepository) FindByEmailOrUsernameOrPhone(identifier string) (*model
|
||||||
}
|
}
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) Create(user *model.User) error {
|
||||||
|
err := r.DB.Create(user).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
|
|
||||||
type UserService interface {
|
type UserService interface {
|
||||||
Login(credentials dto.LoginDTO) (*dto.UserResponseWithToken, error)
|
Login(credentials dto.LoginDTO) (*dto.UserResponseWithToken, error)
|
||||||
|
Register(user dto.RegisterDTO) (*model.User, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type userService struct {
|
type userService struct {
|
||||||
|
@ -73,4 +74,31 @@ func (s *userService) generateJWT(user *model.User) (string, error) {
|
||||||
func CheckPasswordHash(password, hashedPassword string) bool {
|
func CheckPasswordHash(password, hashedPassword string) bool {
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -23,5 +23,6 @@ func AuthRouter(app *fiber.App) {
|
||||||
userHandler := handler.NewUserHandler(userService)
|
userHandler := handler.NewUserHandler(userService)
|
||||||
|
|
||||||
api.Post("/login", userHandler.Login)
|
api.Post("/login", userHandler.Login)
|
||||||
|
api.Post("/register", userHandler.Register)
|
||||||
api.Post("/logout", userHandler.Logout)
|
api.Post("/logout", userHandler.Logout)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue