Add user Roles

This commit is contained in:
Aditya Siregar 2025-08-09 15:28:25 +07:00
parent 9e95e8ee5e
commit 001d02c587
9 changed files with 486 additions and 1 deletions

View File

@ -43,14 +43,16 @@ func (a *App) Initialize(cfg *config.Config) error {
middlewares := a.initMiddleware(services) middlewares := a.initMiddleware(services)
healthHandler := handler.NewHealthHandler() healthHandler := handler.NewHealthHandler()
fileHandler := handler.NewFileHandler(services.fileService) fileHandler := handler.NewFileHandler(services.fileService)
rbacHandler := handler.NewRBACHandler(services.rbacService)
a.router = router.NewRouter( a.router = router.NewRouter(
cfg, cfg,
handler.NewAuthHandler(services.authService), handler.NewAuthHandler(services.authService),
middlewares.authMiddleware, middlewares.authMiddleware,
healthHandler, healthHandler,
handler.NewUserHandler(services.userService, &validator.UserValidatorImpl{}), handler.NewUserHandler(services.userService, validator.NewUserValidator()),
fileHandler, fileHandler,
rbacHandler,
) )
return nil return nil
@ -99,6 +101,7 @@ type repositories struct {
userRepo *repository.UserRepositoryImpl userRepo *repository.UserRepositoryImpl
userProfileRepo *repository.UserProfileRepository userProfileRepo *repository.UserProfileRepository
titleRepo *repository.TitleRepository titleRepo *repository.TitleRepository
rbacRepo *repository.RBACRepository
} }
func (a *App) initRepositories() *repositories { func (a *App) initRepositories() *repositories {
@ -106,6 +109,7 @@ func (a *App) initRepositories() *repositories {
userRepo: repository.NewUserRepository(a.db), userRepo: repository.NewUserRepository(a.db),
userProfileRepo: repository.NewUserProfileRepository(a.db), userProfileRepo: repository.NewUserProfileRepository(a.db),
titleRepo: repository.NewTitleRepository(a.db), titleRepo: repository.NewTitleRepository(a.db),
rbacRepo: repository.NewRBACRepository(a.db),
} }
} }
@ -123,6 +127,7 @@ type services struct {
userService *service.UserServiceImpl userService *service.UserServiceImpl
authService *service.AuthServiceImpl authService *service.AuthServiceImpl
fileService *service.FileServiceImpl fileService *service.FileServiceImpl
rbacService *service.RBACServiceImpl
} }
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services { func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
@ -137,10 +142,13 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
s3Client := client.NewFileClient(fileCfg) s3Client := client.NewFileClient(fileCfg)
fileSvc := service.NewFileService(s3Client, processors.userProcessor, "profile", "documents") fileSvc := service.NewFileService(s3Client, processors.userProcessor, "profile", "documents")
rbacSvc := service.NewRBACService(repos.rbacRepo)
return &services{ return &services{
userService: userSvc, userService: userSvc,
authService: authService, authService: authService,
fileService: fileSvc, fileService: fileSvc,
rbacService: rbacSvc,
} }
} }

View File

