feat(rbac): integrate RbacService into AuthService login, update JWT payload
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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: '用户已删除' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 || [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user