forked from wangziqi/gongxue-base
feat(task1): restructure directories for turborepo monorepo
- Move backend/ to apps/server/ via git mv - Move frontend/ to apps/admin/ via git mv - Create packages/typescript-config/ with base, nestjs, and react-vite presets
This commit is contained in:
91
apps/server/src/auth/auth.service.ts
Normal file
91
apps/server/src/auth/auth.service.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
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.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 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) {
|
||||
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 } });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user