- 重构 PrismaService 使其类型包含所有模型访问器 - 使用 PrismaServiceImpl 内部类 + 类型断言导出 PrismaService - 所有 Service 现在可以直接使用 PrismaService 类型注入 - 修复 NestJS 依赖注入无法识别类型别名的问题 - 统一各 Service 的 PrismaService 导入方式 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
222 lines
6.2 KiB
TypeScript
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: '密码重置成功' };
|
|
}
|
|
}
|