100 lines
3.4 KiB
TypeScript
100 lines
3.4 KiB
TypeScript
import { Injectable, UnauthorizedException, forwardRef, Inject } from '@nestjs/common';
|
||
import { InjectRepository } from '@nestjs/typeorm';
|
||
import { Repository } from 'typeorm';
|
||
import { JwtService } from '@nestjs/jwt';
|
||
import * as bcrypt from 'bcryptjs';
|
||
import { User } from '../entities/user.entity';
|
||
import { LoginDto } from './dto/auth.dto';
|
||
import { RbacService } from '../rbac/rbac.service';
|
||
|
||
// 内存中的登录失败计数器(按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,
|
||
@Inject(forwardRef(() => RbacService)) private rbacService: RbacService,
|
||
) {}
|
||
|
||
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 },
|
||
relations: ['roles'],
|
||
});
|
||
if (!user) {
|
||
this.recordFailedAttempt(attemptKey);
|
||
throw new UnauthorizedException('用户名或密码错误');
|
||
}
|
||
if (user.isArchived) {
|
||
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 permissions = await this.rbacService.getUserPermissions(user.id);
|
||
const isSuperAdmin =
|
||
user.roles?.some(
|
||
(role) =>
|
||
role.status === 1 &&
|
||
(role.name === '超管' || role.name === 'super_admin' || role.code === 'super_admin'),
|
||
) ?? false;
|
||
const payload = { sub: user.id, username: user.username, permissions, isSuperAdmin };
|
||
|
||
// 获取角色名称列表
|
||
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) {
|
||
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 } });
|
||
}
|
||
}
|