diff --git a/backend/src/rbac/dto/rbac.dto.ts b/backend/src/rbac/dto/rbac.dto.ts new file mode 100644 index 0000000..b3d4924 --- /dev/null +++ b/backend/src/rbac/dto/rbac.dto.ts @@ -0,0 +1,68 @@ +import { IsString, MinLength, IsOptional, IsArray, IsBoolean } from 'class-validator'; + +export class CreateRoleDto { + @IsString() + name: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsArray() + permissionIds?: number[]; +} + +export class UpdateRoleDto { + @IsOptional() + @IsString() + name?: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsArray() + permissionIds?: number[]; +} + +export class CreateUserDto { + @IsString() + username: string; + + @IsString() + @MinLength(4) + password: string; + + @IsString() + name: string; + + @IsOptional() + @IsArray() + roleIds?: number[]; +} + +export class UpdateUserDto { + @IsOptional() + @IsString() + username?: string; + + @IsOptional() + @IsString() + name?: string; + + @IsOptional() + @IsBoolean() + isActive?: boolean; + + @IsOptional() + @IsArray() + roleIds?: number[]; +} + +export class ResetPasswordDto { + @IsString() + @MinLength(4) + password: string; +} diff --git a/backend/src/rbac/rbac.controller.ts b/backend/src/rbac/rbac.controller.ts new file mode 100644 index 0000000..83e81d6 --- /dev/null +++ b/backend/src/rbac/rbac.controller.ts @@ -0,0 +1,141 @@ +import { + Controller, Get, Post, Put, Delete, Body, Param, UseGuards, Request, BadRequestException, +} from '@nestjs/common'; +import { RbacService } from './rbac.service'; +import { CreateRoleDto, UpdateRoleDto, CreateUserDto, UpdateUserDto, ResetPasswordDto } from './dto/rbac.dto'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { RequirePermission } from '../auth/decorators/permission.decorator'; +import { OperationLogsService } from '../operation-logs/operation-logs.service'; +import { extractRequestInfo } from '../common/request-utils'; + +@UseGuards(JwtAuthGuard) +@Controller('rbac') +export class RbacController { + constructor( + private rbacService: RbacService, + private logService: OperationLogsService, + ) {} + + // ==================== 角色管理 ==================== + + @Get('roles') + @RequirePermission('role:view') + findAllRoles() { + return this.rbacService.findAllRoles(); + } + + @Get('roles/:id') + @RequirePermission('role:view') + findRoleById(@Param('id') id: string) { + return this.rbacService.findRoleById(+id); + } + + @Post('roles') + @RequirePermission('role:create') + async createRole(@Body() dto: CreateRoleDto, @Request() req: any) { + const { ipAddress, userAgent } = extractRequestInfo(req); + const result = await this.rbacService.createRole(dto); + await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: 'RBAC', action: '创建角色', detail: `角色: ${dto.name}`, ipAddress, userAgent }); + return result; + } + + @Put('roles/:id') + @RequirePermission('role:edit') + async updateRole(@Param('id') id: string, @Body() dto: UpdateRoleDto, @Request() req: any) { + const { ipAddress, userAgent } = extractRequestInfo(req); + try { + const result = await this.rbacService.updateRole(+id, dto); + await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: 'RBAC', action: '编辑角色', targetId: +id, targetType: 'role', detail: JSON.stringify(dto), ipAddress, userAgent }); + return result; + } catch (e: any) { + throw new BadRequestException(e.message); + } + } + + @Delete('roles/:id') + @RequirePermission('role:delete') + async deleteRole(@Param('id') id: string, @Request() req: any) { + const { ipAddress, userAgent } = extractRequestInfo(req); + try { + const result = await this.rbacService.deleteRole(+id); + await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: 'RBAC', action: '删除角色', targetId: +id, targetType: 'role', ipAddress, userAgent }); + return result; + } catch (e: any) { + throw new BadRequestException(e.message); + } + } + + // ==================== 权限管理 ==================== + + @Get('permissions') + @RequirePermission('role:view') + findAllPermissions() { + return this.rbacService.findAllPermissions(); + } + + @Get('permissions/tree') + @RequirePermission('role:view') + getPermissionTree() { + return this.rbacService.getPermissionTree(); + } + + // ==================== 用户管理 ==================== + + @Get('users') + @RequirePermission('user:view') + getUsers() { + return this.rbacService.findAllUsers(); + } + + @Post('users') + @RequirePermission('user:create') + async createUser(@Body() dto: CreateUserDto, @Request() req: any) { + const { ipAddress, userAgent } = extractRequestInfo(req); + try { + const result = await this.rbacService.createUser(dto); + await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账号', action: '创建账号', detail: `用户名: ${dto.username}`, ipAddress, userAgent }); + return result; + } catch (e: any) { + throw new BadRequestException(e.message); + } + } + + @Put('users/:id') + @RequirePermission('user:edit') + async updateUser(@Param('id') id: string, @Body() dto: UpdateUserDto, @Request() req: any) { + const { ipAddress, userAgent } = extractRequestInfo(req); + try { + const result = await this.rbacService.updateUser(+id, dto); + await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账号', action: '更新账号', targetId: +id, targetType: 'user', detail: JSON.stringify(dto), ipAddress, userAgent }); + return result; + } catch (e: any) { + throw new BadRequestException(e.message); + } + } + + @Put('users/:id/password') + @RequirePermission('user:reset-password') + async resetPassword(@Param('id') id: string, @Body() dto: ResetPasswordDto, @Request() req: any) { + const { ipAddress, userAgent } = extractRequestInfo(req); + try { + const result = await this.rbacService.resetPassword(+id, dto.password); + await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账号', action: '重置密码', targetId: +id, targetType: 'user', ipAddress, userAgent }); + return result; + } catch (e: any) { + throw new BadRequestException(e.message); + } + } + + @Delete('users/:id') + @RequirePermission('user:delete') + async deleteUser(@Param('id') id: string, @Request() req: any) { + const { ipAddress, userAgent } = extractRequestInfo(req); + try { + const result = await this.rbacService.deleteUser(+id); + await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账号', action: '删除账号', targetId: +id, targetType: 'user', ipAddress, userAgent }); + return result; + } catch (e: any) { + throw new BadRequestException(e.message); + } + } +} diff --git a/backend/src/rbac/rbac.module.ts b/backend/src/rbac/rbac.module.ts index 7ce50b9..c1a2198 100644 --- a/backend/src/rbac/rbac.module.ts +++ b/backend/src/rbac/rbac.module.ts @@ -2,6 +2,7 @@ import { Module, OnModuleInit, forwardRef } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Permission, Role, User } from '../entities'; import { RbacService } from './rbac.service'; +import { RbacController } from './rbac.controller'; import { AuthModule } from '../auth/auth.module'; @Module({ @@ -9,7 +10,7 @@ import { AuthModule } from '../auth/auth.module'; TypeOrmModule.forFeature([Permission, Role, User]), forwardRef(() => AuthModule), ], - controllers: [], + controllers: [RbacController], providers: [RbacService], exports: [RbacService], }) diff --git a/backend/src/rbac/rbac.service.ts b/backend/src/rbac/rbac.service.ts index 2fc483b..c0f8a7d 100644 --- a/backend/src/rbac/rbac.service.ts +++ b/backend/src/rbac/rbac.service.ts @@ -262,4 +262,70 @@ export class RbacService { } return Array.from(codes); } + + // ---- 用户管理 ---- + + async findAllUsers() { + const users = await this.userRepo.find({ + relations: ['roles'], + order: { createdAt: 'DESC' }, + }); + return users.map(u => ({ + id: u.id, + username: u.username, + name: u.name, + isActive: u.isActive, + lastLoginAt: u.lastLoginAt, + createdAt: u.createdAt, + updatedAt: u.updatedAt, + roles: u.roles?.map(r => ({ id: r.id, name: r.name })) || [], + })); + } + + async createUser(dto: { username: string; password: string; name: string; roleIds?: number[] }) { + const exists = await this.userRepo.findOne({ where: { username: dto.username } }); + if (exists) throw new Error('用户名已存在'); + const hash = await bcrypt.hash(dto.password, 10); + const user = this.userRepo.create({ username: dto.username, passwordHash: hash, name: dto.name }); + if (dto.roleIds && dto.roleIds.length > 0) { + user.roles = await this.roleRepo.findByIds(dto.roleIds); + } + await this.userRepo.save(user); + return { message: '用户创建成功' }; + } + + async updateUser(id: number, dto: { username?: string; name?: string; isActive?: boolean; roleIds?: number[] }) { + const user = await this.userRepo.findOne({ where: { id }, relations: ['roles'] }); + if (!user) throw new Error('用户不存在'); + if (dto.username !== undefined && dto.username !== user.username) { + const exists = await this.userRepo.findOne({ where: { username: dto.username } }); + if (exists) throw new Error('用户名已存在'); + user.username = dto.username; + } + if (dto.name !== undefined) user.name = dto.name; + if (dto.isActive !== undefined) user.isActive = dto.isActive; + if (dto.roleIds !== undefined) { + user.roles = dto.roleIds.length > 0 + ? await this.roleRepo.findByIds(dto.roleIds) + : []; + } + await this.userRepo.save(user); + return { message: '更新成功' }; + } + + async resetPassword(id: number, newPassword: string) { + const user = await this.userRepo.findOne({ where: { id } }); + if (!user) throw new Error('用户不存在'); + user.passwordHash = await bcrypt.hash(newPassword, 10); + await this.userRepo.save(user); + return { message: '密码已重置' }; + } + + async deleteUser(id: number) { + const user = await this.userRepo.findOne({ where: { id } }); + if (!user) throw new Error('用户不存在'); + if (user.username === 'admin') throw new Error('不能删除默认管理员'); + await this.userRepo.remove(user); + return { message: '用户已删除' }; + } }