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
API_KEY=
#SECRET_KEY
SECRET_KEY=

View File

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

View File

@ -6,13 +6,15 @@ import (
)
type LoginDTO struct {
Identifier string `json:"identifier" validate:"required"`
Password string `json:"password" validate:"required,min=6"`
RoleID string `json:"roleid"`
Identifier string `json:"identifier"`
Password string `json:"password"`
}
type UserResponseWithToken struct {
UserID string `json:"user_id"`
Token string `json:"token"`
UserID string `json:"user_id"`
RoleName string `json:"loginas"`
Token string `json:"token"`
}
type RegisterDTO struct {
@ -22,6 +24,7 @@ type RegisterDTO struct {
Email string `json:"email"`
Password string `json:"password"`
ConfirmPassword string `json:"confirm_password"`
RoleID string `json:"roleId,omitempty"`
}
type UserResponseDTO struct {
@ -31,10 +34,30 @@ type UserResponseDTO struct {
Phone string `json:"phone"`
Email string `json:"email"`
EmailVerified bool `json:"emailVerified"`
RoleName string `json:"role"`
CreatedAt string `json:"createdAt"`
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) {
errors := make(map[string][]string)
@ -68,6 +91,9 @@ func (r *RegisterDTO) Validate() (map[string][]string, bool) {
} else if r.Password != r.ConfirmPassword {
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 {
return errors, false

View File

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

View File

@ -0,0 +1 @@
package handler

View File

@ -6,7 +6,11 @@ import (
)
type UserRepository interface {
FindByIdentifierAndRole(identifier, roleID 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
}
@ -18,6 +22,42 @@ func NewUserRepository(db *gorm.DB) UserRepository {
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) {
var user model.User
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
import (
"errors"
"fmt"
"time"
@ -19,23 +20,26 @@ type UserService interface {
type userService struct {
UserRepo repositories.UserRepository
RoleRepo repositories.RoleRepository
SecretKey string
}
func NewUserService(userRepo repositories.UserRepository, secretKey string) UserService {
return &userService{UserRepo: userRepo, SecretKey: secretKey}
func NewUserService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository, secretKey string) UserService {
return &userService{UserRepo: userRepo, RoleRepo: roleRepo, SecretKey: secretKey}
}
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 {
return nil, fmt.Errorf("user not found")
return nil, errors.New("akun dengan role tersebut belum terdaftar")
}
if !CheckPasswordHash(credentials.Password, user.Password) {
return nil, bcrypt.ErrMismatchedHashAndPassword
return nil, errors.New("password yang anda masukkan salah")
}
token, err := s.generateJWT(user)
@ -49,8 +53,9 @@ func (s *userService) Login(credentials dto.LoginDTO) (*dto.UserResponseWithToke
}
return &dto.UserResponseWithToken{
UserID: user.ID,
Token: token,
RoleName: user.Role.RoleName,
UserID: user.ID,
Token: token,
}, nil
}
@ -77,11 +82,34 @@ func CheckPasswordHash(password, hashedPassword string) bool {
}
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")
}
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)
if err != nil {
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,
Email: user.Email,
Password: string(hashedPassword),
RoleID: user.RoleID,
}
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)
}
newUser.Role = *role
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"`
EmailVerified bool `gorm:"default:false" json:"emailVerified"`
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"`
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)
userService := services.NewUserService(userRepo, secretKey)
roleRepo := repositories.NewRoleRepository(config.DB)
userService := services.NewUserService(userRepo, roleRepo, secretKey)
userHandler := handler.NewUserHandler(userService)
api.Post("/login", userHandler.Login)
api.Post("/register", userHandler.Register)
api.Post("/logout", userHandler.Logout)

View File

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