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:
2026-07-02 15:05:12 +08:00
parent 4704adcba1
commit 46a817503e
137 changed files with 52 additions and 0 deletions

View 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 } });
}
}