forked from wangziqi/gongxue-base
- 后端: NestJS 11 + TypeORM + JWT认证 + SQLite/MySQL - 前端: React 19 + Ant Design 6 + Vite 8 + ECharts - 功能模块: 数据面板、学生管理、宿舍管理、入住管理、费用录入、账单管理、教室管理、押金管理、操作日志、账号管理 - 支持Docker一键部署
150 lines
6.3 KiB
TypeScript
150 lines
6.3 KiB
TypeScript
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: '用户已删除' };
|
||
}
|
||
}
|