feat: add role validation schema and validation field
This commit is contained in:
parent
12f18c529b
commit
15c0e5ab85
|
@ -17,3 +17,6 @@ REDIS_DB=
|
|||
|
||||
# Keyauth
|
||||
API_KEY=
|
||||
|
||||
#SECRET_KEY
|
||||
SECRET_KEY=
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
package handler
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
package services
|
|
@ -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"`
|
||||
}
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -19,7 +19,8 @@ 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)
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue