Skip to content

Commit 995c31d

Browse files
add rbac
1 parent c3fa821 commit 995c31d

File tree

11 files changed

+99
-14
lines changed

11 files changed

+99
-14
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ name: CI
22

33
on:
44
push:
5-
branches: [ main ]
5+
branches: [ "**" ]
66
pull_request:
7-
branches: [ main ]
7+
branches: [ "**" ]
88

99
jobs:
1010
test:

data/seed_users.json

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,55 +6,62 @@
66
"password": "123456",
77
"first_name": "John",
88
"last_name": "Doe",
9-
"is_active": true
9+
"is_active": true,
10+
"role": "admin"
1011
},
1112
{
1213
"username": "janedoe",
1314
"email": "[email protected]",
1415
"password": "123456",
1516
"first_name": "Jane",
1617
"last_name": "Doe",
17-
"is_active": true
18+
"is_active": true,
19+
"role": "user"
1820
},
1921
{
2022
"username": "testuser1",
2123
"email": "[email protected]",
2224
"password": "123456",
2325
"first_name": "Test",
2426
"last_name": "User1",
25-
"is_active": true
27+
"is_active": true,
28+
"role": "user"
2629
},
2730
{
2831
"username": "testuser2",
2932
"email": "[email protected]",
3033
"password": "123456",
3134
"first_name": "Test",
3235
"last_name": "User2",
33-
"is_active": true
36+
"is_active": true,
37+
"role": "user"
3438
},
3539
{
3640
"username": "inactiveuser",
3741
"email": "[email protected]",
3842
"password": "123456",
3943
"first_name": "Test",
4044
"last_name": "User2",
41-
"is_active": false
45+
"is_active": false,
46+
"role": "user"
4247
},
4348
{
4449
"username": "admin",
4550
"email": "[email protected]",
4651
"password": "admin123",
4752
"first_name": "Admin",
4853
"last_name": "User",
49-
"is_active": true
54+
"is_active": true,
55+
"role": "admin"
5056
},
5157
{
5258
"username": "qa_tester",
5359
"email": "[email protected]",
5460
"password": "qa123456",
5561
"first_name": "QA",
5662
"last_name": "Tester",
57-
"is_active": true
63+
"is_active": true,
64+
"role": "user"
5865
}
5966
]
6067
}

internal/database/migrations/000001_create_users_table.up.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS users (
66
first_name VARCHAR(50) NOT NULL,
77
last_name VARCHAR(50) NOT NULL,
88
is_active BOOLEAN DEFAULT TRUE,
9+
role VARCHAR(50) NOT NULL DEFAULT 'user',
910
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
1011
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
1112
deleted_at TIMESTAMP WITH TIME ZONE

internal/database/seed.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type SeedUser struct {
1717
FirstName string `json:"first_name"`
1818
LastName string `json:"last_name"`
1919
IsActive bool `json:"is_active"`
20+
Role string `json:"role"`
2021
}
2122

2223
type SeedData struct {
@@ -60,6 +61,7 @@ func Seed(db *gorm.DB) error {
6061
FirstName: seedUser.FirstName,
6162
LastName: seedUser.LastName,
6263
IsActive: seedUser.IsActive,
64+
Role: seedUser.Role,
6365
}
6466

6567
if err := db.Create(user).Error; err != nil {

internal/handlers/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ func (s *Server) setupRoutes() {
8787
{
8888
protected.GET("/profile", userHandler.GetProfile)
8989
protected.GET("/users/:id", middleware.AuthorizeSelf(), userHandler.GetUser)
90+
protected.GET("/users", middleware.RestrictToRoles("admin", "superuser"), userHandler.GetAllUsers)
9091
}
9192
}
9293

internal/handlers/user.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,19 @@ func (h *UserHandler) GetUser(c *gin.Context) {
141141
c.JSON(http.StatusOK, gin.H{"user": user})
142142
}
143143

144+
// @Summary Get all users
145+
// @Description Retrieve a list of all users
146+
// @Tags Users
147+
// @Produce json
148+
// @Success 200 {object} map[string]interface{} "Users retrieved successfully"
149+
// @Failure 500 {object} map[string]string "Internal server error"
150+
// @Router /api/v1/users [get]
151+
func (h *UserHandler) GetAllUsers(c *gin.Context) {
152+
users, err := h.userService.GetAllUsers()
153+
if err != nil {
154+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
155+
return
156+
}
157+
158+
c.JSON(http.StatusOK, gin.H{"users": users})
159+
}

internal/middleware/auth.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
2828
}
2929

3030
c.Set("user_id", claims.UserID)
31-
c.Set("user_email", claims.Email)
31+
c.Set("role", claims.Role)
3232
c.Next()
3333
}
3434
}
@@ -70,4 +70,32 @@ func AuthorizeSelf() gin.HandlerFunc {
7070

7171
c.Next()
7272
}
73+
}
74+
75+
func RestrictToRoles(allowedRoles ...string) gin.HandlerFunc {
76+
return func(c *gin.Context) {
77+
userRoleRaw, exists := c.Get("role")
78+
if !exists {
79+
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
80+
c.Abort()
81+
return
82+
}
83+
84+
userRole, ok := userRoleRaw.(string)
85+
if !ok {
86+
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid user role in context"})
87+
c.Abort()
88+
return
89+
}
90+
91+
for _, role := range allowedRoles {
92+
if userRole == role {
93+
c.Next()
94+
return
95+
}
96+
}
97+
98+
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
99+
c.Abort()
100+
}
73101
}

