feat: 分层 RBAC 权限管理系统
后端: - 新增 Role / RolePermission 实体(自动 seed 系统角色) - PermissionService——通过 isAdmin / TenantMember 链路解析用户权限 - @Permission() 装饰器 + PermissionsGuard 守卫 - /api/permissions 和 /api/roles REST API - UserController 内联 role 检查迁移到 @Permission() - PermissionModule 全局注册 前端: - usePermissions hook——获取当前用户权限集 - PermissionGate 组件级门控 - PermissionSettingsView——角色列表+权限矩阵编辑页面 - SettingsView 新增「权限管理」Tab(仅 admin 可见) - 权限预览(26 项,7 分类) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ import { TenantModule } from './tenant/tenant.module';
|
||||
import { SuperAdminModule } from './super-admin/super-admin.module';
|
||||
import { AdminModule } from './admin/admin.module';
|
||||
import { FeishuModule } from './feishu/feishu.module';
|
||||
import { PermissionModule } from './auth/permission/permission.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -82,6 +83,7 @@ import { FeishuModule } from './feishu/feishu.module';
|
||||
SuperAdminModule,
|
||||
AdminModule,
|
||||
FeishuModule,
|
||||
PermissionModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 所有可用权限的常量定义
|
||||
* 格式: resource:action
|
||||
*
|
||||
* 分类说明:
|
||||
* - user: 用户管理
|
||||
* - tenant: 租户管理
|
||||
* - kb: 知识库
|
||||
* - assess: 考核评估
|
||||
* - model: 模型配置
|
||||
* - plugin: 插件管理
|
||||
* - settings: 系统设置
|
||||
*/
|
||||
|
||||
export const PERMISSIONS = {
|
||||
// ── 用户管理 ──
|
||||
USER_VIEW: 'user:view',
|
||||
USER_CREATE: 'user:create',
|
||||
USER_EDIT: 'user:edit',
|
||||
USER_DELETE: 'user:delete',
|
||||
USER_ROLE: 'user:role', // 修改他人角色/权限
|
||||
USER_PASSWORD: 'user:password', // 重置他人密码
|
||||
|
||||
// ── 租户管理 ──
|
||||
TENANT_VIEW: 'tenant:view',
|
||||
TENANT_CREATE: 'tenant:create',
|
||||
TENANT_EDIT: 'tenant:edit',
|
||||
TENANT_DELETE: 'tenant:delete',
|
||||
TENANT_MEMBERS: 'tenant:members',
|
||||
|
||||
// ── 知识库 ──
|
||||
KB_VIEW: 'kb:view',
|
||||
KB_CREATE: 'kb:create',
|
||||
KB_EDIT: 'kb:edit',
|
||||
KB_DELETE: 'kb:delete',
|
||||
KB_PUBLISH: 'kb:publish',
|
||||
|
||||
// ── 考核评估 ──
|
||||
ASSESS_VIEW: 'assess:view',
|
||||
ASSESS_MANAGE: 'assess:manage',
|
||||
ASSESS_TEMPLATE: 'assess:template',
|
||||
ASSESS_BANK: 'assess:bank',
|
||||
|
||||
// ── 模型配置 ──
|
||||
MODEL_VIEW: 'model:view',
|
||||
MODEL_CONFIG: 'model:config',
|
||||
|
||||
// ── 插件管理 ──
|
||||
PLUGIN_VIEW: 'plugin:view',
|
||||
PLUGIN_MANAGE: 'plugin:manage',
|
||||
|
||||
// ── 系统设置 ──
|
||||
SETTINGS_VIEW: 'settings:view',
|
||||
SETTINGS_SYSTEM: 'settings:system',
|
||||
} as const;
|
||||
|
||||
export type PermissionKey = (typeof PERMISSIONS)[keyof typeof PERMISSIONS];
|
||||
export const ALL_PERMISSIONS = Object.values(PERMISSIONS) as PermissionKey[];
|
||||
|
||||
/** 权限分类元数据——给前端渲染用 */
|
||||
export interface PermissionMeta {
|
||||
key: PermissionKey;
|
||||
category: string;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const PERMISSION_META: PermissionMeta[] = [
|
||||
// ── 用户管理 ──
|
||||
{ key: PERMISSIONS.USER_VIEW, category: '用户管理', label: '查看用户', description: '查看用户列表和基本信息' },
|
||||
{ key: PERMISSIONS.USER_CREATE, category: '用户管理', label: '创建用户', description: '添加新用户到系统' },
|
||||
{ key: PERMISSIONS.USER_EDIT, category: '用户管理', label: '编辑用户', description: '修改用户基本信息' },
|
||||
{ key: PERMISSIONS.USER_DELETE, category: '用户管理', label: '删除用户', description: '从系统删除用户' },
|
||||
{ key: PERMISSIONS.USER_ROLE, category: '用户管理', label: '管理角色', description: '修改用户角色和权限' },
|
||||
{ key: PERMISSIONS.USER_PASSWORD, category: '用户管理', label: '重置密码', description: '重置其他用户的密码' },
|
||||
|
||||
// ── 租户管理 ──
|
||||
{ key: PERMISSIONS.TENANT_VIEW, category: '租户管理', label: '查看租户', description: '查看租户信息和成员' },
|
||||
{ key: PERMISSIONS.TENANT_CREATE, category: '租户管理', label: '创建租户', description: '创建新的租户' },
|
||||
{ key: PERMISSIONS.TENANT_EDIT, category: '租户管理', label: '编辑租户', description: '修改租户设置' },
|
||||
{ key: PERMISSIONS.TENANT_DELETE, category: '租户管理', label: '删除租户', description: '删除租户' },
|
||||
{ key: PERMISSIONS.TENANT_MEMBERS, category: '租户管理', label: '管理成员', description: '添加/移除租户成员' },
|
||||
|
||||
// ── 知识库 ──
|
||||
{ key: PERMISSIONS.KB_VIEW, category: '知识库', label: '查看知识库', description: '查看知识库内容' },
|
||||
{ key: PERMISSIONS.KB_CREATE, category: '知识库', label: '创建知识库', description: '创建新的知识库' },
|
||||
{ key: PERMISSIONS.KB_EDIT, category: '知识库', label: '编辑知识库', description: '编辑知识库内容' },
|
||||
{ key: PERMISSIONS.KB_DELETE, category: '知识库', label: '删除知识库', description: '删除知识库' },
|
||||
{ key: PERMISSIONS.KB_PUBLISH, category: '知识库', label: '发布知识库', description: '将知识库发布上线' },
|
||||
|
||||
// ── 考核评估 ──
|
||||
{ key: PERMISSIONS.ASSESS_VIEW, category: '考核评估', label: '查看考核', description: '查看考核结果和报告' },
|
||||
{ key: PERMISSIONS.ASSESS_MANAGE, category: '考核评估', label: '管理考核', description: '管理考核会话' },
|
||||
{ key: PERMISSIONS.ASSESS_TEMPLATE, category: '考核评估', label: '管理模板', description: '创建和编辑考核模板' },
|
||||
{ key: PERMISSIONS.ASSESS_BANK, category: '考核评估', label: '管理题库', description: '管理题库内容' },
|
||||
|
||||
// ── 模型配置 ──
|
||||
{ key: PERMISSIONS.MODEL_VIEW, category: '模型配置', label: '查看模型', description: '查看模型配置' },
|
||||
{ key: PERMISSIONS.MODEL_CONFIG, category: '模型配置', label: '配置模型', description: '修改模型配置' },
|
||||
|
||||
// ── 插件管理 ──
|
||||
{ key: PERMISSIONS.PLUGIN_VIEW, category: '插件管理', label: '查看插件', description: '查看插件列表' },
|
||||
{ key: PERMISSIONS.PLUGIN_MANAGE, category: '插件管理', label: '管理插件', description: '启停和配置插件' },
|
||||
|
||||
// ── 系统设置 ──
|
||||
{ key: PERMISSIONS.SETTINGS_VIEW, category: '系统设置', label: '查看设置', description: '查看系统设置' },
|
||||
{ key: PERMISSIONS.SETTINGS_SYSTEM, category: '系统设置', label: '系统设置', description: '修改系统级设置(仅超级管理员)' },
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Controller, Get, Request, UseGuards } from '@nestjs/common';
|
||||
import { PermissionService } from './permission.service';
|
||||
import { CombinedAuthGuard } from '../combined-auth.guard';
|
||||
|
||||
@Controller('permissions')
|
||||
@UseGuards(CombinedAuthGuard)
|
||||
export class PermissionController {
|
||||
constructor(private readonly permissionService: PermissionService) {}
|
||||
|
||||
/** 获取所有可用权限(含分类) */
|
||||
@Get()
|
||||
getAll() {
|
||||
return this.permissionService.getPermissionsByCategory();
|
||||
}
|
||||
|
||||
/** 获取所有权限的扁平元数据列表 */
|
||||
@Get('meta')
|
||||
getMeta() {
|
||||
return this.permissionService.getAllPermissionMeta();
|
||||
}
|
||||
|
||||
/** 获取当前用户在活动租户下的权限集 */
|
||||
@Get('mine')
|
||||
async getMine(@Request() req) {
|
||||
const userId = req.user.id;
|
||||
const tenantId = req.tenantId || req.user.tenantId;
|
||||
const perms = await this.permissionService.getUserPermissions(userId, tenantId);
|
||||
return { permissions: [...perms] };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const PERMISSIONS_KEY = 'permissions';
|
||||
|
||||
/**
|
||||
* 权限装饰器——标记路由需要的权限
|
||||
* 多个权限之间为 OR 关系(有任一匹配即可)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Permission('user:view') // 需要 user:view 权限
|
||||
* @Permission('user:create', 'user:edit') // 需要 user:create 或 user:edit
|
||||
* ```
|
||||
*/
|
||||
export const Permission = (...permissions: string[]) =>
|
||||
SetMetadata(PERMISSIONS_KEY, permissions);
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { PermissionService } from './permission.service';
|
||||
import { PERMISSIONS_KEY } from './permission.decorator';
|
||||
|
||||
/**
|
||||
* 权限守卫——配合 @Permission() 装饰器使用
|
||||
*
|
||||
* 在 CombinedAuthGuard 和 RolesGuard 之后运行
|
||||
* 检查 request.user 是否有 @Permission() 指定的任一权限
|
||||
*/
|
||||
@Injectable()
|
||||
export class PermissionsGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly reflector: Reflector,
|
||||
private readonly permissionService: PermissionService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
|
||||
PERMISSIONS_KEY,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
|
||||
if (!requiredPermissions || requiredPermissions.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
|
||||
if (!user) return false;
|
||||
|
||||
const userId = user.id;
|
||||
const tenantId = request.tenantId || user.tenantId;
|
||||
if (!userId || !tenantId) return false;
|
||||
|
||||
const userPermissions = await this.permissionService.getUserPermissions(userId, tenantId);
|
||||
|
||||
// OR 模式:任一权限匹配即可
|
||||
const hasPermission = requiredPermissions.some(p => userPermissions.has(p));
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new ForbiddenException(`需要权限: ${requiredPermissions.join(', ')}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Role } from './role.entity';
|
||||
import { RolePermission } from './role-permission.entity';
|
||||
import { TenantMember } from '../../tenant/tenant-member.entity';
|
||||
import { User } from '../../user/user.entity';
|
||||
import { PermissionService } from './permission.service';
|
||||
import { PermissionController } from './permission.controller';
|
||||
import { RoleController } from './role.controller';
|
||||
import { PermissionsGuard } from './permission.guard';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Role, RolePermission, TenantMember, User]),
|
||||
],
|
||||
controllers: [PermissionController, RoleController],
|
||||
providers: [PermissionService, PermissionsGuard],
|
||||
exports: [PermissionService, PermissionsGuard],
|
||||
})
|
||||
export class PermissionModule {}
|
||||
@@ -0,0 +1,247 @@
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Role } from './role.entity';
|
||||
import { RolePermission } from './role-permission.entity';
|
||||
import { ALL_PERMISSIONS, PERMISSION_META, PermissionKey, PermissionMeta } from './permission.constants';
|
||||
import { UserRole } from '../../user/user-role.enum';
|
||||
import { TenantMember } from '../../tenant/tenant-member.entity';
|
||||
import { User } from '../../user/user.entity';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
/**
|
||||
* 角色预设——系统内置角色默认挂载的权限
|
||||
*/
|
||||
const ROLE_DEFAULT_PERMISSIONS: Record<string, PermissionKey[]> = {
|
||||
[UserRole.SUPER_ADMIN]: [...ALL_PERMISSIONS],
|
||||
|
||||
[UserRole.TENANT_ADMIN]: [
|
||||
// 用户管理(不含删除和角色管理——安全考虑)
|
||||
'user:view', 'user:create', 'user:edit', 'user:password',
|
||||
// 租户管理(只能查看和编辑自己的)
|
||||
'tenant:view', 'tenant:edit', 'tenant:members',
|
||||
// 知识库
|
||||
'kb:view', 'kb:create', 'kb:edit', 'kb:delete', 'kb:publish',
|
||||
// 考核评估
|
||||
'assess:view', 'assess:manage', 'assess:template', 'assess:bank',
|
||||
// 模型配置
|
||||
'model:view', 'model:config',
|
||||
// 插件
|
||||
'plugin:view', 'plugin:manage',
|
||||
// 设置
|
||||
'settings:view',
|
||||
],
|
||||
|
||||
[UserRole.USER]: [
|
||||
'kb:view', 'kb:create', 'kb:edit',
|
||||
'assess:view',
|
||||
'plugin:view',
|
||||
],
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PermissionService implements OnModuleInit {
|
||||
private readonly logger = new Logger(PermissionService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Role)
|
||||
private readonly roleRepository: Repository<Role>,
|
||||
@InjectRepository(RolePermission)
|
||||
private readonly rolePermissionRepository: Repository<RolePermission>,
|
||||
@InjectRepository(TenantMember)
|
||||
private readonly tenantMemberRepository: Repository<TenantMember>,
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 启动时自动种子化系统角色和权限
|
||||
*/
|
||||
async onModuleInit() {
|
||||
const isTest = this.configService.get<string>('NODE_ENV') === 'test';
|
||||
if (isTest) return;
|
||||
|
||||
try {
|
||||
const existing = await this.roleRepository.count({ where: { isSystem: true } });
|
||||
if (existing > 0) {
|
||||
this.logger.log(`[Permission Seed] ${existing} 个系统角色已存在,跳过`);
|
||||
return;
|
||||
}
|
||||
await this.seedSystemRoles();
|
||||
this.logger.log('[Permission Seed] ✅ 系统角色和权限已初始化');
|
||||
} catch (err) {
|
||||
this.logger.error('[Permission Seed] ❌ 初始化失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
private async seedSystemRoles() {
|
||||
const roles = [
|
||||
{ name: UserRole.SUPER_ADMIN, baseRole: UserRole.SUPER_ADMIN },
|
||||
{ name: UserRole.TENANT_ADMIN, baseRole: UserRole.TENANT_ADMIN },
|
||||
{ name: UserRole.USER, baseRole: UserRole.USER },
|
||||
];
|
||||
|
||||
for (const r of roles) {
|
||||
const role = await this.roleRepository.save({
|
||||
name: r.name,
|
||||
description: this.getRoleDescription(r.name as UserRole),
|
||||
isSystem: true,
|
||||
baseRole: r.baseRole,
|
||||
tenantId: null,
|
||||
});
|
||||
|
||||
const perms = ROLE_DEFAULT_PERMISSIONS[r.name] || [];
|
||||
if (perms.length > 0) {
|
||||
await this.rolePermissionRepository.save(
|
||||
perms.map(key => ({ roleId: role.id, permissionKey: key })),
|
||||
);
|
||||
}
|
||||
this.logger.log(` - ${r.name}: ${perms.length} 项权限`);
|
||||
}
|
||||
}
|
||||
|
||||
private getRoleDescription(role: UserRole): string {
|
||||
switch (role) {
|
||||
case UserRole.SUPER_ADMIN: return '全局超级管理员——拥有系统全部权限';
|
||||
case UserRole.TENANT_ADMIN: return '租户管理员——管理本租户内的用户和资源';
|
||||
case UserRole.USER: return '普通用户——使用系统功能';
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────── 角色 CRUD ────────────
|
||||
|
||||
async findAllRoles(tenantId?: string): Promise<Role[]> {
|
||||
const where: any[] = [{ isSystem: true, tenantId: null }];
|
||||
if (tenantId) {
|
||||
where.push({ tenantId, isSystem: false });
|
||||
}
|
||||
return this.roleRepository.find({ where, order: { isSystem: 'DESC', name: 'ASC' } });
|
||||
}
|
||||
|
||||
async findRoleById(id: string): Promise<Role | null> {
|
||||
return this.roleRepository.findOne({ where: { id } });
|
||||
}
|
||||
|
||||
async createRole(name: string, description: string, tenantId: string): Promise<Role> {
|
||||
const existing = await this.roleRepository.findOne({ where: { name } });
|
||||
if (existing) throw new Error(`角色名 "${name}" 已存在`);
|
||||
|
||||
return this.roleRepository.save({
|
||||
name,
|
||||
description,
|
||||
isSystem: false,
|
||||
baseRole: null,
|
||||
tenantId,
|
||||
});
|
||||
}
|
||||
|
||||
async updateRole(id: string, data: { name?: string; description?: string }): Promise<Role> {
|
||||
const role = await this.roleRepository.findOne({ where: { id } });
|
||||
if (!role) throw new Error('角色不存在');
|
||||
if (role.isSystem) throw new Error('系统角色不可编辑');
|
||||
|
||||
if (data.name) role.name = data.name;
|
||||
if (data.description !== undefined) role.description = data.description;
|
||||
return this.roleRepository.save(role);
|
||||
}
|
||||
|
||||
async deleteRole(id: string): Promise<void> {
|
||||
const role = await this.roleRepository.findOne({ where: { id } });
|
||||
if (!role) throw new Error('角色不存在');
|
||||
if (role.isSystem) throw new Error('系统角色不可删除');
|
||||
await this.roleRepository.remove(role);
|
||||
}
|
||||
|
||||
// ──────────── 权限管理 ────────────
|
||||
|
||||
async getRolePermissions(roleId: string): Promise<string[]> {
|
||||
const rps = await this.rolePermissionRepository.find({
|
||||
where: { roleId },
|
||||
});
|
||||
return rps.map(rp => rp.permissionKey);
|
||||
}
|
||||
|
||||
async setRolePermissions(roleId: string, permissionKeys: string[]): Promise<void> {
|
||||
const role = await this.roleRepository.findOne({ where: { id: roleId } });
|
||||
if (!role) throw new Error('角色不存在');
|
||||
|
||||
// 验证权限键是否有效
|
||||
const valid = ALL_PERMISSIONS;
|
||||
const invalid = permissionKeys.filter(k => !valid.includes(k as any));
|
||||
if (invalid.length > 0) throw new Error(`无效的权限: ${invalid.join(', ')}`);
|
||||
|
||||
// 替换角色的所有权限
|
||||
await this.rolePermissionRepository.delete({ roleId });
|
||||
if (permissionKeys.length > 0) {
|
||||
await this.rolePermissionRepository.save(
|
||||
permissionKeys.map(key => ({ roleId, permissionKey: key })),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────── 用户权限解析 ────────────
|
||||
|
||||
/**
|
||||
* 获取用户在指定租户下的最终权限集
|
||||
*
|
||||
* 解析链路:
|
||||
* 1. 如果是 global admin(isAdmin=true),直接返回所有权限
|
||||
* 2. 通过 TenantMember.role → 对应 role → role_permissions
|
||||
*/
|
||||
async getUserPermissions(userId: string, tenantId: string): Promise<Set<string>> {
|
||||
// 检查全局管理员(遗留 isAdmin 字段)
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: userId },
|
||||
select: ['id', 'isAdmin'],
|
||||
});
|
||||
if (user?.isAdmin) {
|
||||
const superAdminRole = await this.roleRepository.findOne({
|
||||
where: { baseRole: UserRole.SUPER_ADMIN, isSystem: true },
|
||||
});
|
||||
if (superAdminRole) {
|
||||
const perms = await this.getRolePermissions(superAdminRole.id);
|
||||
return new Set(perms);
|
||||
}
|
||||
}
|
||||
|
||||
// 通过租户成员角色
|
||||
const membership = await this.tenantMemberRepository.findOne({
|
||||
where: { userId, tenantId },
|
||||
});
|
||||
|
||||
if (!membership) return new Set();
|
||||
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: { baseRole: membership.role, isSystem: true },
|
||||
});
|
||||
if (!role) return new Set();
|
||||
|
||||
const perms = await this.getRolePermissions(role.id);
|
||||
return new Set(perms);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有指定权限
|
||||
*/
|
||||
async checkPermission(userId: string, tenantId: string, permissionKey: string): Promise<boolean> {
|
||||
// 先尝试从请求级缓存获取(由 PermissionsGuard 设置)
|
||||
const perms = await this.getUserPermissions(userId, tenantId);
|
||||
return perms.has(permissionKey);
|
||||
}
|
||||
|
||||
// ──────────── 元数据 ────────────
|
||||
|
||||
getAllPermissionMeta(): PermissionMeta[] {
|
||||
return PERMISSION_META;
|
||||
}
|
||||
|
||||
getPermissionsByCategory(): Record<string, PermissionMeta[]> {
|
||||
const grouped: Record<string, PermissionMeta[]> = {};
|
||||
for (const meta of PERMISSION_META) {
|
||||
if (!grouped[meta.category]) grouped[meta.category] = [];
|
||||
grouped[meta.category].push(meta);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Role } from './role.entity';
|
||||
|
||||
/**
|
||||
* 角色-权限关联表
|
||||
* 每个角色可以挂载多个权限
|
||||
*/
|
||||
@Entity('role_permissions')
|
||||
export class RolePermission {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'role_id' })
|
||||
roleId: string;
|
||||
|
||||
@ManyToOne(() => Role, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'role_id' })
|
||||
role: Role;
|
||||
|
||||
@Column({ name: 'permission_key', length: 50 })
|
||||
permissionKey: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Request,
|
||||
UseGuards,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { PermissionService } from './permission.service';
|
||||
import { CombinedAuthGuard } from '../combined-auth.guard';
|
||||
import { RolesGuard } from '../roles.guard';
|
||||
import { Roles } from '../roles.decorator';
|
||||
import { UserRole } from '../../user/user-role.enum';
|
||||
|
||||
@Controller('roles')
|
||||
@UseGuards(CombinedAuthGuard, RolesGuard)
|
||||
export class RoleController {
|
||||
constructor(private readonly permissionService: PermissionService) {}
|
||||
|
||||
/** 列出角色(系统角色 + 租户自定义角色) */
|
||||
@Get()
|
||||
@Roles(UserRole.SUPER_ADMIN, UserRole.TENANT_ADMIN)
|
||||
async findAll(@Request() req) {
|
||||
const tenantId = req.tenantId || req.user.tenantId;
|
||||
return this.permissionService.findAllRoles(tenantId);
|
||||
}
|
||||
|
||||
/** 获取单个角色 */
|
||||
@Get(':id')
|
||||
@Roles(UserRole.SUPER_ADMIN, UserRole.TENANT_ADMIN)
|
||||
async findOne(@Param('id') id: string) {
|
||||
const role = await this.permissionService.findRoleById(id);
|
||||
if (!role) throw new NotFoundException('角色不存在');
|
||||
return role;
|
||||
}
|
||||
|
||||
/** 创建自定义角色 */
|
||||
@Post()
|
||||
@Roles(UserRole.SUPER_ADMIN, UserRole.TENANT_ADMIN)
|
||||
async create(@Body() body: { name: string; description?: string }, @Request() req) {
|
||||
if (!body.name) throw new BadRequestException('角色名不能为空');
|
||||
const tenantId = req.tenantId || req.user.tenantId;
|
||||
try {
|
||||
return await this.permissionService.createRole(body.name, body.description || '', tenantId);
|
||||
} catch (err: any) {
|
||||
throw new BadRequestException(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/** 修改角色基本信息 */
|
||||
@Put(':id')
|
||||
@Roles(UserRole.SUPER_ADMIN, UserRole.TENANT_ADMIN)
|
||||
async update(@Param('id') id: string, @Body() body: { name?: string; description?: string }) {
|
||||
try {
|
||||
return await this.permissionService.updateRole(id, body);
|
||||
} catch (err: any) {
|
||||
throw new BadRequestException(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除自定义角色 */
|
||||
@Delete(':id')
|
||||
@Roles(UserRole.SUPER_ADMIN, UserRole.TENANT_ADMIN)
|
||||
async remove(@Param('id') id: string) {
|
||||
try {
|
||||
await this.permissionService.deleteRole(id);
|
||||
return { success: true };
|
||||
} catch (err: any) {
|
||||
throw new BadRequestException(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取角色的权限列表 */
|
||||
@Get(':id/permissions')
|
||||
@Roles(UserRole.SUPER_ADMIN, UserRole.TENANT_ADMIN)
|
||||
async getPermissions(@Param('id') id: string) {
|
||||
const perms = await this.permissionService.getRolePermissions(id);
|
||||
return { permissions: perms };
|
||||
}
|
||||
|
||||
/** 设置角色的权限(全量替换) */
|
||||
@Put(':id/permissions')
|
||||
@Roles(UserRole.SUPER_ADMIN, UserRole.TENANT_ADMIN)
|
||||
async setPermissions(@Param('id') id: string, @Body() body: { permissions: string[] }) {
|
||||
if (!Array.isArray(body.permissions)) {
|
||||
throw new BadRequestException('permissions 必须是数组');
|
||||
}
|
||||
try {
|
||||
await this.permissionService.setRolePermissions(id, body.permissions);
|
||||
return { success: true, permissions: body.permissions };
|
||||
} catch (err: any) {
|
||||
throw new BadRequestException(err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { UserRole } from '../../user/user-role.enum';
|
||||
|
||||
/**
|
||||
* 角色表
|
||||
* is_system = true: 系统内置角色(SUPER_ADMIN/TENANT_ADMIN/USER),不可删除
|
||||
* tenant_id = null: 系统级角色(所有租户可见)
|
||||
* tenant_id != null: 租户自定义角色
|
||||
*/
|
||||
@Entity('roles')
|
||||
export class Role {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ unique: true, length: 50 })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string;
|
||||
|
||||
/** 是否为系统内置角色 */
|
||||
@Column({ name: 'is_system', default: false })
|
||||
isSystem: boolean;
|
||||
|
||||
/** 关联的内置角色 enum(仅 is_system=true 时有值) */
|
||||
@Column({
|
||||
name: 'base_role',
|
||||
type: 'simple-enum',
|
||||
enum: UserRole,
|
||||
nullable: true,
|
||||
})
|
||||
baseRole: UserRole | null;
|
||||
|
||||
/** 所属租户:null=系统级,非 null=租户自定义 */
|
||||
@Column({ name: 'tenant_id', nullable: true, type: 'text' })
|
||||
tenantId: string | null;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -20,6 +20,8 @@ import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { I18nService } from '../i18n/i18n.service';
|
||||
import { UserRole } from './user-role.enum';
|
||||
import { UserSettingService } from './user-setting.service';
|
||||
import { Permission } from '../auth/permission/permission.decorator';
|
||||
import { PermissionsGuard } from '../auth/permission/permission.guard';
|
||||
|
||||
@Controller('users')
|
||||
@UseGuards(CombinedAuthGuard)
|
||||
@@ -92,25 +94,17 @@ export class UserController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permission('user:view')
|
||||
async findAll(
|
||||
@Request() req,
|
||||
@Query('page') page?: string,
|
||||
@Query('limit') limit?: string,
|
||||
) {
|
||||
const callerRole = req.user.role;
|
||||
if (
|
||||
callerRole !== UserRole.SUPER_ADMIN &&
|
||||
callerRole !== UserRole.TENANT_ADMIN
|
||||
) {
|
||||
throw new ForbiddenException(
|
||||
this.i18nService.getErrorMessage('adminOnlyViewList'),
|
||||
);
|
||||
}
|
||||
|
||||
const p = page ? parseInt(page) : undefined;
|
||||
const l = limit ? parseInt(limit) : undefined;
|
||||
|
||||
if (callerRole === UserRole.SUPER_ADMIN) {
|
||||
if (req.user.role === UserRole.SUPER_ADMIN) {
|
||||
return this.userService.findAll(p, l);
|
||||
} else {
|
||||
return this.userService.findByTenantId(req.user.tenantId, p, l);
|
||||
@@ -144,17 +138,9 @@ export class UserController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permission('user:create')
|
||||
async createUser(@Request() req, @Body() body: CreateUserDto) {
|
||||
const callerRole = req.user.role;
|
||||
if (
|
||||
callerRole !== UserRole.SUPER_ADMIN &&
|
||||
callerRole !== UserRole.TENANT_ADMIN
|
||||
) {
|
||||
throw new ForbiddenException(
|
||||
this.i18nService.getErrorMessage('adminOnlyCreateUser'),
|
||||
);
|
||||
}
|
||||
|
||||
const { username, password } = body;
|
||||
|
||||
if (!username || !password) {
|
||||
@@ -169,16 +155,9 @@ export class UserController {
|
||||
);
|
||||
}
|
||||
|
||||
// All new global users default to non-admin.
|
||||
// Elevation to Super Admin status is handled separately.
|
||||
// All new users default to non-admin.
|
||||
let isAdmin = false;
|
||||
|
||||
if (callerRole === UserRole.SUPER_ADMIN) {
|
||||
isAdmin = false;
|
||||
} else if (callerRole === UserRole.TENANT_ADMIN) {
|
||||
isAdmin = false;
|
||||
}
|
||||
|
||||
// Pass the calculated params to the service
|
||||
return this.userService.createUser(
|
||||
username,
|
||||
@@ -190,20 +169,14 @@ export class UserController {
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permission('user:edit')
|
||||
async updateUser(
|
||||
@Request() req,
|
||||
@Body() body: UpdateUserDto,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
const callerRole = req.user.role;
|
||||
if (
|
||||
callerRole !== UserRole.SUPER_ADMIN &&
|
||||
callerRole !== UserRole.TENANT_ADMIN
|
||||
) {
|
||||
throw new ForbiddenException(
|
||||
this.i18nService.getErrorMessage('adminOnlyUpdateUser'),
|
||||
);
|
||||
}
|
||||
|
||||
// Get user info to update
|
||||
const userToUpdate = await this.userService.findOneById(id);
|
||||
@@ -228,7 +201,6 @@ export class UserController {
|
||||
}
|
||||
|
||||
// Role modification is now obsolete on global level.
|
||||
// If Admin wants to elevate, they set isAdmin property directly.
|
||||
if (body.isAdmin !== undefined && userToUpdate.isAdmin !== body.isAdmin) {
|
||||
if (callerRole !== UserRole.SUPER_ADMIN) {
|
||||
throw new ForbiddenException(
|
||||
@@ -248,16 +220,10 @@ export class UserController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permission('user:delete')
|
||||
async deleteUser(@Request() req, @Param('id') id: string) {
|
||||
const callerRole = req.user.role;
|
||||
if (
|
||||
callerRole !== UserRole.SUPER_ADMIN &&
|
||||
callerRole !== UserRole.TENANT_ADMIN
|
||||
) {
|
||||
throw new ForbiddenException(
|
||||
this.i18nService.getErrorMessage('adminOnlyDeleteUser'),
|
||||
);
|
||||
}
|
||||
|
||||
// Prevent admin from deleting themselves
|
||||
if (req.user.id === id) {
|
||||
|
||||
@@ -8,12 +8,14 @@ import { ApiKey } from '../auth/entities/api-key.entity';
|
||||
import { UserService } from './user.service';
|
||||
import { UserController } from './user.controller';
|
||||
import { TenantModule } from '../tenant/tenant.module';
|
||||
import { PermissionModule } from '../auth/permission/permission.module';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([User, ApiKey, TenantMember, UserSetting]),
|
||||
TenantModule,
|
||||
PermissionModule,
|
||||
],
|
||||
controllers: [UserController],
|
||||
providers: [UserService, UserSettingService],
|
||||
|
||||
Reference in New Issue
Block a user