Skip to content

Commit b5be379

Browse files
update rbac
1 parent 0127012 commit b5be379

File tree

5 files changed

+141
-34
lines changed

5 files changed

+141
-34
lines changed

internal/handlers/server.go

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -121,63 +121,63 @@ func (s *Server) setupRoutes() {
121121
protected.DELETE("/users/:id", middleware.AuthorizeSelf(), userHandler.DeleteUser)
122122

123123
protected.POST("/clubs", clubHandler.CreateClub)
124-
protected.PUT("/clubs/:id", clubHandler.UpdateClub)
125-
protected.DELETE("/clubs/:id", clubHandler.DeleteClub)
124+
protected.PUT("/clubs/:id", middleware.RequireClubMembershipWithRoles(clubRepo, "club_admin"), clubHandler.UpdateClub)
125+
protected.DELETE("/clubs/:id", middleware.RequireClubMembershipWithRoles(clubRepo, "club_admin"), clubHandler.DeleteClub)
126126

127127
protected.POST("/clubs/:id/join", clubHandler.JoinClub)
128-
protected.POST("/clubs/:id/leave", clubHandler.LeaveClub)
129-
protected.POST("/clubs/:id/ratings", clubHandler.RateClub)
128+
protected.POST("/clubs/:id/leave", middleware.RequireClubMembership(clubRepo), clubHandler.LeaveClub)
129+
protected.POST("/clubs/:id/ratings", middleware.RequireClubMembership(clubRepo), clubHandler.RateClub)
130130

131131
protected.GET("/clubs/:id/members", clubHandler.ListClubMembers)
132-
protected.PUT("/clubs/:id/members/:user_id", middleware.AuthorizeSelf(), clubHandler.UpdateClubMember)
132+
protected.PUT("/clubs/:id/members/:user_id", middleware.RequireClubMembershipWithRoles(clubRepo, "club_admin", "moderator"), clubHandler.UpdateClubMember)
133133
protected.GET("/clubs/:id/members/:user_id", clubHandler.GetClubMember)
134134

135-
protected.POST("/clubs/:id/events", middleware.RestrictToRoles("admin", "moderator"), eventHandler.CreateEvent)
135+
protected.POST("/clubs/:id/events", middleware.RequireClubMembershipWithRoles(clubRepo, "club_admin", "moderator"), eventHandler.CreateEvent)
136136
protected.GET("/clubs/:id/events", eventHandler.GetClubEvents)
137137
protected.GET("/events/:id", eventHandler.GetEvent)
138-
protected.PUT("/events/:id", middleware.RestrictToRoles("admin", "moderator"), eventHandler.UpdateEvent)
139-
protected.DELETE("/events/:id", middleware.RestrictToRoles("admin", "moderator"), eventHandler.DeleteEvent)
138+
protected.PUT("/events/:id", middleware.RequireClubMembershipWithRoles(clubRepo, "club_admin", "moderator"), eventHandler.UpdateEvent)
139+
protected.DELETE("/events/:id", middleware.RequireClubMembershipWithRoles(clubRepo, "club_admin", "moderator"), eventHandler.DeleteEvent)
140140

141-
protected.POST("/events/:id/rsvp", eventHandler.RSVPToEvent)
142-
protected.GET("/events/:id/attendees", middleware.RestrictToRoles("admin", "moderator"), eventHandler.GetEventAttendees)
141+
protected.POST("/events/:id/rsvp", middleware.RequireClubMembership(clubRepo), eventHandler.RSVPToEvent)
142+
protected.GET("/events/:id/attendees", middleware.RequireClubMembership(clubRepo), eventHandler.GetEventAttendees)
143143

144-
protected.POST("/books", middleware.RestrictToRoles("admin"), bookHandler.CreateBook)
144+
protected.POST("/books", middleware.RestrictToRoles("admin", "superuser"), bookHandler.CreateBook)
145145
protected.GET("/books/:id", bookHandler.GetBookByID)
146-
protected.PUT("/books/:id", middleware.RestrictToRoles("admin"), bookHandler.UpdateBook)
147-
protected.DELETE("/books/:id", middleware.RestrictToRoles("admin"), bookHandler.DeleteBook)
146+
protected.PUT("/books/:id", middleware.RestrictToRoles("admin", "superuser"), bookHandler.UpdateBook)
147+
protected.DELETE("/books/:id", middleware.RestrictToRoles("admin", "superuser"), bookHandler.DeleteBook)
148148
protected.GET("/books", bookHandler.ListBooks)
149149

150-
protected.POST("/posts", postHandler.CreatePost)
150+
protected.POST("/posts", middleware.RequireClubMembership(clubRepo), postHandler.CreatePost)
151151
protected.GET("/posts/:id", postHandler.GetPostByID)
152-
protected.PUT("/posts/:id", postHandler.UpdatePost)
153-
protected.DELETE("/posts/:id", postHandler.DeletePost)
152+
protected.PUT("/posts/:id", middleware.RequireClubMembership(clubRepo), postHandler.UpdatePost)
153+
protected.DELETE("/posts/:id", middleware.RequireClubMembership(clubRepo), postHandler.DeletePost)
154154
protected.GET("/posts", postHandler.ListAllPosts)
155155

156-
protected.POST("/posts/:id/like", postHandler.LikePost)
157-
protected.POST("/posts/:id/unlike", postHandler.UnlikePost)
156+
protected.POST("/posts/:id/like", middleware.RequireClubMembership(clubRepo), postHandler.LikePost)
157+
protected.POST("/posts/:id/unlike", middleware.RequireClubMembership(clubRepo), postHandler.UnlikePost)
158158
protected.GET("/posts/:id/likes", postHandler.ListLikesByPostID)
159159

160-
protected.POST("/posts/:id/comments", commentHandler.CreateComment)
160+
protected.POST("/posts/:id/comments", middleware.RequireClubMembership(clubRepo), commentHandler.CreateComment)
161161
protected.GET("/comments/:id", commentHandler.GetCommentByID)
162-
protected.PUT("/comments/:id", commentHandler.UpdateComment)
163-
protected.DELETE("/comments/:id", commentHandler.DeleteComment)
162+
protected.PUT("/comments/:id", middleware.RequireClubMembership(clubRepo), commentHandler.UpdateComment)
163+
protected.DELETE("/comments/:id", middleware.RequireClubMembership(clubRepo), commentHandler.DeleteComment)
164164
protected.GET("/posts/:id/comments", commentHandler.ListCommentsByPostID)
165165
protected.GET("/users/:id/comments", commentHandler.ListCommentsByUserID)
166166

167-
protected.POST("/comments/:id/like", commentHandler.LikeComment)
168-
protected.POST("/comments/:id/unlike", commentHandler.UnlikeComment)
167+
protected.POST("/comments/:id/like", middleware.RequireClubMembership(clubRepo), commentHandler.LikeComment)
168+
protected.POST("/comments/:id/unlike", middleware.RequireClubMembership(clubRepo), commentHandler.UnlikeComment)
169169
protected.GET("/comments/:id/likes", commentHandler.ListLikesByCommentID)
170170

171-
protected.POST("/users/:id/reading/sync", readingHandler.SyncUserStats)
171+
protected.POST("/users/:id/reading/sync", middleware.AuthorizeSelf(), readingHandler.SyncUserStats)
172172
protected.POST("/users/:id/reading/start", middleware.AuthorizeSelf(), readingHandler.StartReading)
173173
protected.PATCH("/users/:id/reading/:bookID/progress", middleware.AuthorizeSelf(), readingHandler.UpdateProgress)
174174
protected.POST("/users/:id/reading/:bookID/complete", middleware.AuthorizeSelf(), readingHandler.CompleteReading)
175175
protected.GET("/users/:id/reading", middleware.AuthorizeSelf(), readingHandler.ListUserProgress)
176176
protected.GET("/users/:id/reading/history", readingHandler.UserReadingHistory)
177177

178-
protected.POST("/clubs/:id/reading/assign", middleware.RestrictToRoles("admin", "moderator"), readingHandler.AssignBookToClub)
179-
protected.PATCH("/clubs/:id/reading/checkpoint", middleware.RestrictToRoles("admin", "moderator"), readingHandler.UpdateClubCheckpoint)
180-
protected.POST("/clubs/:id/reading/complete", middleware.RestrictToRoles("admin", "moderator"), readingHandler.CompleteClubAssignment)
178+
protected.POST("/clubs/:id/reading/assign", middleware.RequireClubMembershipWithRoles(clubRepo, "admin", "moderator"), readingHandler.AssignBookToClub)
179+
protected.PATCH("/clubs/:id/reading/checkpoint", middleware.RequireClubMembershipWithRoles(clubRepo, "admin", "moderator"), readingHandler.UpdateClubCheckpoint)
180+
protected.POST("/clubs/:id/reading/complete", middleware.RequireClubMembershipWithRoles(clubRepo, "admin", "moderator"), readingHandler.CompleteClubAssignment)
181181
protected.GET("/clubs/:id/reading", readingHandler.ListClubAssignments)
182182
}
183183
}