@ -0,0 +1,57 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type PermissionResponse struct {
ID uuid.UUID `json:"id"`
Code string `json:"code"`
Description *string `json:"description,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreatePermissionRequest struct {
Code string `json:"code"` // unique
Description *string `json:"description,omitempty"`
}
type UpdatePermissionRequest struct {
Code *string `json:"code,omitempty"`
Description *string `json:"description,omitempty"`
}
type ListPermissionsResponse struct {
Permissions []PermissionResponse `json:"permissions"`
}
type RoleWithPermissionsResponse struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
Description *string `json:"description,omitempty"`
Permissions []PermissionResponse `json:"permissions"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateRoleRequest struct {
Name string `json:"name"`
Code string `json:"code"`
Description *string `json:"description,omitempty"`
PermissionCodes []string `json:"permission_codes,omitempty"`
}
type UpdateRoleRequest struct {
Name *string `json:"name,omitempty"`
Code *string `json:"code,omitempty"`
Description *string `json:"description,omitempty"`
PermissionCodes *[]string `json:"permission_codes,omitempty"`
}
type ListRolesResponse struct {
Roles []RoleWithPermissionsResponse `json:"roles"`
}

View File

@ -0,0 +1,10 @@
package entities
import "github.com/google/uuid"
type RolePermission struct {
RoleID uuid.UUID `gorm:"type:uuid;primaryKey" json:"role_id"`
PermissionID uuid.UUID `gorm:"type:uuid;primaryKey" json:"permission_id"`
}
func (RolePermission) TableName() string { return "role_permissions" }

View File

@ -0,0 +1,137 @@
package handler
import (
"context"
"net/http"
"eslogad-be/internal/contract"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type RBACService interface {
CreatePermission(ctx context.Context, req *contract.CreatePermissionRequest) (*contract.PermissionResponse, error)
UpdatePermission(ctx context.Context, id uuid.UUID, req *contract.UpdatePermissionRequest) (*contract.PermissionResponse, error)
DeletePermission(ctx context.Context, id uuid.UUID) error
ListPermissions(ctx context.Context) (*contract.ListPermissionsResponse, error)
CreateRole(ctx context.Context, req *contract.CreateRoleRequest) (*contract.RoleWithPermissionsResponse, error)
UpdateRole(ctx context.Context, id uuid.UUID, req *contract.UpdateRoleRequest) (*contract.RoleWithPermissionsResponse, error)
DeleteRole(ctx context.Context, id uuid.UUID) error
ListRoles(ctx context.Context) (*contract.ListRolesResponse, error)
}
type RBACHandler struct{ svc RBACService }
func NewRBACHandler(svc RBACService) *RBACHandler { return &RBACHandler{svc: svc} }
func (h *RBACHandler) CreatePermission(c *gin.Context) {
var req contract.CreatePermissionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
return
}
resp, err := h.svc.CreatePermission(c.Request.Context(), &req)
if err != nil {
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
}
func (h *RBACHandler) UpdatePermission(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
var req contract.UpdatePermissionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
resp, err := h.svc.UpdatePermission(c.Request.Context(), id, &req)
if err != nil {
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
}
func (h *RBACHandler) DeletePermission(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
if err := h.svc.DeletePermission(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "deleted"})
}
func (h *RBACHandler) ListPermissions(c *gin.Context) {
resp, err := h.svc.ListPermissions(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
}
func (h *RBACHandler) CreateRole(c *gin.Context) {
var req contract.CreateRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
resp, err := h.svc.CreateRole(c.Request.Context(), &req)
if err != nil {
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
}
func (h *RBACHandler) UpdateRole(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
var req contract.UpdateRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
resp, err := h.svc.UpdateRole(c.Request.Context(), id, &req)
if err != nil {
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
}
func (h *RBACHandler) DeleteRole(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
return
}
if err := h.svc.DeleteRole(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "deleted"})
}
func (h *RBACHandler) ListRoles(c *gin.Context) {
resp, err := h.svc.ListRoles(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
}
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
}

View File

@ -0,0 +1,97 @@
package repository
import (
"context"
"eslogad-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
type RBACRepository struct {
db *gorm.DB
}
func NewRBACRepository(db *gorm.DB) *RBACRepository { return &RBACRepository{db: db} }
// Permissions
func (r *RBACRepository) CreatePermission(ctx context.Context, p *entities.Permission) error {
return r.db.WithContext(ctx).Create(p).Error
}
func (r *RBACRepository) UpdatePermission(ctx context.Context, p *entities.Permission) error {
return r.db.WithContext(ctx).Model(&entities.Permission{}).Where("id = ?", p.ID).Updates(p).Error
}
func (r *RBACRepository) DeletePermission(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.Permission{}, "id = ?", id).Error
}
func (r *RBACRepository) ListPermissions(ctx context.Context) ([]entities.Permission, error) {
var perms []entities.Permission
if err := r.db.WithContext(ctx).Order("code ASC").Find(&perms).Error; err != nil {
return nil, err
}
return perms, nil
}
func (r *RBACRepository) GetPermissionByCode(ctx context.Context, code string) (*entities.Permission, error) {
var p entities.Permission
if err := r.db.WithContext(ctx).First(&p, "code = ?", code).Error; err != nil {
return nil, err
}
return &p, nil
}
// Roles
func (r *RBACRepository) CreateRole(ctx context.Context, role *entities.Role) error {
return r.db.WithContext(ctx).Create(role).Error
}
func (r *RBACRepository) UpdateRole(ctx context.Context, role *entities.Role) error {
return r.db.WithContext(ctx).Model(&entities.Role{}).Where("id = ?", role.ID).Updates(role).Error
}
func (r *RBACRepository) DeleteRole(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.Role{}, "id = ?", id).Error
}
func (r *RBACRepository) ListRoles(ctx context.Context) ([]entities.Role, error) {
var roles []entities.Role
if err := r.db.WithContext(ctx).Order("name ASC").Find(&roles).Error; err != nil {
return nil, err
}
return roles, nil
}
func (r *RBACRepository) GetRoleByCode(ctx context.Context, code string) (*entities.Role, error) {
var role entities.Role
if err := r.db.WithContext(ctx).First(&role, "code = ?", code).Error; err != nil {
return nil, err
}
return &role, nil
}
func (r *RBACRepository) SetRolePermissionsByCodes(ctx context.Context, roleID uuid.UUID, permCodes []string) error {
if err := r.db.WithContext(ctx).Where("role_id = ?", roleID).Delete(&entities.RolePermission{}).Error; err != nil {
return err
}
if len(permCodes) == 0 {
return nil
}
var perms []entities.Permission
if err := r.db.WithContext(ctx).Where("code IN ?", permCodes).Find(&perms).Error; err != nil {
return err
}
pairs := make([]entities.RolePermission, 0, len(perms))
for _, p := range perms {
pairs = append(pairs, entities.RolePermission{RoleID: roleID, PermissionID: p.ID})
}
return r.db.WithContext(ctx).Create(&pairs).Error
}
func (r *RBACRepository) GetPermissionsByRoleID(ctx context.Context, roleID uuid.UUID) ([]entities.Permission, error) {
var perms []entities.Permission
if err := r.db.WithContext(ctx).
Table("permissions p").
Select("p.*").
Joins("JOIN role_permissions rp ON rp.permission_id = p.id").
Where("rp.role_id = ?", roleID).
Find(&perms).Error; err != nil {
return nil, err
}
return perms, nil
}

View File

@ -18,3 +18,15 @@ type FileHandler interface {
UploadProfileAvatar(c *gin.Context) UploadProfileAvatar(c *gin.Context)
UploadDocument(c *gin.Context) UploadDocument(c *gin.Context)
} }
type RBACHandler interface {
CreatePermission(c *gin.Context)
UpdatePermission(c *gin.Context)
DeletePermission(c *gin.Context)
ListPermissions(c *gin.Context)
CreateRole(c *gin.Context)
UpdateRole(c *gin.Context)
DeleteRole(c *gin.Context)
ListRoles(c *gin.Context)
}

View File

@ -14,6 +14,7 @@ type Router struct {
authMiddleware AuthMiddleware authMiddleware AuthMiddleware
userHandler UserHandler userHandler UserHandler
fileHandler FileHandler fileHandler FileHandler
rbacHandler RBACHandler
} }
func NewRouter( func NewRouter(
@ -23,6 +24,7 @@ func NewRouter(
healthHandler HealthHandler, healthHandler HealthHandler,
userHandler UserHandler, userHandler UserHandler,
fileHandler FileHandler, fileHandler FileHandler,
rbacHandler RBACHandler,
) *Router { ) *Router {
return &Router{ return &Router{
config: cfg, config: cfg,
@ -31,6 +33,7 @@ func NewRouter(
healthHandler: healthHandler, healthHandler: healthHandler,
userHandler: userHandler, userHandler: userHandler,
fileHandler: fileHandler, fileHandler: fileHandler,
rbacHandler: rbacHandler,
} }
} }
@ -77,5 +80,18 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
{ {
files.POST("/documents", r.fileHandler.UploadDocument) files.POST("/documents", r.fileHandler.UploadDocument)
} }
rbac := v1.Group("/rbac")
rbac.Use(r.authMiddleware.RequireAuth())
{
rbac.GET("/permissions", r.rbacHandler.ListPermissions)
rbac.POST("/permissions", r.rbacHandler.CreatePermission)
rbac.PUT("/permissions/:id", r.rbacHandler.UpdatePermission)
rbac.DELETE("/permissions/:id", r.rbacHandler.DeletePermission)
rbac.GET("/roles", r.rbacHandler.ListRoles)
rbac.POST("/roles", r.rbacHandler.CreateRole)
rbac.PUT("/roles/:id", r.rbacHandler.UpdateRole)
rbac.DELETE("/roles/:id", r.rbacHandler.DeleteRole)
}
} }
} }

