Files
gongxue-base/backend/src/auth/auth.service.ts
陈浩 78676a124a feat: 恭学教育基地管理系统初始提交
- 后端: NestJS 11 + TypeORM + JWT认证 + SQLite/MySQL
- 前端: React 19 + Ant Design 6 + Vite 8 + ECharts
- 功能模块: 数据面板、学生管理、宿舍管理、入住管理、费用录入、账单管理、教室管理、押金管理、操作日志、账号管理
- 支持Docker一键部署
2026-06-06 17:26:10 +08:00

150 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, { count: number; lockedUntil?: Date }>();
const MAX_ATTEMPTS = 5;
const LOCK_MINUTES = 15;
@Injectable()
export class AuthService {
constructor(
@InjectRepository(User) private userRepo: Repository<User>,
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: '用户已删除' };
}
}