Files
seclusion/apps/api/src/auth/auth.service.ts
charilezhou d747f98d08 refactor(api): 优化 PrismaService 类型设计,修复依赖注入问题
- 重构 PrismaService 使其类型包含所有模型访问器
- 使用 PrismaServiceImpl 内部类 + 类型断言导出 PrismaService
- 所有 Service 现在可以直接使用 PrismaService 类型注入
- 修复 NestJS 依赖注入无法识别类型别名的问题
- 统一各 Service 的 PrismaService 导入方式

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 12:34:11 +08:00

222 lines
6.2 KiB
TypeScript

import { Injectable, UnauthorizedException, ConflictException, Logger, BadRequestException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import type { TokenPayload, RefreshTokenResponse, SendResetPasswordEmailResponse, ResetPasswordResponse } from '@seclusion/shared';
import { CaptchaScene, SystemRoleCode } from '@seclusion/shared';
import * as bcrypt from 'bcrypt';
import { CaptchaService } from '../common/captcha/captcha.service';
import { EmailCodeService } from '../common/email-code/email-code.service';
import { MailService } from '../common/mail/mail.service';
import { RegisterDto, LoginDto, RefreshTokenDto } from './dto/auth.dto';
import { SendResetPasswordEmailDto, ResetPasswordDto } from './dto/reset-password.dto';
import { PrismaService } from '@/prisma/prisma.service';
// Token 有效期配置(秒)
const ACCESS_TOKEN_EXPIRES_IN = 5 * 60; // 5 分钟
const REFRESH_TOKEN_EXPIRES_IN = 30 * 24 * 60 * 60; // 30 天
@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
private captchaService: CaptchaService,
private emailCodeService: EmailCodeService,
private mailService: MailService
) {}
async register(dto: RegisterDto) {
// 验证验证码
await this.captchaService.verifyOrThrow({
captchaId: dto.captchaId,
code: dto.captchaCode,
scene: CaptchaScene.REGISTER,
});
// 底层自动过滤已删除用户
const existingUser = await this.prisma.user.findFirst({
where: { email: dto.email },
});
if (existingUser) {
throw new ConflictException('该邮箱已被注册');
}
// 加密密码
const hashedPassword = await bcrypt.hash(dto.password, 10);
// 查找默认用户角色
const userRole = await this.prisma.role.findUnique({
where: { code: SystemRoleCode.USER },
});
// 创建用户并分配默认角色
const user = await this.prisma.user.create({
data: {
email: dto.email,
password: hashedPassword,
name: dto.name,
// 如果存在 user 角色则自动分配
...(userRole && {
roles: {
create: { roleId: userRole.id },
},
}),
},
});
if (!userRole) {
this.logger.warn(`默认用户角色 "${SystemRoleCode.USER}" 不存在,新用户未分配角色`);
}
// 生成 token
const tokens = await this.generateTokens(user.id, user.email);
return {
user: {
id: user.id,
email: user.email,
name: user.name,
avatarId: user.avatarId,
},
...tokens,
};
}
async login(dto: LoginDto) {
// 验证验证码
await this.captchaService.verifyOrThrow({
captchaId: dto.captchaId,
code: dto.captchaCode,
scene: CaptchaScene.LOGIN,
});
// 底层自动过滤已删除用户
const user = await this.prisma.user.findFirst({
where: { email: dto.email },
});
if (!user) {
throw new UnauthorizedException('邮箱或密码错误');
}
// 验证密码
const isPasswordValid = await bcrypt.compare(dto.password, user.password);
if (!isPasswordValid) {
throw new UnauthorizedException('邮箱或密码错误');
}
// 生成 token
const tokens = await this.generateTokens(user.id, user.email);
return {
user: {
id: user.id,
email: user.email,
name: user.name,
avatarId: user.avatarId,
},
...tokens,
};
}
private async generateTokens(userId: string, email: string) {
const payload: Omit<TokenPayload, 'iat' | 'exp'> = { sub: userId, email };
const accessToken = await this.jwtService.signAsync(payload, {
expiresIn: ACCESS_TOKEN_EXPIRES_IN,
});
const refreshToken = await this.jwtService.signAsync(payload, {
expiresIn: REFRESH_TOKEN_EXPIRES_IN,
});
return {
accessToken,
refreshToken,
accessTokenExpiresIn: ACCESS_TOKEN_EXPIRES_IN,
refreshTokenExpiresIn: REFRESH_TOKEN_EXPIRES_IN,
};
}
async refreshToken(dto: RefreshTokenDto): Promise<RefreshTokenResponse> {
try {
// 验证 refresh token
const payload = await this.jwtService.verifyAsync<TokenPayload>(dto.refreshToken);
// 检查用户是否存在
const user = await this.prisma.user.findUnique({
where: { id: payload.sub },
});
if (!user) {
throw new UnauthorizedException('用户不存在');
}
// 生成新的 token 对
return this.generateTokens(user.id, user.email);
} catch (error) {
if (error instanceof UnauthorizedException) {
throw error;
}
throw new UnauthorizedException('刷新令牌无效或已过期');
}
}
async sendResetPasswordEmail(dto: SendResetPasswordEmailDto): Promise<SendResetPasswordEmailResponse> {
// 验证图形验证码
await this.captchaService.verifyOrThrow({
captchaId: dto.captchaId,
code: dto.captchaCode,
scene: CaptchaScene.RESET_PASSWORD,
});
// 检查用户是否存在
const user = await this.prisma.user.findFirst({
where: { email: dto.email },
});
if (!user) {
throw new BadRequestException('该邮箱未注册');
}
// 生成邮箱验证码
const { emailCodeId, code, expiresIn } = await this.emailCodeService.generate(dto.email);
// 发送邮件
await this.mailService.sendResetPasswordCode(dto.email, code);
return { emailCodeId, expiresIn };
}
async resetPassword(dto: ResetPasswordDto): Promise<ResetPasswordResponse> {
// 验证邮箱验证码并获取邮箱
const email = await this.emailCodeService.verify(dto.emailCodeId, dto.emailCode);
// 查找用户
const user = await this.prisma.user.findFirst({
where: { email },
});
if (!user) {
throw new BadRequestException('用户不存在');
}
// 更新密码
const hashedPassword = await bcrypt.hash(dto.password, 10);
await this.prisma.user.update({
where: { id: user.id },
data: { password: hashedPassword },
});
this.logger.log(`用户 ${email} 密码重置成功`);
return { message: '密码重置成功' };
}
}