internal/middleware/auth.go

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import (
77

88
"github.com/gin-gonic/gin"
99
"github.com/nevzattalhaozcan/forgotten/internal/config"
10+
"github.com/nevzattalhaozcan/forgotten/internal/models"
11+
"github.com/nevzattalhaozcan/forgotten/internal/repository"
1012
"github.com/nevzattalhaozcan/forgotten/pkg/utils"
13+
"gorm.io/gorm"
1114
)
1215

1316
func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
@@ -39,7 +42,7 @@ func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
3942
* If there is no user ID in the path parameter, allow access (for routes that do not require a specific user ID)
4043
*/
4144
func AuthorizeSelf() gin.HandlerFunc {
42-
return func (c *gin.Context) {
45+
return func(c *gin.Context) {
4346
ctxUserIDRaw, exists := c.Get("user_id")
4447
if !exists {
4548
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
@@ -54,6 +57,17 @@ func AuthorizeSelf() gin.HandlerFunc {
5457
return
5558
}
5659

60+
// Allow admin and superuser to access any resource
61+
userRoleRaw, exists := c.Get("user_role")
62+
if exists {
63+
if role, ok := userRoleRaw.(string); ok {
64+
if role == "admin" || role == "superuser" {
65+
c.Next()
66+
return
67+
}
68+
}
69+
}
70+
5771
idParam := c.Param("id")
5872
if idParam == "" {
5973
c.Next()
@@ -106,4 +120,97 @@ func RestrictToRoles(allowedRoles ...string) gin.HandlerFunc {
106120
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
107121
c.Abort()
108122
}
123+
}
124+
125+
func RequireClubMembership(clubRepo repository.ClubRepository) gin.HandlerFunc {
126+
return func(c *gin.Context) {
127+
// allow admin and superuser to bypass club membership check
128+
userRoleRaw, exists := c.Get("user_role")
129+
if exists {
130+
if role, ok := userRoleRaw.(string); ok {
131+
if role == "admin" || role == "superuser" {
132+
c.Next()
133+
return
134+
}
135+
}
136+
}
137+
138+
userID, exists := c.Get("user_id")
139+
if !exists {
140+
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
141+
c.Abort()
142+
return
143+
}
144+
145+
clubIDParam := c.Param("id")
146+
if clubIDParam == "" {
147+
clubIDParam = c.Param("club_id")
148+
}
149+
150+
clubID, err := strconv.ParseUint(clubIDParam, 10, 32)
151+
if err != nil {
152+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid club ID"})
153+
c.Abort()
154+
return
155+
}
156+
157+
membership, err := clubRepo.GetClubMemberByUserID(uint(clubID), userID.(uint))
158+
if err != nil {
159+
if err == gorm.ErrRecordNotFound {
160+
c.JSON(http.StatusForbidden, gin.H{"error": "club membership required"})
161+
} else {
162+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check membership"})
163+
}
164+
c.Abort()
165+
return
166+
}
167+
168+
if !membership.IsApproved {
169+
c.JSON(http.StatusForbidden, gin.H{"error": "membership approval required"})
170+
c.Abort()
171+
return
172+
}
173+
174+
c.Set("club_membership", membership)
175+
c.Next()
176+
}
177+
}
178+
179+
func RequireClubMembershipWithRoles(clubRepo repository.ClubRepository, allowedRoles ...string) gin.HandlerFunc {
180+
return func(c *gin.Context) {
181+
// allow admin and superuser to bypass club membership check
182+
userRoleRaw, exists := c.Get("user_role")
183+
if exists {
184+
if role, ok := userRoleRaw.(string); ok {
185+
if role == "admin" || role == "superuser" {
186+
c.Next()
187+
return
188+
}
189+
}
190+
}
191+
192+
RequireClubMembership(clubRepo)(c)
193+
if c.IsAborted() {
194+
return
195+
}
196+
197+
membership, exists := c.Get("club_membership")
198+
if !exists {
199+
c.JSON(http.StatusInternalServerError, gin.H{"error": "membership check failed"})
200+
c.Abort()
201+
return
202+
}
203+
204+
membershipData := membership.(*models.ClubMembership)
205+
206+
for _, role := range allowedRoles {
207+
if membershipData.Role == role {
208+
c.Next()
209+
return
210+
}
211+
}
212+
213+
c.JSON(http.StatusForbidden, gin.H{"error": "insufficient club permissions"})
214+
c.Abort()
215+
}
109216
}

internal/models/club.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ type UpdateClubRequest struct {
100100

101101
type UpdateClubMembershipRequest struct {
102102
UserID *uint `json:"user_id" validate:"omitempty"`
103-
Role *string `json:"role" validate:"omitempty,oneof=member moderator admin"`
103+
Role *string `json:"role" validate:"omitempty,oneof=member moderator club_admin"`
104104
IsApproved *bool `json:"is_approved" validate:"omitempty"`
105105
}
106106

internal/repository/club.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func (r *clubRepository) CountApprovedMembers(clubID uint) (int64, error) {
111111
func (r *clubRepository) ListClubMembers(clubID uint) ([]*models.ClubMembership, error) {
112112
var members []*models.ClubMembership
113113
err := r.db.
114-
Where("club_id = ?", clubID).
114+
Where("club_id = ? AND is_approved = true", clubID).
115115
Preload("User").
116116
Find(&members).Error
117117
if err != nil {

internal/services/club.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (s *ClubService) CanManageClub(clubID, userID uint) bool {
4949
if !member.IsApproved {
5050
return false
5151
}
52-
return member.Role == "moderator" || member.Role == "admin"
52+
return member.Role == "moderator" || member.Role == "club_admin"
5353
}
5454

5555
func (s *ClubService) CreateClub(ownerID uint, req *models.CreateClubRequest) (*models.ClubResponse, error) {
@@ -80,7 +80,7 @@ func (s *ClubService) CreateClub(ownerID uint, req *models.CreateClubRequest) (*
8080
ownerMembership := &models.ClubMembership{
8181
ClubID: club.ID,
8282
UserID: ownerID,
83-
Role: "admin",
83+
Role: "club_admin",
8484
IsApproved: true,
8585
}
8686
if err := s.clubRepo.JoinClub(ownerMembership); err != nil {
@@ -505,8 +505,8 @@ func (s *ClubService) TransferOwnership(clubID, currentOwnerID, newOwnerID uint)
505505
return err
506506
}
507507

508-
if newOwnerMembership.Role != "admin" {
509-
newOwnerMembership.Role = "admin"
508+
if newOwnerMembership.Role != "club_admin" {
509+
newOwnerMembership.Role = "club_admin"
510510
if err := s.clubRepo.UpdateMembership(newOwnerMembership); err != nil {
511511
return err
512512
}

0 commit comments

Comments
 (0)