feat(rbac): integrate RbacService into AuthService login, update JWT payload

This commit is contained in:
2026-07-02 11:36:04 +08:00
parent 7ea0d268fb
commit 80ff225e92
4 changed files with 41 additions and 140 deletions

View File

@@ -1,17 +1,19 @@
import { Controller, Post, Body, UseGuards, Get, Put, Delete, Param, Request, Req } from '@nestjs/common';
import { Controller, Post, Body, Get, Request, Req, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto, RegisterDto } from './dto/auth.dto';
import { LoginDto } from './dto/auth.dto';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { OperationLogsService } from '../operation-logs/operation-logs.service';
import { extractRequestInfo } from '../common/request-utils';
import { Throttle } from '@nestjs/throttler';
import { Public } from './decorators/public.decorator';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService, private logService: OperationLogsService) {}
@Public()
@Post('login')
@Throttle({ default: { ttl: 60000, limit: 5 } }) // 登录接口每分钟最多5次
@Throttle({ default: { ttl: 60000, limit: 5 } })
async login(@Body() dto: LoginDto, @Req() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
try {
@@ -33,53 +35,9 @@ export class AuthController {
}
}
@Post('register')
@UseGuards(JwtAuthGuard)
async register(@Body() dto: RegisterDto, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.authService.register(dto);
await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账号', action: '创建账号', detail: `用户名: ${dto.username}, 姓名: ${dto.name}`, ipAddress, userAgent });
return result;
}
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req: any) {
return req.user;
}
// ---- 用户管理 ----
@UseGuards(JwtAuthGuard)
@Get('users')
findAllUsers() {
return this.authService.findAllUsers();
}
@UseGuards(JwtAuthGuard)
@Put('users/:id')
async updateUser(@Param('id') id: string, @Body() body: { name?: string; role?: string; isActive?: boolean; username?: string; allowedMenus?: string[] }, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.authService.updateUser(+id, body);
await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账号', action: '更新账号', targetId: +id, targetType: 'user', detail: JSON.stringify(body), ipAddress, userAgent });
return result;
}
@UseGuards(JwtAuthGuard)
@Put('users/:id/password')
async resetPassword(@Param('id') id: string, @Body() body: { password: string }, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.authService.resetPassword(+id, body.password);
await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账号', action: '重置密码', targetId: +id, targetType: 'user', ipAddress, userAgent });
return result;
}
@UseGuards(JwtAuthGuard)
@Delete('users/:id')
async removeUser(@Param('id') id: string, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.authService.removeUser(+id);
await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账号', action: '删除账号', targetId: +id, targetType: 'user', ipAddress, userAgent });
return result;
}
}

View File

@@ -1,4 +1,4 @@
import { Module, OnModuleInit } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
@@ -7,6 +7,7 @@ import { User } from '../entities/user.entity';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategies/jwt.strategy';
import { RbacModule } from '../rbac/rbac.module';
@Module({
imports: [
@@ -20,14 +21,10 @@ import { JwtStrategy } from './strategies/jwt.strategy';
signOptions: { expiresIn: config.get('JWT_EXPIRES_IN', '4h') },
}),
}),
forwardRef(() => RbacModule),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule implements OnModuleInit {
constructor(private authService: AuthService) {}
async onModuleInit() {
await this.authService.initAdmin();
}
}
export class AuthModule {}

View File

