feat: add role validation schema and validation field

This commit is contained in:
pahmiudahgede 2025-01-29 15:38:16 +07:00
parent 12f18c529b
commit 15c0e5ab85
13 changed files with 184 additions and 39 deletions

View File

@ -17,3 +17,6 @@ REDIS_DB=
# Keyauth # Keyauth
API_KEY= API_KEY=
#SECRET_KEY
SECRET_KEY=

View File

@ -30,7 +30,10 @@ func ConnectDatabase() {
} }
log.Println("Database connected successfully!") log.Println("Database connected successfully!")
err = DB.AutoMigrate(&model.User{}) err = DB.AutoMigrate(
&model.User{},
&model.Role{},
)
if err != nil { if err != nil {
log.Fatalf("Error performing auto-migration: %v", err) log.Fatalf("Error performing auto-migration: %v", err)
} }

View File

@ -6,13 +6,15 @@ import (
) )
type LoginDTO struct { type LoginDTO struct {
Identifier string `json:"identifier" validate:"required"` RoleID string `json:"roleid"`
Password string `json:"password" validate:"required,min=6"` Identifier string `json:"identifier"`
Password string `json:"password"`
} }
type UserResponseWithToken struct { type UserResponseWithToken struct {
UserID string `json:"user_id"` UserID string `json:"user_id"`
Token string `json:"token"` RoleName string `json:"loginas"`
Token string `json:"token"`
} }
type RegisterDTO struct { type RegisterDTO struct {
@ -22,6 +24,7 @@ type RegisterDTO struct {
Email string `json:"email"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
ConfirmPassword string `json:"confirm_password"` ConfirmPassword string `json:"confirm_password"`
RoleID string `json:"roleId,omitempty"`
} }
type UserResponseDTO struct { type UserResponseDTO struct {
@ -31,10 +34,30 @@ type UserResponseDTO struct {
Phone string `json:"phone"` Phone string `json:"phone"`
Email string `json:"email"` Email string `json:"email"`
EmailVerified bool `json:"emailVerified"` EmailVerified bool `json:"emailVerified"`
RoleName string `json:"role"`
CreatedAt string `json:"createdAt"` CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"` UpdatedAt string `json:"updatedAt"`
} }
func (l *LoginDTO) Validate() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(l.RoleID) == "" {
errors["roleid"] = append(errors["roleid"], "Role ID is required")
}
if strings.TrimSpace(l.Identifier) == "" {
errors["identifier"] = append(errors["identifier"], "Identifier (username, email, or phone) is required")
}
if strings.TrimSpace(l.Password) == "" {
errors["password"] = append(errors["password"], "Password is required")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
func (r *RegisterDTO) Validate() (map[string][]string, bool) { func (r *RegisterDTO) Validate() (map[string][]string, bool) {
errors := make(map[string][]string) errors := make(map[string][]string)
@ -68,6 +91,9 @@ func (r *RegisterDTO) Validate() (map[string][]string, bool) {
} else if r.Password != r.ConfirmPassword { } else if r.Password != r.ConfirmPassword {
errors["confirm_password"] = append(errors["confirm_password"], "Password and confirm password do not match") errors["confirm_password"] = append(errors["confirm_password"], "Password and confirm password do not match")
} }
if strings.TrimSpace(r.RoleID) == "" {
errors["roleId"] = append(errors["roleId"], "RoleID is required")
}
if len(errors) > 0 { if len(errors) > 0 {
return errors, false return errors, false

View File

@ -1,6 +1,8 @@
package handler package handler
import ( import (
"errors"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/pahmiudahgede/senggoldong/dto" "github.com/pahmiudahgede/senggoldong/dto"
"github.com/pahmiudahgede/senggoldong/internal/services" "github.com/pahmiudahgede/senggoldong/internal/services"
@ -22,18 +24,23 @@ func (h *UserHandler) Login(c *fiber.Ctx) error {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
} }
validationErrors, valid := loginDTO.Validate()
if !valid {
return utils.ValidationErrorResponse(c, validationErrors)
}
user, err := h.UserService.Login(loginDTO) user, err := h.UserService.Login(loginDTO)
if err != nil { if err != nil {
if err.Error() == "user not found" { if err.Error() == "akun dengan role tersebut belum terdaftar" {
return utils.GenericErrorResponse(c, fiber.StatusNotFound, "akun dengan role tersebut belum terdaftar")
return utils.ErrorResponse(c, "User not found")
} }
if err == bcrypt.ErrMismatchedHashAndPassword { if err.Error() == "password yang anda masukkan salah" {
return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "password yang anda masukkan salah")
return utils.ErrorResponse(c, "Invalid password")
} }
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return utils.InternalServerErrorResponse(c, "Error logging in") return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "password yang anda masukkan salah")
}
return utils.GenericErrorResponse(c, fiber.StatusNotFound, "akun tidak ditemukan")
} }
return utils.LogResponse(c, user, "Login successful") return utils.LogResponse(c, user, "Login successful")
@ -46,9 +53,7 @@ func (h *UserHandler) Register(c *fiber.Ctx) error {
} }
errors, valid := registerDTO.Validate() errors, valid := registerDTO.Validate()
if !valid { if !valid {
return utils.ValidationErrorResponse(c, errors) return utils.ValidationErrorResponse(c, errors)
} }
@ -57,15 +62,8 @@ func (h *UserHandler) Register(c *fiber.Ctx) error {
return utils.ErrorResponse(c, err.Error()) return utils.ErrorResponse(c, err.Error())
} }
createdAt, err := utils.FormatDateToIndonesianFormat(user.CreatedAt) createdAt, _ := utils.FormatDateToIndonesianFormat(user.CreatedAt)
if err != nil { updatedAt, _ := utils.FormatDateToIndonesianFormat(user.UpdatedAt)
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{ userResponse := dto.UserResponseDTO{
ID: user.ID, ID: user.ID,
@ -74,6 +72,7 @@ func (h *UserHandler) Register(c *fiber.Ctx) error {
Phone: user.Phone, Phone: user.Phone,
Email: user.Email, Email: user.Email,
EmailVerified: user.EmailVerified, EmailVerified: user.EmailVerified,
RoleName: user.Role.RoleName,
CreatedAt: createdAt, CreatedAt: createdAt,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
} }

View File

@ -0,0 +1 @@
package handler

View File

@ -6,7 +6,11 @@ import (
) )
type UserRepository interface { type UserRepository interface {
FindByIdentifierAndRole(identifier, roleID string) (*model.User, error)
FindByEmailOrUsernameOrPhone(identifier string) (*model.User, error) FindByEmailOrUsernameOrPhone(identifier string) (*model.User, error)
FindByUsername(username string) (*model.User, error)
FindByPhoneAndRole(phone, roleID string) (*model.User, error)
FindByEmailAndRole(email, roleID string) (*model.User, error)
Create(user *model.User) error Create(user *model.User) error
} }
@ -18,6 +22,42 @@ func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{DB: db} return &userRepository{DB: db}
} }
func (r *userRepository) FindByIdentifierAndRole(identifier, roleID string) (*model.User, error) {
var user model.User
err := r.DB.Preload("Role").Where("(email = ? OR username = ? OR phone = ?) AND role_id = ?", identifier, identifier, identifier, roleID).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByUsername(username string) (*model.User, error) {
var user model.User
err := r.DB.Where("username = ?", username).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByPhoneAndRole(phone, roleID string) (*model.User, error) {
var user model.User
err := r.DB.Where("phone = ? AND role_id = ?", phone, roleID).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByEmailAndRole(email, roleID string) (*model.User, error) {
var user model.User
err := r.DB.Where("email = ? AND role_id = ?", email, roleID).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByEmailOrUsernameOrPhone(identifier string) (*model.User, error) { func (r *userRepository) FindByEmailOrUsernameOrPhone(identifier string) (*model.User, error) {
var user model.User var user model.User
err := r.DB.Where("email = ? OR username = ? OR phone = ?", identifier, identifier, identifier).First(&user).Error err := r.DB.Where("email = ? OR username = ? OR phone = ?", identifier, identifier, identifier).First(&user).Error

View File

@ -0,0 +1,27 @@
package repositories
import (
"github.com/pahmiudahgede/senggoldong/model"
"gorm.io/gorm"
)
type RoleRepository interface {
FindByID(id string) (*model.Role, error)
}
type roleRepository struct {
DB *gorm.DB
}
func NewRoleRepository(db *gorm.DB) RoleRepository {
return &roleRepository{DB: db}
}
func (r *roleRepository) FindByID(id string) (*model.Role, error) {
var role model.Role
err := r.DB.Where("id = ?", id).First(&role).Error
if err != nil {
return nil, err
}
return &role, nil
}

View File

@ -1,6 +1,7 @@
package services package services
import ( import (
"errors"
"fmt" "fmt"
"time" "time"
@ -19,23 +20,26 @@ type UserService interface {
type userService struct { type userService struct {
UserRepo repositories.UserRepository UserRepo repositories.UserRepository
RoleRepo repositories.RoleRepository
SecretKey string SecretKey string
} }
func NewUserService(userRepo repositories.UserRepository, secretKey string) UserService { func NewUserService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository, secretKey string) UserService {
return &userService{UserRepo: userRepo, SecretKey: secretKey} return &userService{UserRepo: userRepo, RoleRepo: roleRepo, SecretKey: secretKey}
} }
func (s *userService) Login(credentials dto.LoginDTO) (*dto.UserResponseWithToken, error) { func (s *userService) Login(credentials dto.LoginDTO) (*dto.UserResponseWithToken, error) {
if credentials.RoleID == "" {
return nil, errors.New("roleId is required")
}
user, err := s.UserRepo.FindByEmailOrUsernameOrPhone(credentials.Identifier) user, err := s.UserRepo.FindByIdentifierAndRole(credentials.Identifier, credentials.RoleID)
if err != nil { if err != nil {
return nil, errors.New("akun dengan role tersebut belum terdaftar")
return nil, fmt.Errorf("user not found")
} }
if !CheckPasswordHash(credentials.Password, user.Password) { if !CheckPasswordHash(credentials.Password, user.Password) {
return nil, bcrypt.ErrMismatchedHashAndPassword return nil, errors.New("password yang anda masukkan salah")
} }
token, err := s.generateJWT(user) token, err := s.generateJWT(user)
@ -49,8 +53,9 @@ func (s *userService) Login(credentials dto.LoginDTO) (*dto.UserResponseWithToke
} }
return &dto.UserResponseWithToken{ return &dto.UserResponseWithToken{
UserID: user.ID, RoleName: user.Role.RoleName,
Token: token, UserID: user.ID,
Token: token,
}, nil }, nil
} }
@ -77,11 +82,34 @@ func CheckPasswordHash(password, hashedPassword string) bool {
} }
func (s *userService) Register(user dto.RegisterDTO) (*model.User, error) { func (s *userService) Register(user dto.RegisterDTO) (*model.User, error) {
if user.Password != user.ConfirmPassword { if user.Password != user.ConfirmPassword {
return nil, fmt.Errorf("password and confirm password do not match") return nil, fmt.Errorf("password and confirm password do not match")
} }
if user.RoleID == "" {
return nil, fmt.Errorf("roleId is required")
}
role, err := s.RoleRepo.FindByID(user.RoleID)
if err != nil {
return nil, fmt.Errorf("invalid roleId")
}
existingUser, _ := s.UserRepo.FindByUsername(user.Username)
if existingUser != nil {
return nil, fmt.Errorf("username is already taken")
}
existingPhone, _ := s.UserRepo.FindByPhoneAndRole(user.Phone, user.RoleID)
if existingPhone != nil {
return nil, fmt.Errorf("phone number is already used for this role")
}
existingEmail, _ := s.UserRepo.FindByEmailAndRole(user.Email, user.RoleID)
if existingEmail != nil {
return nil, fmt.Errorf("email is already used for this role")
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to hash password: %v", err) return nil, fmt.Errorf("failed to hash password: %v", err)
@ -93,6 +121,7 @@ func (s *userService) Register(user dto.RegisterDTO) (*model.User, error) {
Phone: user.Phone, Phone: user.Phone,
Email: user.Email, Email: user.Email,
Password: string(hashedPassword), Password: string(hashedPassword),
RoleID: user.RoleID,
} }
err = s.UserRepo.Create(&newUser) err = s.UserRepo.Create(&newUser)
@ -100,5 +129,7 @@ func (s *userService) Register(user dto.RegisterDTO) (*model.User, error) {
return nil, fmt.Errorf("failed to create user: %v", err) return nil, fmt.Errorf("failed to create user: %v", err)
} }
newUser.Role = *role
return &newUser, nil return &newUser, nil
} }

View File

@ -0,0 +1 @@
package services

11
model/role_model.go Normal file
View File

@ -0,0 +1,11 @@
package model
import "time"
type Role struct {
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"`
RoleName string `gorm:"unique;not null" json:"roleName"`
Users []User `gorm:"foreignKey:RoleID" json:"users"`
CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"`
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"`
}

View File

@ -11,6 +11,8 @@ type User struct {
Email string `gorm:"not null" json:"email"` Email string `gorm:"not null" json:"email"`
EmailVerified bool `gorm:"default:false" json:"emailVerified"` EmailVerified bool `gorm:"default:false" json:"emailVerified"`
Password string `gorm:"not null" json:"password"` Password string `gorm:"not null" json:"password"`
RoleID string `gorm:"not null" json:"roleId"`
Role Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"`
CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"`
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"`
} }

View File

@ -19,9 +19,10 @@ func AuthRouter(app *fiber.App) {
} }
userRepo := repositories.NewUserRepository(config.DB) userRepo := repositories.NewUserRepository(config.DB)
userService := services.NewUserService(userRepo, secretKey) roleRepo := repositories.NewRoleRepository(config.DB)
userService := services.NewUserService(userRepo, roleRepo, secretKey)
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("/register", userHandler.Register)
api.Post("/logout", userHandler.Logout) api.Post("/logout", userHandler.Logout)

View File

@ -1,8 +1,8 @@
package utils package utils
const ( const (
RoleMasyarakat = "63191315-c59f-4af9-91a7-367c698cc486" RoleAdministrator = "46f75bb9-7f64-44b7-b378-091a67b3e229"
RolePengepul = "bda3827a-3c61-459a-9a95-42e2bb88d737" RoleMasyarakat = "6cfa867b-536c-448d-ba11-fe060b5af971"
RolePengelola = "fc75351d-eded-4314-a41b-e4a901e6540c" RolePengepul = "8171883c-ea9e-4d17-9f28-a7896d88380f"
RoleAdministrator = "fe4a15ce-5a0c-40d0-9be0-a7d4b6d05480" RolePengelola = "84d72ddb-68a8-430c-9b79-5d71f90cb1be"
) )