feat(rbac): M3 RBAC 权限模型完整实现
- 4 表设计: permissions, roles, role_permissions(m2m), user_roles - 多租户隔离: 角色和用户角色分配按 tenant_id 隔离 - 权限并集: 多角色用户取权限并集,无继承 - 系统角色保护: is_system 角色不可修改/删除 - 种子数据: 7 个默认权限 + 3 个系统角色(super_admin/tenant_admin/member) - 权限中间件: RequirePermission 支持按路由粒度控制 - API 路由: /api/v1/rbac/ 下完整 CRUD 端点 - fx 注册: RBAC Module 集成到 app.go
This commit is contained in:
parent
22dba67920
commit
f8c374a624
@ -13,6 +13,7 @@ import (
|
|||||||
"mengstack/internal/config"
|
"mengstack/internal/config"
|
||||||
"mengstack/internal/logger"
|
"mengstack/internal/logger"
|
||||||
authinterfaces "mengstack/internal/modules/auth/interfaces"
|
authinterfaces "mengstack/internal/modules/auth/interfaces"
|
||||||
|
rbacinterfaces "mengstack/internal/modules/rbac/interfaces"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
@ -37,6 +38,7 @@ func newEngine(
|
|||||||
rdb *redis.Client,
|
rdb *redis.Client,
|
||||||
authHandler *authinterfaces.Handler,
|
authHandler *authinterfaces.Handler,
|
||||||
authMW gin.HandlerFunc,
|
authMW gin.HandlerFunc,
|
||||||
|
rbacHandler *rbacinterfaces.Handler,
|
||||||
) *gin.Engine {
|
) *gin.Engine {
|
||||||
ginMode := "release"
|
ginMode := "release"
|
||||||
if cfg.Server.Mode == "debug" || cfg.Server.Mode == "dev" {
|
if cfg.Server.Mode == "debug" || cfg.Server.Mode == "dev" {
|
||||||
@ -56,6 +58,7 @@ func newEngine(
|
|||||||
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||||
|
|
||||||
authinterfaces.SetupRoutes(r, authHandler, authMW)
|
authinterfaces.SetupRoutes(r, authHandler, authMW)
|
||||||
|
rbacinterfaces.SetupRoutes(r, rbacHandler, authMW)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
@ -97,6 +100,7 @@ func NewApp() *fx.App {
|
|||||||
database.Module,
|
database.Module,
|
||||||
cache.Module,
|
cache.Module,
|
||||||
authinterfaces.Module,
|
authinterfaces.Module,
|
||||||
|
rbacinterfaces.Module,
|
||||||
Module,
|
Module,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
266
internal/modules/rbac/application/service.go
Normal file
266
internal/modules/rbac/application/service.go
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"mengstack/internal/kernel/errors"
|
||||||
|
"mengstack/internal/kernel/tenant"
|
||||||
|
"mengstack/internal/modules/rbac/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
permRepo domain.PermissionRepository
|
||||||
|
roleRepo domain.RoleRepository
|
||||||
|
userRoleRepo domain.UserRoleRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(
|
||||||
|
permRepo domain.PermissionRepository,
|
||||||
|
roleRepo domain.RoleRepository,
|
||||||
|
userRoleRepo domain.UserRoleRepository,
|
||||||
|
) *Service {
|
||||||
|
return &Service{
|
||||||
|
permRepo: permRepo,
|
||||||
|
roleRepo: roleRepo,
|
||||||
|
userRoleRepo: userRoleRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListPermissions(ctx context.Context) ([]domain.PermissionDTO, error) {
|
||||||
|
perms, err := s.permRepo.FindAll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "QUERY_PERMS_FAILED", "failed to query permissions", 500)
|
||||||
|
}
|
||||||
|
dtos := make([]domain.PermissionDTO, len(perms))
|
||||||
|
for i, p := range perms {
|
||||||
|
dtos[i] = domain.ToPermissionDTO(&p)
|
||||||
|
}
|
||||||
|
return dtos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListPermissionsByModule(ctx context.Context, module string) ([]domain.PermissionDTO, error) {
|
||||||
|
perms, err := s.permRepo.FindByModule(ctx, module)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "QUERY_PERMS_FAILED", "failed to query permissions", 500)
|
||||||
|
}
|
||||||
|
dtos := make([]domain.PermissionDTO, len(perms))
|
||||||
|
for i, p := range perms {
|
||||||
|
dtos[i] = domain.ToPermissionDTO(&p)
|
||||||
|
}
|
||||||
|
return dtos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateRole(ctx context.Context, req domain.CreateRoleRequest) (domain.RoleDTO, error) {
|
||||||
|
tenantID, ok := tenant.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return domain.RoleDTO{}, errors.ErrTenantRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
role := &domain.Role{
|
||||||
|
TenantID: tenantID,
|
||||||
|
Name: req.Name,
|
||||||
|
Description: req.Description,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.roleRepo.Create(ctx, role); err != nil {
|
||||||
|
return domain.RoleDTO{}, errors.Wrap(err, "CREATE_ROLE_FAILED", "failed to create role", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain.ToRoleDTO(role), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListRoles(ctx context.Context) ([]domain.RoleDTO, error) {
|
||||||
|
tenantID, ok := tenant.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.ErrTenantRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
roles, err := s.roleRepo.FindByTenant(ctx, tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "QUERY_ROLES_FAILED", "failed to query roles", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
dtos := make([]domain.RoleDTO, len(roles))
|
||||||
|
for i, r := range roles {
|
||||||
|
dtos[i] = domain.ToRoleDTO(&r)
|
||||||
|
}
|
||||||
|
return dtos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetRole(ctx context.Context, roleID uint) (domain.RoleDTO, error) {
|
||||||
|
tenantID, ok := tenant.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return domain.RoleDTO{}, errors.ErrTenantRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
role, err := s.roleRepo.FindByIDWithPermissions(ctx, tenantID, roleID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.RoleDTO{}, errors.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain.ToRoleDTO(role), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdateRole(ctx context.Context, roleID uint, req domain.UpdateRoleRequest) (domain.RoleDTO, error) {
|
||||||
|
tenantID, ok := tenant.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return domain.RoleDTO{}, errors.ErrTenantRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
role, err := s.roleRepo.FindByID(ctx, tenantID, roleID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.RoleDTO{}, errors.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if role.IsSystem {
|
||||||
|
return domain.RoleDTO{}, errors.New("SYSTEM_ROLE", "cannot modify system role", 403)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name != "" {
|
||||||
|
role.Name = req.Name
|
||||||
|
}
|
||||||
|
if req.Description != "" {
|
||||||
|
role.Description = req.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.roleRepo.Update(ctx, role); err != nil {
|
||||||
|
return domain.RoleDTO{}, errors.Wrap(err, "UPDATE_ROLE_FAILED", "failed to update role", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain.ToRoleDTO(role), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) DeleteRole(ctx context.Context, roleID uint) error {
|
||||||
|
tenantID, ok := tenant.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return errors.ErrTenantRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
role, err := s.roleRepo.FindByID(ctx, tenantID, roleID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if role.IsSystem {
|
||||||
|
return errors.New("SYSTEM_ROLE", "cannot delete system role", 403)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.roleRepo.Delete(ctx, tenantID, roleID); err != nil {
|
||||||
|
return errors.Wrap(err, "DELETE_ROLE_FAILED", "failed to delete role", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SetRolePermissions(ctx context.Context, roleID uint, req domain.SetRolePermissionsRequest) error {
|
||||||
|
tenantID, ok := tenant.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return errors.ErrTenantRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
role, err := s.roleRepo.FindByID(ctx, tenantID, roleID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if role.IsSystem {
|
||||||
|
return errors.New("SYSTEM_ROLE", "cannot modify system role permissions", 403)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pid := range req.PermissionIDs {
|
||||||
|
if _, err := s.permRepo.FindByID(ctx, pid); err != nil {
|
||||||
|
return errors.New("PERM_NOT_FOUND", "permission not found", 404)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.roleRepo.SetPermissions(ctx, roleID, req.PermissionIDs); err != nil {
|
||||||
|
return errors.Wrap(err, "SET_PERMS_FAILED", "failed to set permissions", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) AssignRoleToUser(ctx context.Context, userID uint, req domain.AssignRoleRequest) error {
|
||||||
|
tenantID, ok := tenant.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return errors.ErrTenantRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.roleRepo.FindByID(ctx, tenantID, req.RoleID); err != nil {
|
||||||
|
return errors.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
ur := &domain.UserRole{
|
||||||
|
UserID: userID,
|
||||||
|
TenantID: tenantID,
|
||||||
|
RoleID: req.RoleID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.userRoleRepo.Assign(ctx, ur); err != nil {
|
||||||
|
return errors.Wrap(err, "ASSIGN_ROLE_FAILED", "failed to assign role", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) RemoveRoleFromUser(ctx context.Context, userID uint, roleID uint) error {
|
||||||
|
tenantID, ok := tenant.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return errors.ErrTenantRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.userRoleRepo.Remove(ctx, userID, tenantID, roleID); err != nil {
|
||||||
|
return errors.Wrap(err, "REMOVE_ROLE_FAILED", "failed to remove role", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetUserPermissions(ctx context.Context, userID uint) ([]domain.PermissionDTO, error) {
|
||||||
|
tenantID, ok := tenant.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.ErrTenantRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
roles, err := s.userRoleRepo.FindRolesByUser(ctx, userID, tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "QUERY_USER_ROLES_FAILED", "failed to query user roles", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
permMap := make(map[uint]domain.PermissionDTO)
|
||||||
|
for _, role := range roles {
|
||||||
|
for _, perm := range role.Permissions {
|
||||||
|
if _, exists := permMap[perm.ID]; !exists {
|
||||||
|
permMap[perm.ID] = domain.ToPermissionDTO(&perm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dtos := make([]domain.PermissionDTO, 0, len(permMap))
|
||||||
|
for _, dto := range permMap {
|
||||||
|
dtos = append(dtos, dto)
|
||||||
|
}
|
||||||
|
return dtos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UserHasPermission(ctx context.Context, userID uint, permCode string) (bool, error) {
|
||||||
|
tenantID, ok := tenant.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return false, errors.ErrTenantRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
roles, err := s.userRoleRepo.FindRolesByUser(ctx, userID, tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "QUERY_USER_ROLES_FAILED", "failed to query user roles", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, role := range roles {
|
||||||
|
for _, perm := range role.Permissions {
|
||||||
|
if perm.Code == permCode {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
35
internal/modules/rbac/domain/permission.go
Normal file
35
internal/modules/rbac/domain/permission.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Permission struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
Code string `json:"code" gorm:"uniqueIndex;size:128;not null"`
|
||||||
|
Name string `json:"name" gorm:"size:128;not null"`
|
||||||
|
Module string `json:"module" gorm:"size:64;not null;index"`
|
||||||
|
Description string `json:"description" gorm:"size:256"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Permission) TableName() string {
|
||||||
|
return "permissions"
|
||||||
|
}
|
||||||
|
|
||||||
|
type PermissionDTO struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Module string `json:"module"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPermissionDTO(p *Permission) PermissionDTO {
|
||||||
|
return PermissionDTO{
|
||||||
|
ID: p.ID,
|
||||||
|
Code: p.Code,
|
||||||
|
Name: p.Name,
|
||||||
|
Module: p.Module,
|
||||||
|
Description: p.Description,
|
||||||
|
}
|
||||||
|
}
|
||||||
28
internal/modules/rbac/domain/repository.go
Normal file
28
internal/modules/rbac/domain/repository.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type PermissionRepository interface {
|
||||||
|
Create(ctx context.Context, perm *Permission) error
|
||||||
|
FindByID(ctx context.Context, id uint) (*Permission, error)
|
||||||
|
FindByCode(ctx context.Context, code string) (*Permission, error)
|
||||||
|
FindAll(ctx context.Context) ([]Permission, error)
|
||||||
|
FindByModule(ctx context.Context, module string) ([]Permission, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoleRepository interface {
|
||||||
|
Create(ctx context.Context, role *Role) error
|
||||||
|
FindByID(ctx context.Context, tenantID string, id uint) (*Role, error)
|
||||||
|
FindByIDWithPermissions(ctx context.Context, tenantID string, id uint) (*Role, error)
|
||||||
|
FindByTenant(ctx context.Context, tenantID string) ([]Role, error)
|
||||||
|
Update(ctx context.Context, role *Role) error
|
||||||
|
Delete(ctx context.Context, tenantID string, id uint) error
|
||||||
|
SetPermissions(ctx context.Context, roleID uint, permIDs []uint) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserRoleRepository interface {
|
||||||
|
Assign(ctx context.Context, ur *UserRole) error
|
||||||
|
Remove(ctx context.Context, userID uint, tenantID string, roleID uint) error
|
||||||
|
FindByUser(ctx context.Context, userID uint, tenantID string) ([]UserRole, error)
|
||||||
|
FindRolesByUser(ctx context.Context, userID uint, tenantID string) ([]Role, error)
|
||||||
|
}
|
||||||
82
internal/modules/rbac/domain/role.go
Normal file
82
internal/modules/rbac/domain/role.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Role struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
TenantID string `json:"tenant_id" gorm:"type:uuid;not null;primaryKey;index:idx_tenant_role_name,unique"`
|
||||||
|
Name string `json:"name" gorm:"size:64;not null;index:idx_tenant_role_name,unique"`
|
||||||
|
Description string `json:"description" gorm:"size:256"`
|
||||||
|
IsSystem bool `json:"is_system" gorm:"default:false"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
Permissions []Permission `json:"permissions,omitempty" gorm:"many2many:role_permissions;"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Role) TableName() string {
|
||||||
|
return "roles"
|
||||||
|
}
|
||||||
|
|
||||||
|
type RolePermission struct {
|
||||||
|
RoleID uint `gorm:"primaryKey"`
|
||||||
|
PermissionID uint `gorm:"primaryKey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RolePermission) TableName() string {
|
||||||
|
return "role_permissions"
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserRole struct {
|
||||||
|
UserID uint `json:"user_id" gorm:"primaryKey"`
|
||||||
|
TenantID string `json:"tenant_id" gorm:"type:uuid;not null;primaryKey"`
|
||||||
|
RoleID uint `json:"role_id" gorm:"not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UserRole) TableName() string {
|
||||||
|
return "user_roles"
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoleDTO struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
TenantID string `json:"tenant_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
IsSystem bool `json:"is_system"`
|
||||||
|
Permissions []PermissionDTO `json:"permissions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToRoleDTO(r *Role) RoleDTO {
|
||||||
|
dto := RoleDTO{
|
||||||
|
ID: r.ID,
|
||||||
|
TenantID: r.TenantID,
|
||||||
|
Name: r.Name,
|
||||||
|
Description: r.Description,
|
||||||
|
IsSystem: r.IsSystem,
|
||||||
|
}
|
||||||
|
if r.Permissions != nil {
|
||||||
|
dto.Permissions = make([]PermissionDTO, len(r.Permissions))
|
||||||
|
for i, p := range r.Permissions {
|
||||||
|
dto.Permissions[i] = ToPermissionDTO(&p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dto
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateRoleRequest struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateRoleRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetRolePermissionsRequest struct {
|
||||||
|
PermissionIDs []uint `json:"permission_ids" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AssignRoleRequest struct {
|
||||||
|
RoleID uint `json:"role_id" binding:"required"`
|
||||||
|
}
|
||||||
16
internal/modules/rbac/infrastructure/migrate.go
Normal file
16
internal/modules/rbac/infrastructure/migrate.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package infrastructure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mengstack/internal/modules/rbac/domain"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Migrate(db *gorm.DB) error {
|
||||||
|
return db.AutoMigrate(
|
||||||
|
&domain.Permission{},
|
||||||
|
&domain.Role{},
|
||||||
|
&domain.RolePermission{},
|
||||||
|
&domain.UserRole{},
|
||||||
|
)
|
||||||
|
}
|
||||||
45
internal/modules/rbac/infrastructure/permission_repo.go
Normal file
45
internal/modules/rbac/infrastructure/permission_repo.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package infrastructure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"mengstack/internal/modules/rbac/domain"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type permRepo struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPermissionRepository(db *gorm.DB) domain.PermissionRepository {
|
||||||
|
return &permRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *permRepo) Create(ctx context.Context, perm *domain.Permission) error {
|
||||||
|
return r.db.WithContext(ctx).Create(perm).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *permRepo) FindByID(ctx context.Context, id uint) (*domain.Permission, error) {
|
||||||
|
var perm domain.Permission
|
||||||
|
err := r.db.WithContext(ctx).First(&perm, id).Error
|
||||||
|
return &perm, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *permRepo) FindByCode(ctx context.Context, code string) (*domain.Permission, error) {
|
||||||
|
var perm domain.Permission
|
||||||
|
err := r.db.WithContext(ctx).Where("code = ?", code).First(&perm).Error
|
||||||
|
return &perm, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *permRepo) FindAll(ctx context.Context) ([]domain.Permission, error) {
|
||||||
|
var perms []domain.Permission
|
||||||
|
err := r.db.WithContext(ctx).Order("module, code").Find(&perms).Error
|
||||||
|
return perms, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *permRepo) FindByModule(ctx context.Context, module string) ([]domain.Permission, error) {
|
||||||
|
var perms []domain.Permission
|
||||||
|
err := r.db.WithContext(ctx).Where("module = ?", module).Order("code").Find(&perms).Error
|
||||||
|
return perms, err
|
||||||
|
}
|
||||||
65
internal/modules/rbac/infrastructure/role_repo.go
Normal file
65
internal/modules/rbac/infrastructure/role_repo.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package infrastructure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"mengstack/internal/modules/rbac/domain"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type roleRepo struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRoleRepository(db *gorm.DB) domain.RoleRepository {
|
||||||
|
return &roleRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roleRepo) Create(ctx context.Context, role *domain.Role) error {
|
||||||
|
return r.db.WithContext(ctx).Create(role).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roleRepo) FindByID(ctx context.Context, tenantID string, id uint) (*domain.Role, error) {
|
||||||
|
var role domain.Role
|
||||||
|
err := r.db.WithContext(ctx).Where("id = ? AND tenant_id = ?", id, tenantID).First(&role).Error
|
||||||
|
return &role, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roleRepo) FindByIDWithPermissions(ctx context.Context, tenantID string, id uint) (*domain.Role, error) {
|
||||||
|
var role domain.Role
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("Permissions").
|
||||||
|
Where("roles.id = ? AND roles.tenant_id = ?", id, tenantID).
|
||||||
|
First(&role).Error
|
||||||
|
return &role, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roleRepo) FindByTenant(ctx context.Context, tenantID string) ([]domain.Role, error) {
|
||||||
|
var roles []domain.Role
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("Permissions").
|
||||||
|
Where("tenant_id = ?", tenantID).
|
||||||
|
Order("name").
|
||||||
|
Find(&roles).Error
|
||||||
|
return roles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roleRepo) Update(ctx context.Context, role *domain.Role) error {
|
||||||
|
return r.db.WithContext(ctx).Save(role).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roleRepo) Delete(ctx context.Context, tenantID string, id uint) error {
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Where("id = ? AND tenant_id = ?", id, tenantID).
|
||||||
|
Delete(&domain.Role{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roleRepo) SetPermissions(ctx context.Context, roleID uint, permIDs []uint) error {
|
||||||
|
role := &domain.Role{ID: roleID}
|
||||||
|
perms := make([]domain.Permission, len(permIDs))
|
||||||
|
for i, id := range permIDs {
|
||||||
|
perms[i] = domain.Permission{ID: id}
|
||||||
|
}
|
||||||
|
return r.db.WithContext(ctx).Model(role).Association("Permissions").Replace(perms)
|
||||||
|
}
|
||||||
179
internal/modules/rbac/infrastructure/seed.go
Normal file
179
internal/modules/rbac/infrastructure/seed.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
package infrastructure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"mengstack/internal/modules/rbac/domain"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type permDef struct {
|
||||||
|
Code string
|
||||||
|
Name string
|
||||||
|
Module string
|
||||||
|
Desc string
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultPermissions = []permDef{
|
||||||
|
{"auth.user.read", "查看用户", "auth", "查看用户列表和详情"},
|
||||||
|
{"auth.user.write", "管理用户", "auth", "创建、编辑、删除用户"},
|
||||||
|
{"rbac.role.read", "查看角色", "rbac", "查看角色列表和详情"},
|
||||||
|
{"rbac.role.write", "管理角色", "rbac", "创建、编辑、删除角色"},
|
||||||
|
{"rbac.permission.read", "查看权限", "rbac", "查看权限列表"},
|
||||||
|
{"rbac.permission.assign", "分配权限", "rbac", "为角色分配权限"},
|
||||||
|
{"rbac.user_role.write", "分配角色", "rbac", "为用户分配角色"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func Seed(db *gorm.DB, log *zap.Logger) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for _, p := range defaultPermissions {
|
||||||
|
var count int64
|
||||||
|
db.Model(&domain.Permission{}).Where("code = ?", p.Code).Count(&count)
|
||||||
|
if count > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
perm := &domain.Permission{
|
||||||
|
Code: p.Code,
|
||||||
|
Name: p.Name,
|
||||||
|
Module: p.Module,
|
||||||
|
Description: p.Desc,
|
||||||
|
}
|
||||||
|
if err := db.WithContext(ctx).Create(perm).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Info("seeded permission", zap.String("code", p.Code))
|
||||||
|
}
|
||||||
|
|
||||||
|
var allPerms []domain.Permission
|
||||||
|
if err := db.WithContext(ctx).Find(&allPerms).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(allPerms) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var tenantIDs []string
|
||||||
|
if err := db.WithContext(ctx).Table("users").Distinct("tenant_id").
|
||||||
|
Pluck("tenant_id", &tenantIDs).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tid := range tenantIDs {
|
||||||
|
if err := seedTenantRoles(ctx, db, log, tid, allPerms); err != nil {
|
||||||
|
log.Error("seed tenant roles failed",
|
||||||
|
zap.String("tenant_id", tid),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func seedTenantRoles(ctx context.Context, db *gorm.DB, log *zap.Logger, tenantID string, allPerms []domain.Permission) error {
|
||||||
|
superAdmin := &domain.Role{}
|
||||||
|
err := db.WithContext(ctx).
|
||||||
|
Where("tenant_id = ? AND name = ?", tenantID, "super_admin").
|
||||||
|
First(superAdmin).Error
|
||||||
|
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
superAdmin = &domain.Role{
|
||||||
|
TenantID: tenantID,
|
||||||
|
Name: "super_admin",
|
||||||
|
Description: "超级管理员",
|
||||||
|
IsSystem: true,
|
||||||
|
}
|
||||||
|
if err := db.WithContext(ctx).Create(superAdmin).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
permRefs := make([]domain.Permission, len(allPerms))
|
||||||
|
copy(permRefs, allPerms)
|
||||||
|
if err := db.WithContext(ctx).Model(superAdmin).Association("Permissions").Replace(permRefs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Info("seeded super_admin role", zap.String("tenant_id", tenantID))
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantAdmin := &domain.Role{}
|
||||||
|
err = db.WithContext(ctx).
|
||||||
|
Where("tenant_id = ? AND name = ?", tenantID, "tenant_admin").
|
||||||
|
First(tenantAdmin).Error
|
||||||
|
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
tenantAdmin = &domain.Role{
|
||||||
|
TenantID: tenantID,
|
||||||
|
Name: "tenant_admin",
|
||||||
|
Description: "租户管理员",
|
||||||
|
IsSystem: true,
|
||||||
|
}
|
||||||
|
if err := db.WithContext(ctx).Create(tenantAdmin).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
adminPermCodes := []string{
|
||||||
|
"auth.user.read", "auth.user.write",
|
||||||
|
"rbac.role.read", "rbac.permission.read", "rbac.user_role.write",
|
||||||
|
}
|
||||||
|
var adminPerms []domain.Permission
|
||||||
|
for _, p := range allPerms {
|
||||||
|
for _, code := range adminPermCodes {
|
||||||
|
if p.Code == code {
|
||||||
|
adminPerms = append(adminPerms, p)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(adminPerms) > 0 {
|
||||||
|
if err := db.WithContext(ctx).Model(tenantAdmin).Association("Permissions").Replace(adminPerms); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Info("seeded tenant_admin role", zap.String("tenant_id", tenantID))
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
member := &domain.Role{}
|
||||||
|
err = db.WithContext(ctx).
|
||||||
|
Where("tenant_id = ? AND name = ?", tenantID, "member").
|
||||||
|
First(member).Error
|
||||||
|
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
member = &domain.Role{
|
||||||
|
TenantID: tenantID,
|
||||||
|
Name: "member",
|
||||||
|
Description: "普通成员",
|
||||||
|
IsSystem: true,
|
||||||
|
}
|
||||||
|
if err := db.WithContext(ctx).Create(member).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
memberPermCodes := []string{"auth.user.read", "rbac.permission.read"}
|
||||||
|
var memberPerms []domain.Permission
|
||||||
|
for _, p := range allPerms {
|
||||||
|
for _, code := range memberPermCodes {
|
||||||
|
if strings.EqualFold(p.Code, code) {
|
||||||
|
memberPerms = append(memberPerms, p)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(memberPerms) > 0 {
|
||||||
|
if err := db.WithContext(ctx).Model(member).Association("Permissions").Replace(memberPerms); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Info("seeded member role", zap.String("tenant_id", tenantID))
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
45
internal/modules/rbac/infrastructure/user_role_repo.go
Normal file
45
internal/modules/rbac/infrastructure/user_role_repo.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package infrastructure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"mengstack/internal/modules/rbac/domain"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userRoleRepo struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserRoleRepository(db *gorm.DB) domain.UserRoleRepository {
|
||||||
|
return &userRoleRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRoleRepo) Assign(ctx context.Context, ur *domain.UserRole) error {
|
||||||
|
return r.db.WithContext(ctx).Create(ur).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRoleRepo) Remove(ctx context.Context, userID uint, tenantID string, roleID uint) error {
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Where("user_id = ? AND tenant_id = ? AND role_id = ?", userID, tenantID, roleID).
|
||||||
|
Delete(&domain.UserRole{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRoleRepo) FindByUser(ctx context.Context, userID uint, tenantID string) ([]domain.UserRole, error) {
|
||||||
|
var urs []domain.UserRole
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("user_id = ? AND tenant_id = ?", userID, tenantID).
|
||||||
|
Find(&urs).Error
|
||||||
|
return urs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRoleRepo) FindRolesByUser(ctx context.Context, userID uint, tenantID string) ([]domain.Role, error) {
|
||||||
|
var roles []domain.Role
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Joins("JOIN user_roles ON user_roles.role_id = roles.id").
|
||||||
|
Where("user_roles.user_id = ? AND user_roles.tenant_id = ?", userID, tenantID).
|
||||||
|
Preload("Permissions").
|
||||||
|
Find(&roles).Error
|
||||||
|
return roles, err
|
||||||
|
}
|
||||||
215
internal/modules/rbac/interfaces/handler.go
Normal file
215
internal/modules/rbac/interfaces/handler.go
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"mengstack/internal/kernel/response"
|
||||||
|
"mengstack/internal/modules/rbac/application"
|
||||||
|
"mengstack/internal/modules/rbac/domain"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
svc *application.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(svc *application.Service) *Handler {
|
||||||
|
return &Handler{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListPermissions(c *gin.Context) {
|
||||||
|
module := c.Query("module")
|
||||||
|
|
||||||
|
var (
|
||||||
|
dtos []domain.PermissionDTO
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if module != "" {
|
||||||
|
dtos, err = h.svc.ListPermissionsByModule(c.Request.Context(), module)
|
||||||
|
} else {
|
||||||
|
dtos, err = h.svc.ListPermissions(c.Request.Context())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, dtos)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CreateRole(c *gin.Context) {
|
||||||
|
var req domain.CreateRoleRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid request body"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dto, err := h.svc.CreateRole(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListRoles(c *gin.Context) {
|
||||||
|
dtos, err := h.svc.ListRoles(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, dtos)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetRole(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid role id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dto, err := h.svc.GetRole(c.Request.Context(), uint(id))
|
||||||
|
if err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) UpdateRole(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid role id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req domain.UpdateRoleRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid request body"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dto, err := h.svc.UpdateRole(c.Request.Context(), uint(id), req)
|
||||||
|
if err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) DeleteRole(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid role id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.DeleteRole(c.Request.Context(), uint(id)); err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) SetRolePermissions(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid role id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req domain.SetRolePermissionsRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid request body"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.SetRolePermissions(c.Request.Context(), uint(id), req); err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetRolePermissions(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid role id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dto, err := h.svc.GetRole(c.Request.Context(), uint(id))
|
||||||
|
if err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, dto.Permissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) AssignRole(c *gin.Context) {
|
||||||
|
userID, err := strconv.ParseUint(c.Param("userId"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid user id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req domain.AssignRoleRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid request body"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.AssignRoleToUser(c.Request.Context(), uint(userID), req); err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) RemoveRole(c *gin.Context) {
|
||||||
|
userID, err := strconv.ParseUint(c.Param("userId"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid user id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
roleID, err := strconv.ParseUint(c.Param("roleId"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid role id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.RemoveRoleFromUser(c.Request.Context(), uint(userID), uint(roleID)); err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetUserRoles(c *gin.Context) {
|
||||||
|
userID, err := strconv.ParseUint(c.Param("userId"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, response.NewBadRequest("invalid user id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dtos, err := h.svc.GetUserPermissions(c.Request.Context(), uint(userID))
|
||||||
|
if err != nil {
|
||||||
|
response.HandleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, dtos)
|
||||||
|
}
|
||||||
56
internal/modules/rbac/interfaces/routes.go
Normal file
56
internal/modules/rbac/interfaces/routes.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mengstack/internal/app/middleware"
|
||||||
|
"mengstack/internal/modules/rbac/application"
|
||||||
|
"mengstack/internal/modules/rbac/infrastructure"
|
||||||
|
rbacmiddleware "mengstack/internal/modules/rbac/middleware"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupRoutes(
|
||||||
|
r *gin.Engine,
|
||||||
|
h *Handler,
|
||||||
|
authMW gin.HandlerFunc,
|
||||||
|
) {
|
||||||
|
rbac := r.Group("/api/v1/rbac")
|
||||||
|
rbac.Use(authMW, middleware.MultiTenant())
|
||||||
|
{
|
||||||
|
rbac.GET("/permissions", h.ListPermissions)
|
||||||
|
|
||||||
|
rbac.POST("/roles", h.CreateRole)
|
||||||
|
rbac.GET("/roles", h.ListRoles)
|
||||||
|
rbac.GET("/roles/:id", h.GetRole)
|
||||||
|
rbac.PUT("/roles/:id", h.UpdateRole)
|
||||||
|
rbac.DELETE("/roles/:id", h.DeleteRole)
|
||||||
|
rbac.PUT("/roles/:id/permissions", h.SetRolePermissions)
|
||||||
|
rbac.GET("/roles/:id/permissions", h.GetRolePermissions)
|
||||||
|
|
||||||
|
rbac.POST("/users/:userId/roles", h.AssignRole)
|
||||||
|
rbac.DELETE("/users/:userId/roles/:roleId", h.RemoveRole)
|
||||||
|
rbac.GET("/users/:userId/roles", h.GetUserRoles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Module = fx.Module("rbac",
|
||||||
|
fx.Provide(
|
||||||
|
infrastructure.NewPermissionRepository,
|
||||||
|
infrastructure.NewRoleRepository,
|
||||||
|
infrastructure.NewUserRoleRepository,
|
||||||
|
application.NewService,
|
||||||
|
NewHandler,
|
||||||
|
rbacmiddleware.NewPermissionMiddleware,
|
||||||
|
),
|
||||||
|
fx.Invoke(func(db *gorm.DB, log *zap.Logger) {
|
||||||
|
if err := infrastructure.Migrate(db); err != nil {
|
||||||
|
log.Fatal("rbac migration failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
if err := infrastructure.Seed(db, log); err != nil {
|
||||||
|
log.Error("rbac seed failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
41
internal/modules/rbac/middleware/permission.go
Normal file
41
internal/modules/rbac/middleware/permission.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"mengstack/internal/modules/rbac/application"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PermissionMiddleware struct {
|
||||||
|
svc *application.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequirePermissionFunc func(permCode string) gin.HandlerFunc
|
||||||
|
|
||||||
|
func NewPermissionMiddleware(svc *application.Service) *PermissionMiddleware {
|
||||||
|
return &PermissionMiddleware{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *PermissionMiddleware) RequirePermission(permCode string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
userID, exists := c.Get("user_id")
|
||||||
|
if !exists {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": -1, "message": "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
has, err := m.svc.UserHasPermission(c.Request.Context(), userID.(uint), permCode)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"code": -1, "message": "permission check failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !has {
|
||||||
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": -1, "message": "permission denied"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user