@@ -1,11 +1,11 @@
import { Injectable, UnauthorizedException, BadRequestException, NotFoundException } from '@nestjs/common';
import { Injectable, UnauthorizedException, forwardRef, Inject } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Not, MoreThan } from 'typeorm';
import { Repository } from 'typeorm';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcryptjs';
import { User } from '../entities/user.entity';
import { LoginDto, RegisterDto } from './dto/auth.dto';
import { LoginDto } from './dto/auth.dto';
import { RbacService } from '../rbac/rbac.service';
// 内存中的登录失败计数器按IP+用户名)
const loginAttempts = new Map<string, { count: number; lockedUntil?: Date }>();
@@ -17,24 +17,9 @@ export class AuthService {
constructor(
@InjectRepository(User) private userRepo: Repository<User>,
private jwtService: JwtService,
private configService: ConfigService,
@Inject(forwardRef(() => RbacService)) private rbacService: RbacService,
) {}
async register(dto: RegisterDto) {
const exists = await this.userRepo.findOne({ where: { username: dto.username } });
if (exists) throw new UnauthorizedException('用户名已存在');
const hash = await bcrypt.hash(dto.password, 10);
const user = this.userRepo.create({
username: dto.username,
passwordHash: hash,
name: dto.name,
role: 'operator',
allowedMenus: (dto as any).allowedMenus ? JSON.stringify((dto as any).allowedMenus) : null as any,
});
await this.userRepo.save(user);
return { message: '注册成功' };
}
async login(dto: LoginDto, ip?: string) {
const attemptKey = `${ip || 'unknown'}:${dto.username}`;
const attempt = loginAttempts.get(attemptKey);
@@ -45,7 +30,10 @@ export class AuthService {
throw new UnauthorizedException(`账号已被临时锁定,请 ${remaining} 分钟后重试`);
}
const user = await this.userRepo.findOne({ where: { username: dto.username } });
const user = await this.userRepo.findOne({
where: { username: dto.username },
relations: ['roles'],
});
if (!user) {
this.recordFailedAttempt(attemptKey);
throw new UnauthorizedException('用户名或密码错误');
@@ -68,9 +56,24 @@ export class AuthService {
// 记录登录时间
user.lastLoginAt = new Date();
await this.userRepo.save(user);
const payload = { sub: user.id, username: user.username, role: user.role };
const allowedMenus = user.allowedMenus ? JSON.parse(user.allowedMenus) : null;
return { access_token: this.jwtService.sign(payload), user: { id: user.id, username: user.username, name: user.name, role: user.role, allowedMenus } };
// 获取用户权限
const permissions = await this.rbacService.getUserPermissions(user.id);
const payload = { sub: user.id, username: user.username, permissions };
// 获取角色名称列表
const roleNames = user.roles ? user.roles.filter(r => r.status === 1).map(r => r.name) : [];
return {
access_token: this.jwtService.sign(payload),
user: {
id: user.id,
username: user.username,
name: user.name,
roles: roleNames,
permissions,
},
};
}
private recordFailedAttempt(key: string) {
@@ -85,65 +88,4 @@ export class AuthService {
async validateUser(payload: any) {
return this.userRepo.findOne({ where: { id: payload.sub } });
}
async initAdmin() {
const count = await this.userRepo.count();
if (count === 0) {
const adminPassword = this.configService.get('ADMIN_PASSWORD', 'admin123');
const hash = await bcrypt.hash(adminPassword, 10);
await this.userRepo.save(this.userRepo.create({ username: 'admin', passwordHash: hash, name: '管理员', role: 'admin' }));
console.log(`已创建默认管理员: admin / ${adminPassword === 'admin123' ? 'admin123 (请尽快修改!)' : '******'}`);
}
}
// ---- 用户管理 CRUD ----
async findAllUsers() {
const users = await this.userRepo.find({
select: ['id', 'username', 'name', 'role', 'isActive', 'allowedMenus', 'lastLoginAt', 'createdAt', 'updatedAt'],
order: { createdAt: 'DESC' },
});
return users.map(u => ({
...u,
allowedMenus: u.allowedMenus ? JSON.parse(u.allowedMenus) : null,
}));
}
async updateUser(id: number, data: { name?: string; role?: string; isActive?: boolean; username?: string; allowedMenus?: string[] }) {
const user = await this.userRepo.findOne({ where: { id } });
if (!user) throw new NotFoundException('用户不存在');
if (user.username === 'admin' && data.role && data.role !== 'admin') {
throw new BadRequestException('不能修改默认管理员的角色');
}
if (user.username === 'admin' && data.isActive === false) {
throw new BadRequestException('不能禁用默认管理员');
}
if (data.username !== undefined && data.username !== user.username) {
const exists = await this.userRepo.findOne({ where: { username: data.username } });
if (exists) throw new BadRequestException('用户名已存在');
user.username = data.username;
}
if (data.name !== undefined) user.name = data.name;
if (data.role !== undefined) user.role = data.role;
if (data.isActive !== undefined) user.isActive = data.isActive;
if (data.allowedMenus !== undefined) user.allowedMenus = data.allowedMenus ? JSON.stringify(data.allowedMenus) : null as any;
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 NotFoundException('用户不存在');
user.passwordHash = await bcrypt.hash(newPassword, 10);
await this.userRepo.save(user);
return { message: '密码已重置' };
}
async removeUser(id: number) {
const user = await this.userRepo.findOne({ where: { id } });
if (!user) throw new NotFoundException('用户不存在');
if (user.username === 'admin') throw new BadRequestException('不能删除默认管理员');
await this.userRepo.delete(id);
return { message: '用户已删除' };
}
}

View File

@@ -14,6 +14,10 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
}
async validate(payload: any) {
return { id: payload.sub, username: payload.username, role: payload.role };
return {
id: payload.sub,
username: payload.username,
permissions: payload.permissions || [],
};
}
}