diff --git a/backend/src/rbac/rbac.module.ts b/backend/src/rbac/rbac.module.ts new file mode 100644 index 0000000..7ce50b9 --- /dev/null +++ b/backend/src/rbac/rbac.module.ts @@ -0,0 +1,21 @@ +import { Module, OnModuleInit, forwardRef } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Permission, Role, User } from '../entities'; +import { RbacService } from './rbac.service'; +import { AuthModule } from '../auth/auth.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Permission, Role, User]), + forwardRef(() => AuthModule), + ], + controllers: [], + providers: [RbacService], + exports: [RbacService], +}) +export class RbacModule implements OnModuleInit { + constructor(private rbacService: RbacService) {} + async onModuleInit() { + await this.rbacService.seedData(); + } +} diff --git a/backend/src/rbac/rbac.service.ts b/backend/src/rbac/rbac.service.ts new file mode 100644 index 0000000..2fc483b --- /dev/null +++ b/backend/src/rbac/rbac.service.ts @@ -0,0 +1,265 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import * as bcrypt from 'bcryptjs'; +import { Permission, Role, User } from '../entities'; + +const PRESET_PERMISSIONS: Array<{ code: string; name: string; group: string }> = [ + { code: 'dashboard:view', name: '查看数据面板', group: 'dashboard' }, + { code: 'student:view', name: '查看学生', group: 'student' }, + { code: 'student:create', name: '新增学生', group: 'student' }, + { code: 'student:edit', name: '编辑学生', group: 'student' }, + { code: 'student:delete', name: '删除学生', group: 'student' }, + { code: 'student:import', name: '导入学生', group: 'student' }, + { code: 'student:export', name: '导出学生', group: 'student' }, + { code: 'room:view', name: '查看宿舍', group: 'room' }, + { code: 'room:create', name: '新增宿舍', group: 'room' }, + { code: 'room:edit', name: '编辑宿舍', group: 'room' }, + { code: 'room:delete', name: '删除宿舍', group: 'room' }, + { code: 'occupancy:view', name: '查看入住', group: 'occupancy' }, + { code: 'occupancy:checkin', name: '办理入住', group: 'occupancy' }, + { code: 'occupancy:checkout', name: '办理退宿', group: 'occupancy' }, + { code: 'occupancy:transfer', name: '调换宿舍', group: 'occupancy' }, + { code: 'expense:view', name: '查看费用', group: 'expense' }, + { code: 'expense:create', name: '录入费用', group: 'expense' }, + { code: 'expense:edit', name: '编辑费用', group: 'expense' }, + { code: 'expense:delete', name: '删除费用', group: 'expense' }, + { code: 'bill:view', name: '查看账单', group: 'bill' }, + { code: 'bill:generate', name: '生成账单', group: 'bill' }, + { code: 'bill:confirm', name: '确认账单', group: 'bill' }, + { code: 'bill:delete', name: '删除账单', group: 'bill' }, + { code: 'bill:export-excel', name: '导出 Excel', group: 'bill' }, + { code: 'bill:export-pdf', name: '导出 PDF', group: 'bill' }, + { code: 'deposit:view', name: '查看押金', group: 'deposit' }, + { code: 'deposit:create', name: '新增押金', group: 'deposit' }, + { code: 'deposit:edit', name: '编辑押金', group: 'deposit' }, + { code: 'deposit:delete', name: '删除押金', group: 'deposit' }, + { code: 'classroom:view', name: '查看教室', group: 'classroom' }, + { code: 'classroom:create', name: '新增教室', group: 'classroom' }, + { code: 'classroom:edit', name: '编辑教室', group: 'classroom' }, + { code: 'classroom:delete', name: '删除教室', group: 'classroom' }, + { code: 'tenant:view', name: '查看租赁方', group: 'tenant' }, + { code: 'tenant:create', name: '新增租赁方', group: 'tenant' }, + { code: 'tenant:edit', name: '编辑租赁方', group: 'tenant' }, + { code: 'tenant:delete', name: '删除租赁方', group: 'tenant' }, + { code: 'rental:view', name: '查看租赁订单', group: 'rental' }, + { code: 'rental:create', name: '新增租赁订单', group: 'rental' }, + { code: 'rental:edit', name: '编辑租赁订单', group: 'rental' }, + { code: 'rental:delete', name: '删除租赁订单', group: 'rental' }, + { code: 'log:view', name: '查看操作日志', group: 'log' }, + { code: 'user:view', name: '查看用户', group: 'user' }, + { code: 'user:create', name: '创建用户', group: 'user' }, + { code: 'user:edit', name: '编辑用户', group: 'user' }, + { code: 'user:delete', name: '删除用户', group: 'user' }, + { code: 'user:reset-password', name: '重置密码', group: 'user' }, + { code: 'role:view', name: '查看角色', group: 'role' }, + { code: 'role:create', name: '创建角色', group: 'role' }, + { code: 'role:edit', name: '编辑角色', group: 'role' }, + { code: 'role:delete', name: '删除角色', group: 'role' }, +]; + +const PRESET_ROLES: Array<{ + name: string; + code: string; + description: string; + isSystem: boolean; + permissionGroups: string[]; + extraPermissions?: string[]; +}> = [ + { + name: '超管', + code: 'super_admin', + description: '系统超级管理员,拥有全部权限', + isSystem: true, + permissionGroups: [], + }, + { + name: '宿管老师', + code: 'dormitory_supervisor', + description: '管理宿舍相关业务', + isSystem: true, + permissionGroups: ['student', 'room', 'occupancy', 'expense', 'bill', 'deposit', 'log', 'dashboard'], + }, + { + name: '老师', + code: 'teacher', + description: '查看和管理本班学生', + isSystem: true, + permissionGroups: ['student'], + extraPermissions: ['student:view'], + }, + { + name: '机构负责人', + code: 'institution_head', + description: '管理机构教室和课程', + isSystem: true, + permissionGroups: ['classroom', 'rental', 'tenant'], + }, +]; + +@Injectable() +export class RbacService { + private readonly logger = new Logger(RbacService.name); + + constructor( + @InjectRepository(Permission) private permRepo: Repository, + @InjectRepository(Role) private roleRepo: Repository, + @InjectRepository(User) private userRepo: Repository, + ) {} + + async seedData(): Promise { + // Step 1: 幂等插入所有权限点 + for (const p of PRESET_PERMISSIONS) { + await this.permRepo + .createQueryBuilder() + .insert() + .into(Permission) + .values(p) + .orIgnore() + .execute(); + } + const allPerms = await this.permRepo.find(); + + // Step 2: 幂等插入预置角色 + for (const r of PRESET_ROLES) { + await this.roleRepo + .createQueryBuilder() + .insert() + .into(Role) + .values({ name: r.name, description: r.description, isSystem: r.isSystem }) + .orIgnore() + .execute(); + } + const allRoles = await this.roleRepo.find({ relations: ['permissions'] }); + + // Step 3: 构建角色-权限关联 + for (const preset of PRESET_ROLES) { + const role = allRoles.find((r) => r.name === preset.name); + if (!role) continue; + + let perms: Permission[]; + if (preset.permissionGroups.length === 0) { + // 超管:全部权限 + perms = allPerms; + } else { + // 按 group 匹配 + 额外权限(如老师的 student:view) + const byGroup = allPerms.filter((p) => preset.permissionGroups.includes(p.group)); + const byExtra = preset.extraPermissions + ? allPerms.filter((p) => preset.extraPermissions!.includes(p.code)) + : []; + perms = [...byGroup, ...byExtra].filter( + (p, i, arr) => arr.findIndex((x) => x.id === p.id) === i, + ); + } + + // 幂等:只插入尚未关联的 + const existingIds = new Set(role.permissions.map((p) => p.id)); + const toAdd = perms.filter((p) => !existingIds.has(p.id)); + if (toAdd.length > 0) { + role.permissions = [...role.permissions, ...toAdd]; + await this.roleRepo.save(role); + } + } + + // Step 4: 初始化 admin 用户 + const count = await this.userRepo.count(); + if (count === 0) { + const adminPassword = process.env.ADMIN_PASSWORD || 'admin123'; + const hash = await bcrypt.hash(adminPassword, 10); + const adminUser = this.userRepo.create({ + username: 'admin', + passwordHash: hash, + name: '管理员', + }); + const superAdminRole = allRoles.find((r) => r.name === '超管'); + if (superAdminRole) { + adminUser.roles = [superAdminRole]; + } + await this.userRepo.save(adminUser); + this.logger.log( + `已创建默认管理员: admin / ${adminPassword === 'admin123' ? 'admin123 (请尽快修改!)' : '******'}`, + ); + } + + this.logger.log(`种子数据初始化完成: ${allPerms.length} 权限点, ${allRoles.length} 角色`); + } + + async findAllRoles(): Promise { + return this.roleRepo.find({ + relations: ['permissions'], + order: { id: 'ASC' }, + }); + } + + async findRoleById(id: number): Promise { + return this.roleRepo.findOneOrFail({ where: { id }, relations: ['permissions'] }); + } + + async createRole(dto: { + name: string; + description?: string; + permissionIds?: number[]; + }): Promise { + const role = this.roleRepo.create({ name: dto.name, description: dto.description }); + if (dto.permissionIds && dto.permissionIds.length > 0) { + role.permissions = await this.permRepo.findByIds(dto.permissionIds); + } + return this.roleRepo.save(role); + } + + async updateRole( + id: number, + dto: { name?: string; description?: string; permissionIds?: number[] }, + ): Promise { + const role = await this.roleRepo.findOneOrFail({ + where: { id }, + relations: ['permissions'], + }); + if (dto.name !== undefined && !role.isSystem) role.name = dto.name; + if (dto.description !== undefined) role.description = dto.description; + if (dto.permissionIds !== undefined) { + role.permissions = + dto.permissionIds.length > 0 + ? await this.permRepo.findByIds(dto.permissionIds) + : []; + } + return this.roleRepo.save(role); + } + + async deleteRole(id: number): Promise<{ message: string }> { + const role = await this.roleRepo.findOneOrFail({ where: { id } }); + if (role.isSystem) throw new Error('系统角色不可删除'); + await this.roleRepo.remove(role); + return { message: '角色已删除' }; + } + + async findAllPermissions(): Promise { + return this.permRepo.find({ order: { group: 'ASC', code: 'ASC' } }); + } + + async getPermissionTree(): Promise<{ group: string; permissions: Permission[] }[]> { + const all = await this.findAllPermissions(); + const map = new Map(); + for (const p of all) { + if (!map.has(p.group)) map.set(p.group, []); + map.get(p.group)!.push(p); + } + return Array.from(map.entries()).map(([group, permissions]) => ({ group, permissions })); + } + + async getUserPermissions(userId: number): Promise { + const user = await this.userRepo.findOne({ + where: { id: userId }, + relations: ['roles', 'roles.permissions'], + }); + if (!user || !user.roles) return []; + const codes = new Set(); + for (const role of user.roles) { + if (role.status !== 1) continue; + for (const perm of role.permissions) { + codes.add(perm.code); + } + } + return Array.from(codes); + } +}