diff --git a/api/handlers/user_handler.go b/api/handlers/user_handler.go index f4adf70..e27443d 100644 --- a/api/handlers/user_handler.go +++ b/api/handlers/user_handler.go @@ -1,6 +1,7 @@ package handlers import ( + "context" "net/http" "api-shiners/pkg/user" @@ -42,3 +43,46 @@ func (ctrl *UserController) GetUserByID(c *fiber.Ctx) error { return utils.Success(c, http.StatusOK, "Get user by ID successfully", user, nil) } + +func (ctrl *UserController) SetUserRole(c *fiber.Ctx) error { + ctx := context.Background() + + // 🔹 Ambil user_id dari URL + userIDParam := c.Params("id") + userID, err := uuid.Parse(userIDParam) + if err != nil { + return utils.Error(c, http.StatusBadRequest, "Invalid user ID format", "InvalidUUID", nil) + } + + // 🔹 Ambil nama role dari body + var req struct { + Role string `json:"role"` + } + + if err := c.BodyParser(&req); err != nil { + return utils.Error(c, http.StatusBadRequest, "Invalid request body", "BadRequestException", nil) + } + + if req.Role == "" { + return utils.Error(c, http.StatusBadRequest, "Role name is required", "BadRequestException", nil) + } + + // 🔹 Jalankan service → ambil user hasil update + updatedUser, err := ctrl.userService.SetUserRole(ctx, userID, req.Role) + if err != nil { + return utils.Error(c, http.StatusBadRequest, err.Error(), "SetRoleException", nil) + } + + // 🔹 Siapkan response data + data := fiber.Map{ + "user_id": updatedUser.ID, + "name": updatedUser.Name, + "email": updatedUser.Email, + "role": req.Role, + } + + // 🔹 Response sukses + return utils.Success(c, http.StatusOK, "User role assigned successfully", data, nil) +} + + diff --git a/api/routes/user_routes.go b/api/routes/user_routes.go index 431ad9f..254177d 100644 --- a/api/routes/user_routes.go +++ b/api/routes/user_routes.go @@ -11,4 +11,6 @@ func UserRoutes(app *fiber.App, userController *handlers.UserController) { api := app.Group("/api") api.Get("/users", middleware.AdminMiddleware, userController.GetAllUsers) api.Get("/users/:id", middleware.AuthMiddleware, userController.GetUserByID) + + api.Post("/users/:id/role", middleware.AdminMiddleware, userController.SetUserRole) } \ No newline at end of file diff --git a/main.go b/main.go index d5fdc29..c552af8 100644 --- a/main.go +++ b/main.go @@ -18,8 +18,8 @@ import ( "api-shiners/api/handlers" "api-shiners/api/routes" "api-shiners/pkg/auth" - "api-shiners/pkg/user" "api-shiners/pkg/config" + "api-shiners/pkg/user" _ "api-shiners/docs" @@ -65,7 +65,7 @@ func main() { healthController := handlers.NewHealthController() userRepo := user.NewUserRepository(config.DB) - userService := user.NewUserService(userRepo) + userService := user.NewUserService(userRepo, authRepo) userController := handlers.NewUserController(userService) routes.UserRoutes(app, userController) diff --git a/pkg/auth/repository.go b/pkg/auth/repository.go index 203ec29..3cd6e0e 100644 --- a/pkg/auth/repository.go +++ b/pkg/auth/repository.go @@ -10,12 +10,13 @@ import ( "gorm.io/gorm" ) -type UserRepository interface { +type AuthRepository interface { FindByEmail(ctx context.Context, email string) (*entities.User, error) CreateUser(ctx context.Context, user *entities.User) error FindRoleByName(ctx context.Context, name string) (*entities.Role, error) AssignUserRole(ctx context.Context, userRole *entities.UserRole) error UpdateUser(ctx context.Context, user *entities.User) error + RemoveAllRolesFromUser(ctx context.Context, userID uuid.UUID) error // 🔹 Tambahan untuk Forgot & Reset Password SaveResetToken(ctx context.Context, userID uuid.UUID, token string, expiresAt time.Time) error @@ -28,7 +29,7 @@ type userRepository struct { db *gorm.DB } -func NewUserRepository(db *gorm.DB) UserRepository { +func NewUserRepository(db *gorm.DB) AuthRepository { return &userRepository{db: db} } @@ -62,9 +63,33 @@ func (r *userRepository) FindRoleByName(ctx context.Context, name string) (*enti } func (r *userRepository) AssignUserRole(ctx context.Context, userRole *entities.UserRole) error { + // Cek apakah user sudah punya role ini + var existing entities.UserRole + err := r.db.WithContext(ctx). + Where("user_id = ? AND role_id = ?", userRole.UserID, userRole.RoleID). + First(&existing).Error + + if err == nil { + // Sudah ada, tidak perlu insert ulang + return nil + } + + if !errors.Is(err, gorm.ErrRecordNotFound) { + // Error lain (misal DB error) + return err + } + + // Belum ada → insert baru return r.db.WithContext(ctx).Create(userRole).Error } +func (r *userRepository) RemoveAllRolesFromUser(ctx context.Context, userID uuid.UUID) error { + return r.db.WithContext(ctx). + Where("user_id = ?", userID). + Delete(&entities.UserRole{}).Error +} + + func (r *userRepository) UpdateUser(ctx context.Context, user *entities.User) error { return r.db.WithContext(ctx).Save(user).Error } diff --git a/pkg/auth/service.go b/pkg/auth/service.go index 88188ec..2b50715 100644 --- a/pkg/auth/service.go +++ b/pkg/auth/service.go @@ -34,10 +34,10 @@ type AuthService interface { } type authService struct { - userRepo UserRepository + userRepo AuthRepository } -func NewAuthService(userRepo UserRepository) AuthService { +func NewAuthService(userRepo AuthRepository) AuthService { return &authService{userRepo: userRepo} } diff --git a/pkg/test/auth_test.go b/pkg/test/auth_test.go index 9ae755b..c615c6f 100644 --- a/pkg/test/auth_test.go +++ b/pkg/test/auth_test.go @@ -233,3 +233,8 @@ func TestResetPassword_InvalidToken(t *testing.T) { assert.Error(t, err) assert.EqualError(t, err, "invalid or expired token") } + +func (m *MockUserRepo) RemoveAllRolesFromUser(ctx context.Context, userID uuid.UUID) error { + args := m.Called(ctx, userID) + return args.Error(0) +} diff --git a/pkg/user/repository.go b/pkg/user/repository.go index 05f52d7..2874367 100644 --- a/pkg/user/repository.go +++ b/pkg/user/repository.go @@ -31,3 +31,7 @@ func (r *userRepository) GetByID(id uuid.UUID) (entities.User, error) { err := r.db.Preload("Roles").First(&user, "id = ?", id).Error return user, err } + + + + diff --git a/pkg/user/service.go b/pkg/user/service.go index 02b0c9a..8a628a5 100644 --- a/pkg/user/service.go +++ b/pkg/user/service.go @@ -1,29 +1,35 @@ package user import ( + "api-shiners/pkg/auth" + "api-shiners/pkg/config" + "api-shiners/pkg/entities" "context" "encoding/json" + "errors" "fmt" - "log" "time" - "api-shiners/pkg/config" - "api-shiners/pkg/entities" - "github.com/google/uuid" ) type UserService interface { GetAllUsers() ([]entities.User, error) GetUserByID(id uuid.UUID) (entities.User, error) + SetUserRole(ctx context.Context, userID uuid.UUID, roleName string) (*entities.User, error) } type userService struct { userRepo UserRepository + authRepo auth.AuthRepository } -func NewUserService(userRepo UserRepository) UserService { - return &userService{userRepo: userRepo} +// ✅ Constructor tunggal — wajib dipakai +func NewUserService(userRepo UserRepository, authRepo auth.AuthRepository) UserService { + return &userService{ + userRepo: userRepo, + authRepo: authRepo, + } } // ===== GET ALL USERS (dengan caching Redis opsional) ===== @@ -33,15 +39,13 @@ func (s *userService) GetAllUsers() ([]entities.User, error) { var users []entities.User - // 🔹 Coba ambil dari Redis (jika aktif) + // 🔹 Coba ambil dari Redis if config.RedisClient != nil { val, err := config.RedisClient.Get(ctx, cacheKey).Result() if err == nil && val != "" { if err := json.Unmarshal([]byte(val), &users); err == nil { return users, nil } - } else if err != nil && err.Error() != "redis: client is closed" { - log.Println("⚠️ Redis not available or not running, skip caching...") } } @@ -51,51 +55,83 @@ func (s *userService) GetAllUsers() ([]entities.User, error) { return nil, err } - // 🔹 Simpan ke cache (jika Redis aktif) + // 🔹 Simpan ke Redis if config.RedisClient != nil { data, _ := json.Marshal(users) - err := config.RedisClient.Set(ctx, cacheKey, data, 5*time.Minute).Err() - if err != nil { - log.Println("⚠️ Failed to cache users:", err) - } + config.RedisClient.Set(ctx, cacheKey, data, 5*time.Minute) } return users, nil } -// ===== GET USER BY ID (dengan caching Redis opsional) ===== +// ===== GET USER BY ID ===== func (s *userService) GetUserByID(id uuid.UUID) (entities.User, error) { ctx := context.Background() cacheKey := fmt.Sprintf("user:%s", id.String()) var user entities.User - // 🔹 Coba ambil dari Redis (jika aktif) if config.RedisClient != nil { val, err := config.RedisClient.Get(ctx, cacheKey).Result() if err == nil && val != "" { if err := json.Unmarshal([]byte(val), &user); err == nil { return user, nil } - } else if err != nil && err.Error() != "redis: client is closed" { - log.Println("⚠️ Redis not available or not running, skip caching...") } } - // 🔹 Ambil dari DB user, err := s.userRepo.GetByID(id) if err != nil { return entities.User{}, err } - // 🔹 Simpan ke Redis (jika aktif) if config.RedisClient != nil { data, _ := json.Marshal(user) - err := config.RedisClient.Set(ctx, cacheKey, data, 10*time.Minute).Err() - if err != nil { - log.Println("⚠️ Failed to cache user:", err) - } + config.RedisClient.Set(ctx, cacheKey, data, 10*time.Minute) } return user, nil } + +// ===== SET USER ROLE ===== +func (s *userService) SetUserRole(ctx context.Context, userID uuid.UUID, roleName string) (*entities.User, error) { + // Cek dependency dulu + if s.userRepo == nil || s.authRepo == nil { + return nil, errors.New("userRepo atau authRepo belum diinisialisasi dengan benar") + } + + // 1. Cari user berdasarkan ID + user, err := s.userRepo.GetByID(userID) + if err != nil { + return nil, errors.New("user not found") + } + + // 2. Cari role berdasarkan nama + role, err := s.authRepo.FindRoleByName(ctx, roleName) + if err != nil { + return nil, errors.New("role not found") + } + + // 3. Hapus semua role lama user + if err := s.authRepo.RemoveAllRolesFromUser(ctx, user.ID); err != nil { + return nil, fmt.Errorf("failed to clear old roles: %v", err) + } + + // 4. Tambahkan role baru + userRole := &entities.UserRole{ + UserID: user.ID, + RoleID: role.ID, + } + if err := s.authRepo.AssignUserRole(ctx, userRole); err != nil { + return nil, err + } + + // 5. Ambil ulang user untuk dikembalikan (dengan role terbaru) + updatedUser, err := s.userRepo.GetByID(userID) + if err != nil { + return nil, err + } + + return &updatedUser, nil +} +