View File

@ -0,0 +1,128 @@
package service
import (
"context"
"eslogad-be/internal/contract"
"eslogad-be/internal/entities"
"eslogad-be/internal/repository"
"eslogad-be/internal/transformer"
"github.com/google/uuid"
)
type RBACServiceImpl struct {
repo *repository.RBACRepository
}
func NewRBACService(repo *repository.RBACRepository) *RBACServiceImpl {
return &RBACServiceImpl{repo: repo}
}
// Permissions
func (s *RBACServiceImpl) CreatePermission(ctx context.Context, req *contract.CreatePermissionRequest) (*contract.PermissionResponse, error) {
p := &entities.Permission{Code: req.Code}
if req.Description != nil {
p.Description = *req.Description
}
if err := s.repo.CreatePermission(ctx, p); err != nil {
return nil, err
}
return &contract.PermissionResponse{ID: p.ID, Code: p.Code, Description: &p.Description, CreatedAt: p.CreatedAt, UpdatedAt: p.UpdatedAt}, nil
}
func (s *RBACServiceImpl) UpdatePermission(ctx context.Context, id uuid.UUID, req *contract.UpdatePermissionRequest) (*contract.PermissionResponse, error) {
p := &entities.Permission{ID: id}
if req.Code != nil {
p.Code = *req.Code
}
if req.Description != nil {
p.Description = *req.Description
}
if err := s.repo.UpdatePermission(ctx, p); err != nil {
return nil, err
}
// fetch full row
perms, err := s.repo.ListPermissions(ctx)
if err != nil {
return nil, err
}
for _, x := range perms {
if x.ID == id {
return &contract.PermissionResponse{ID: x.ID, Code: x.Code, Description: &x.Description, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt}, nil
}
}
return nil, nil
}
func (s *RBACServiceImpl) DeletePermission(ctx context.Context, id uuid.UUID) error {
return s.repo.DeletePermission(ctx, id)
}
func (s *RBACServiceImpl) ListPermissions(ctx context.Context) (*contract.ListPermissionsResponse, error) {
perms, err := s.repo.ListPermissions(ctx)
if err != nil {
return nil, err
}
return &contract.ListPermissionsResponse{Permissions: transformer.PermissionsToContract(perms)}, nil
}
// Roles
func (s *RBACServiceImpl) CreateRole(ctx context.Context, req *contract.CreateRoleRequest) (*contract.RoleWithPermissionsResponse, error) {
role := &entities.Role{Name: req.Name, Code: req.Code}
if req.Description != nil {
role.Description = *req.Description
}
if err := s.repo.CreateRole(ctx, role); err != nil {
return nil, err
}
if len(req.PermissionCodes) > 0 {
_ = s.repo.SetRolePermissionsByCodes(ctx, role.ID, req.PermissionCodes)
}
perms, _ := s.repo.GetPermissionsByRoleID(ctx, role.ID)
resp := transformer.RoleWithPermissionsToContract(*role, perms)
return &resp, nil
}
func (s *RBACServiceImpl) UpdateRole(ctx context.Context, id uuid.UUID, req *contract.UpdateRoleRequest) (*contract.RoleWithPermissionsResponse, error) {
role := &entities.Role{ID: id}
if req.Name != nil {
role.Name = *req.Name
}
if req.Code != nil {
role.Code = *req.Code
}
if req.Description != nil {
role.Description = *req.Description
}
if err := s.repo.UpdateRole(ctx, role); err != nil {
return nil, err
}
if req.PermissionCodes != nil {
_ = s.repo.SetRolePermissionsByCodes(ctx, id, *req.PermissionCodes)
}
perms, _ := s.repo.GetPermissionsByRoleID(ctx, id)
// fetch updated role
roles, err := s.repo.ListRoles(ctx)
if err != nil {
return nil, err
}
for _, r := range roles {
if r.ID == id {
resp := transformer.RoleWithPermissionsToContract(r, perms)
return &resp, nil
}
}
return nil, nil
}
func (s *RBACServiceImpl) DeleteRole(ctx context.Context, id uuid.UUID) error {
return s.repo.DeleteRole(ctx, id)
}
func (s *RBACServiceImpl) ListRoles(ctx context.Context) (*contract.ListRolesResponse, error) {
roles, err := s.repo.ListRoles(ctx)
if err != nil {
return nil, err
}
out := make([]contract.RoleWithPermissionsResponse, 0, len(roles))
for _, r := range roles {
perms, _ := s.repo.GetPermissionsByRoleID(ctx, r.ID)
out = append(out, transformer.RoleWithPermissionsToContract(r, perms))
}
return &contract.ListRolesResponse{Roles: out}, nil
}

View File

@ -170,3 +170,23 @@ func TitlesToContract(titles []entities.Title) []contract.TitleResponse {
} }
return out return out
} }
func PermissionsToContract(perms []entities.Permission) []contract.PermissionResponse {
out := make([]contract.PermissionResponse, 0, len(perms))
for _, p := range perms {
out = append(out, contract.PermissionResponse{ID: p.ID, Code: p.Code, Description: &p.Description, CreatedAt: p.CreatedAt, UpdatedAt: p.UpdatedAt})
}
return out
}
func RoleWithPermissionsToContract(role entities.Role, perms []entities.Permission) contract.RoleWithPermissionsResponse {
return contract.RoleWithPermissionsResponse{
ID: role.ID,
Name: role.Name,
Code: role.Code,
Description: &role.Description,
Permissions: PermissionsToContract(perms),
CreatedAt: role.CreatedAt,
UpdatedAt: role.UpdatedAt,
}
}