internal/models/user.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type User struct {
1414
FirstName string `json:"first_name" validate:"min=2,max=50"`
1515
LastName string `json:"last_name" validate:"min=2,max=50"`
1616
IsActive bool `json:"is_active" gorm:"default:true"`
17+
Role string `json:"role" validate:"required,oneof=admin user moderator support superuser" gorm:"default:'user'"`
1718
CreatedAt time.Time `json:"created_at"`
1819
UpdatedAt time.Time `json:"updated_at"`
1920
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
@@ -26,6 +27,7 @@ type UserResponse struct {
2627
FirstName string `json:"first_name"`
2728
LastName string `json:"last_name"`
2829
IsActive bool `json:"is_active"`
30+
Role string `json:"role"`
2931
CreatedAt time.Time `json:"created_at"`
3032
}
3133

@@ -40,6 +42,7 @@ type RegisterRequest struct {
4042
Password string `json:"password" validate:"required,min=6"`
4143
FirstName string `json:"first_name" validate:"required,min=2,max=50"`
4244
LastName string `json:"last_name" validate:"required,min=2,max=50"`
45+
Role string `json:"role" validate:"omitempty,oneof=admin user moderator support superuser" gorm:"default:'user'"`
4346
}
4447

4548
func (u *User) ToResponse() UserResponse {
@@ -50,6 +53,7 @@ func (u *User) ToResponse() UserResponse {
5053
FirstName: u.FirstName,
5154
LastName: u.LastName,
5255
IsActive: u.IsActive,
56+
Role: u.Role,
5357
CreatedAt: u.CreatedAt,
5458
}
5559
}

internal/repository/user_cache.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type userCacheEntry struct {
3232
FirstName string `json:"first_name"`
3333
LastName string `json:"last_name"`
3434
IsActive bool `json:"is_active"`
35+
Role string `json:"role"`
3536
CreatedAt time.Time `json:"created_at"`
3637
UpdatedAt time.Time `json:"updated_at"`
3738
}
@@ -45,6 +46,7 @@ func toCache(user *models.User) *userCacheEntry {
4546
FirstName: user.FirstName,
4647
LastName: user.LastName,
4748
IsActive: user.IsActive,
49+
Role: user.Role,
4850
CreatedAt: user.CreatedAt,
4951
UpdatedAt: user.UpdatedAt,
5052
}
@@ -59,6 +61,7 @@ func toModel(entry *userCacheEntry) *models.User {
5961
FirstName: entry.FirstName,
6062
LastName: entry.LastName,
6163
IsActive: entry.IsActive,
64+
Role: entry.Role,
6265
CreatedAt: entry.CreatedAt,
6366
UpdatedAt: entry.UpdatedAt,
6467
}
@@ -102,6 +105,10 @@ func (r *cachedUserRepository) Create(user *models.User) error {
102105
}
103106

104107
ctx := context.Background()
108+
109+
if saved, err := r.base.GetByID(user.ID); err == nil && saved != nil {
110+
user = saved
111+
}
105112

106113
r.set(ctx, r.keyByID(user.ID), user)
107114
r.set(ctx, r.keyByEmail(user.Email), user)

internal/services/user.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,18 @@ func (s *UserService) Register(req *models.RegisterRequest) (*models.UserRespons
3939
return nil, err
4040
}
4141

42+
if req.Role == "" {
43+
req.Role = "user"
44+
}
45+
4246
user := &models.User{
4347
Username: req.Username,
4448
Email: req.Email,
4549
PasswordHash: hashedPassword,
4650
FirstName: req.FirstName,
4751
LastName: req.LastName,
4852
IsActive: true,
53+
Role: req.Role,
4954
}
5055

5156
if err := s.userRepo.Create(user); err != nil {
@@ -75,7 +80,7 @@ func (s *UserService) Login(req *models.LoginRequest) (string, *models.UserRespo
7580

7681
token, err := utils.GenerateJWT(
7782
user.ID,
78-
user.Email,
83+
user.Role,
7984
s.config.JWT.Secret,
8085
s.config.JWT.ExpirationHours,
8186
)
@@ -99,4 +104,18 @@ func (s *UserService) GetUserByID(id uint) (*models.UserResponse, error) {
99104

100105
response := user.ToResponse()
101106
return &response, nil
107+
}
108+
109+
func (s *UserService) GetAllUsers() ([]models.UserResponse, error) {
110+
users, err := s.userRepo.List(50, 0)
111+
if err != nil {
112+
return nil, err
113+
}
114+
115+
var responses []models.UserResponse
116+
for _, user := range users {
117+
responses = append(responses, user.ToResponse())
118+
}
119+
120+
return responses, nil
102121
}

0 commit comments

Comments
 (0)