diff --git a/.env.example b/.env.example index 40797fc..4b313a9 100644 --- a/.env.example +++ b/.env.example @@ -17,3 +17,6 @@ REDIS_DB= # Keyauth API_KEY= + +#SECRET_KEY +SECRET_KEY= diff --git a/config/database.go b/config/database.go index ffbc685..094fbb7 100644 --- a/config/database.go +++ b/config/database.go @@ -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) } diff --git a/dto/auth_dto.go b/dto/auth_dto.go index 4c5e799..2a05237 100644 --- a/dto/auth_dto.go +++ b/dto/auth_dto.go @@ -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 diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index 8ad1bd3..3749e6b 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -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, } diff --git a/internal/handler/role_handler.go b/internal/handler/role_handler.go new file mode 100644 index 0000000..cd97792 --- /dev/null +++ b/internal/handler/role_handler.go @@ -0,0 +1 @@ +package handler \ No newline at end of file diff --git a/internal/repositories/auth_repo.go b/internal/repositories/auth_repo.go index c5ef993..b15d94f 100644 --- a/internal/repositories/auth_repo.go +++ b/internal/repositories/auth_repo.go @@ -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 diff --git a/internal/repositories/role_repo.go b/internal/repositories/role_repo.go new file mode 100644 index 0000000..f3f8ba4 --- /dev/null +++ b/internal/repositories/role_repo.go @@ -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 +} diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 96c9a69..b2dd068 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -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 } diff --git a/internal/services/role_service.go b/internal/services/role_service.go new file mode 100644 index 0000000..c1ce6ce --- /dev/null +++ b/internal/services/role_service.go @@ -0,0 +1 @@ +package services \ No newline at end of file diff --git a/model/role_model.go b/model/role_model.go new file mode 100644 index 0000000..5f595e0 --- /dev/null +++ b/model/role_model.go @@ -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"` +} diff --git a/model/user_model.go b/model/user_model.go index e7f6806..eb5143f 100644 --- a/model/user_model.go +++ b/model/user_model.go @@ -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"` } diff --git a/presentation/auth_route.go b/presentation/auth_route.go index 563e07c..687492f 100644 --- a/presentation/auth_route.go +++ b/presentation/auth_route.go @@ -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) diff --git a/utils/role_permission.go b/utils/role_permission.go index 51d59eb..e892bdd 100644 --- a/utils/role_permission.go +++ b/utils/role_permission.go @@ -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" )