Skip to content

Commit 54e1376

Browse files
authored
Merge pull request #2 from HSI-Boarding-School/auth/user-management
feat: auth, user-management
2 parents 004985f + 347b1ec commit 54e1376

File tree

8 files changed

+146
-30
lines changed

8 files changed

+146
-30
lines changed

api/handlers/user_handler.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package handlers
22

33
import (
4+
"context"
45
"net/http"
56

67
"api-shiners/pkg/user"
@@ -42,3 +43,46 @@ func (ctrl *UserController) GetUserByID(c *fiber.Ctx) error {
4243

4344
return utils.Success(c, http.StatusOK, "Get user by ID successfully", user, nil)
4445
}
46+
47+
func (ctrl *UserController) SetUserRole(c *fiber.Ctx) error {
48+
ctx := context.Background()
49+
50+
// 🔹 Ambil user_id dari URL
51+
userIDParam := c.Params("id")
52+
userID, err := uuid.Parse(userIDParam)
53+
if err != nil {
54+
return utils.Error(c, http.StatusBadRequest, "Invalid user ID format", "InvalidUUID", nil)
55+
}
56+
57+
// 🔹 Ambil nama role dari body
58+
var req struct {
59+
Role string `json:"role"`
60+
}
61+
62+
if err := c.BodyParser(&req); err != nil {
63+
return utils.Error(c, http.StatusBadRequest, "Invalid request body", "BadRequestException", nil)
64+
}
65+
66+
if req.Role == "" {
67+
return utils.Error(c, http.StatusBadRequest, "Role name is required", "BadRequestException", nil)
68+
}
69+
70+
// 🔹 Jalankan service → ambil user hasil update
71+
updatedUser, err := ctrl.userService.SetUserRole(ctx, userID, req.Role)
72+
if err != nil {
73+
return utils.Error(c, http.StatusBadRequest, err.Error(), "SetRoleException", nil)
74+
}
75+
76+
// 🔹 Siapkan response data
77+
data := fiber.Map{
78+
"user_id": updatedUser.ID,
79+
"name": updatedUser.Name,
80+
"email": updatedUser.Email,
81+
"role": req.Role,
82+
}
83+
84+
// 🔹 Response sukses
85+
return utils.Success(c, http.StatusOK, "User role assigned successfully", data, nil)
86+
}
87+
88+

api/routes/user_routes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ func UserRoutes(app *fiber.App, userController *handlers.UserController) {
1111
api := app.Group("/api")
1212
api.Get("/users", middleware.AdminMiddleware, userController.GetAllUsers)
1313
api.Get("/users/:id", middleware.AuthMiddleware, userController.GetUserByID)
14+
15+
api.Post("/users/:id/role", middleware.AdminMiddleware, userController.SetUserRole)
1416
}

main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import (
1818
"api-shiners/api/handlers"
1919
"api-shiners/api/routes"
2020
"api-shiners/pkg/auth"
21-
"api-shiners/pkg/user"
2221
"api-shiners/pkg/config"
22+
"api-shiners/pkg/user"
2323

2424
_ "api-shiners/docs"
2525

@@ -65,7 +65,7 @@ func main() {
6565
healthController := handlers.NewHealthController()
6666

6767
userRepo := user.NewUserRepository(config.DB)
68-
userService := user.NewUserService(userRepo)
68+
userService := user.NewUserService(userRepo, authRepo)
6969
userController := handlers.NewUserController(userService)
7070

7171
routes.UserRoutes(app, userController)

pkg/auth/repository.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import (
1010
"gorm.io/gorm"
1111
)
1212

13-
type UserRepository interface {
13+
type AuthRepository interface {
1414
FindByEmail(ctx context.Context, email string) (*entities.User, error)
1515
CreateUser(ctx context.Context, user *entities.User) error
1616
FindRoleByName(ctx context.Context, name string) (*entities.Role, error)
1717
AssignUserRole(ctx context.Context, userRole *entities.UserRole) error
1818
UpdateUser(ctx context.Context, user *entities.User) error
19+
RemoveAllRolesFromUser(ctx context.Context, userID uuid.UUID) error
1920

2021
// 🔹 Tambahan untuk Forgot & Reset Password
2122
SaveResetToken(ctx context.Context, userID uuid.UUID, token string, expiresAt time.Time) error
@@ -28,7 +29,7 @@ type userRepository struct {
2829
db *gorm.DB
2930
}
3031

31-
func NewUserRepository(db *gorm.DB) UserRepository {
32+
func NewUserRepository(db *gorm.DB) AuthRepository {
3233
return &userRepository{db: db}
3334
}
3435

@@ -62,9 +63,33 @@ func (r *userRepository) FindRoleByName(ctx context.Context, name string) (*enti
6263
}
6364

6465
func (r *userRepository) AssignUserRole(ctx context.Context, userRole *entities.UserRole) error {
66+
// Cek apakah user sudah punya role ini
67+
var existing entities.UserRole
68+
err := r.db.WithContext(ctx).
69+
Where("user_id = ? AND role_id = ?", userRole.UserID, userRole.RoleID).
70+
First(&existing).Error
71+
72+
if err == nil {
73+
// Sudah ada, tidak perlu insert ulang
74+
return nil
75+
}
76+
77+
if !errors.Is(err, gorm.ErrRecordNotFound) {
78+
// Error lain (misal DB error)
79+
return err
80+
}
81+
82+
// Belum ada → insert baru
6583
return r.db.WithContext(ctx).Create(userRole).Error
6684
}
6785

86+
func (r *userRepository) RemoveAllRolesFromUser(ctx context.Context, userID uuid.UUID) error {
87+
return r.db.WithContext(ctx).
88+
Where("user_id = ?", userID).
89+
Delete(&entities.UserRole{}).Error
90+
}
91+
92+
6893
func (r *userRepository) UpdateUser(ctx context.Context, user *entities.User) error {
6994
return r.db.WithContext(ctx).Save(user).Error
7095
}

pkg/auth/service.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ type AuthService interface {
3434
}
3535

3636
type authService struct {
37-
userRepo UserRepository
37+
userRepo AuthRepository
3838
}
3939

40-
func NewAuthService(userRepo UserRepository) AuthService {
40+
func NewAuthService(userRepo AuthRepository) AuthService {
4141
return &authService{userRepo: userRepo}
4242
}
4343

pkg/test/auth_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,8 @@ func TestResetPassword_InvalidToken(t *testing.T) {
233233
assert.Error(t, err)
234234
assert.EqualError(t, err, "invalid or expired token")
235235
}
236+
237+
func (m *MockUserRepo) RemoveAllRolesFromUser(ctx context.Context, userID uuid.UUID) error {
238+
args := m.Called(ctx, userID)
239+
return args.Error(0)
240+
}

pkg/user/repository.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,7 @@ func (r *userRepository) GetByID(id uuid.UUID) (entities.User, error) {
3131
err := r.db.Preload("Roles").First(&user, "id = ?", id).Error
3232
return user, err
3333
}
34+
35+
36+
37+

pkg/user/service.go

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,35 @@
11
package user
22

33
import (
4+
"api-shiners/pkg/auth"
5+
"api-shiners/pkg/config"
6+
"api-shiners/pkg/entities"
47
"context"
58
"encoding/json"
9+
"errors"
610
"fmt"
7-
"log"
811
"time"
912

10-
"api-shiners/pkg/config"
11-
"api-shiners/pkg/entities"
12-
1313
"github.com/google/uuid"
1414
)
1515

1616
type UserService interface {
1717
GetAllUsers() ([]entities.User, error)
1818
GetUserByID(id uuid.UUID) (entities.User, error)
19+
SetUserRole(ctx context.Context, userID uuid.UUID, roleName string) (*entities.User, error)
1920
}
2021

2122
type userService struct {
2223
userRepo UserRepository
24+
authRepo auth.AuthRepository
2325
}
2426

25-
func NewUserService(userRepo UserRepository) UserService {
26-
return &userService{userRepo: userRepo}
27+
// ✅ Constructor tunggal — wajib dipakai
28+
func NewUserService(userRepo UserRepository, authRepo auth.AuthRepository) UserService {
29+
return &userService{
30+
userRepo: userRepo,
31+
authRepo: authRepo,
32+
}
2733
}
2834

2935
// ===== GET ALL USERS (dengan caching Redis opsional) =====
@@ -33,15 +39,13 @@ func (s *userService) GetAllUsers() ([]entities.User, error) {
3339

3440
var users []entities.User
3541

36-
// 🔹 Coba ambil dari Redis (jika aktif)
42+
// 🔹 Coba ambil dari Redis
3743
if config.RedisClient != nil {
3844
val, err := config.RedisClient.Get(ctx, cacheKey).Result()
3945
if err == nil && val != "" {
4046
if err := json.Unmarshal([]byte(val), &users); err == nil {
4147
return users, nil
4248
}
43-
} else if err != nil && err.Error() != "redis: client is closed" {
44-
log.Println("⚠️ Redis not available or not running, skip caching...")
4549
}
4650
}
4751

@@ -51,51 +55,83 @@ func (s *userService) GetAllUsers() ([]entities.User, error) {
5155
return nil, err
5256
}
5357

54-
// 🔹 Simpan ke cache (jika Redis aktif)
58+
// 🔹 Simpan ke Redis
5559
if config.RedisClient != nil {
5660
data, _ := json.Marshal(users)
57-
err := config.RedisClient.Set(ctx, cacheKey, data, 5*time.Minute).Err()
58-
if err != nil {
59-
log.Println("⚠️ Failed to cache users:", err)
60-
}
61+
config.RedisClient.Set(ctx, cacheKey, data, 5*time.Minute)
6162
}
6263

6364
return users, nil
6465
}
6566

66-
// ===== GET USER BY ID (dengan caching Redis opsional) =====
67+
// ===== GET USER BY ID =====
6768
func (s *userService) GetUserByID(id uuid.UUID) (entities.User, error) {
6869
ctx := context.Background()
6970
cacheKey := fmt.Sprintf("user:%s", id.String())
7071

7172
var user entities.User
7273

73-
// 🔹 Coba ambil dari Redis (jika aktif)
7474
if config.RedisClient != nil {
7575
val, err := config.RedisClient.Get(ctx, cacheKey).Result()
7676
if err == nil && val != "" {
7777
if err := json.Unmarshal([]byte(val), &user); err == nil {
7878
return user, nil
7979
}
80-
} else if err != nil && err.Error() != "redis: client is closed" {
81-
log.Println("⚠️ Redis not available or not running, skip caching...")
8280
}
8381
}
8482

85-
// 🔹 Ambil dari DB
8683
user, err := s.userRepo.GetByID(id)
8784
if err != nil {
8885
return entities.User{}, err
8986
}
9087

91-
// 🔹 Simpan ke Redis (jika aktif)
9288
if config.RedisClient != nil {
9389
data, _ := json.Marshal(user)
94-
err := config.RedisClient.Set(ctx, cacheKey, data, 10*time.Minute).Err()
95-
if err != nil {
96-
log.Println("⚠️ Failed to cache user:", err)
97-
}
90+
config.RedisClient.Set(ctx, cacheKey, data, 10*time.Minute)
9891
}
9992

10093
return user, nil
10194
}
95+
96+
// ===== SET USER ROLE =====
97+
func (s *userService) SetUserRole(ctx context.Context, userID uuid.UUID, roleName string) (*entities.User, error) {
98+
// Cek dependency dulu
99+
if s.userRepo == nil || s.authRepo == nil {
100+
return nil, errors.New("userRepo atau authRepo belum diinisialisasi dengan benar")
101+
}
102+
103+
// 1. Cari user berdasarkan ID
104+
user, err := s.userRepo.GetByID(userID)
105+
if err != nil {
106+
return nil, errors.New("user not found")
107+
}
108+
109+
// 2. Cari role berdasarkan nama
110+
role, err := s.authRepo.FindRoleByName(ctx, roleName)
111+
if err != nil {
112+
return nil, errors.New("role not found")
113+
}
114+
115+
// 3. Hapus semua role lama user
116+
if err := s.authRepo.RemoveAllRolesFromUser(ctx, user.ID); err != nil {
117+
return nil, fmt.Errorf("failed to clear old roles: %v", err)
118+
}
119+
120+
// 4. Tambahkan role baru
121+
userRole := &entities.UserRole{
122+
UserID: user.ID,
123+
RoleID: role.ID,
124+
}
125+
if err := s.authRepo.AssignUserRole(ctx, userRole); err != nil {
126+
return nil, err
127+
}
128+
129+
// 5. Ambil ulang user untuk dikembalikan (dengan role terbaru)
130+
updatedUser, err := s.userRepo.GetByID(userID)
131+
if err != nil {
132+
return nil, err
133+
}
134+
135+
return &updatedUser, nil
136+
}
137+

0 commit comments

Comments
 (0)