import { Injectable, UnauthorizedException, BadRequestException, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, Not, MoreThan } 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'; // 内存中的登录失败计数器(按IP+用户名) const loginAttempts = new Map(); const MAX_ATTEMPTS = 5; const LOCK_MINUTES = 15; @Injectable() export class AuthService { constructor( @InjectRepository(User) private userRepo: Repository, private jwtService: JwtService, private configService: ConfigService, ) {} 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); // 检查是否在锁定期 if (attempt?.lockedUntil && attempt.lockedUntil > new Date()) { const remaining = Math.ceil((attempt.lockedUntil.getTime() - Date.now()) / 60000); throw new UnauthorizedException(`账号已被临时锁定,请 ${remaining} 分钟后重试`); } const user = await this.userRepo.findOne({ where: { username: dto.username } }); if (!user) { this.recordFailedAttempt(attemptKey); throw new UnauthorizedException('用户名或密码错误'); } if (!user.isActive) throw new UnauthorizedException('账号已被禁用,请联系管理员'); const valid = await bcrypt.compare(dto.password, user.passwordHash); if (!valid) { this.recordFailedAttempt(attemptKey); const att = loginAttempts.get(attemptKey); const remaining = MAX_ATTEMPTS - (att?.count || 0); if (remaining > 0) { throw new UnauthorizedException(`用户名或密码错误,还剩 ${remaining} 次尝试机会`); } throw new UnauthorizedException(`登录失败次数过多,账号已被锁定 ${LOCK_MINUTES} 分钟`); } // 登录成功,清除失败计数 loginAttempts.delete(attemptKey); // 记录登录时间 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 } }; } private recordFailedAttempt(key: string) { const attempt = loginAttempts.get(key) || { count: 0 }; attempt.count++; if (attempt.count >= MAX_ATTEMPTS) { attempt.lockedUntil = new Date(Date.now() + LOCK_MINUTES * 60 * 1000); } loginAttempts.set(key, attempt); } 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: '用户已删除' }; } }