import { Injectable, ForbiddenException, UnauthorizedException } from '@nestjs/common'; import { PrismaService } from '../../infrastructure/database/prisma.service'; import { AppleAuthService } from './apple-auth.service'; import { TokenService } from './token.service'; import type { AppleLoginDto, DevLoginDto } from './dto'; @Injectable() export class AuthService { constructor( private readonly prisma: PrismaService, private readonly appleAuthService: AppleAuthService, private readonly tokenService: TokenService, ) {} async devLogin(dto: DevLoginDto) { if (process.env.NODE_ENV === 'production') { throw new ForbiddenException('dev-login is disabled in production'); } const devSecret = process.env.DEV_SECRET; if (!devSecret || dto.devSecret !== devSecret) { throw new UnauthorizedException('devSecret 无效'); } const providerUserId = dto.email; let account = await this.prisma.authAccount.findUnique({ where: { provider_providerUserId: { provider: 'DEV', providerUserId, }, }, include: { user: true }, }); if (!account) { account = await this.prisma.authAccount.create({ data: { provider: 'DEV', providerUserId, email: dto.email, user: { create: { email: dto.email, nickname: dto.nickname || '测试用户', status: 'active', }, }, }, include: { user: true }, }); } return this.buildLoginResponse(account.user); } async appleLogin(dto: AppleLoginDto) { const { appleUserId, email: appleEmail } = await this.appleAuthService.verifyIdentityToken(dto.identityToken); let account = await this.prisma.authAccount.findUnique({ where: { provider_providerUserId: { provider: 'APPLE', providerUserId: appleUserId, }, }, include: { user: true }, }); if (!account) { const displayName = dto.fullName?.givenName ? `${dto.fullName.familyName || ''}${dto.fullName.givenName}` : undefined; account = await this.prisma.authAccount.create({ data: { provider: 'APPLE', providerUserId: appleUserId, email: appleEmail || dto.email || null, user: { create: { email: appleEmail || dto.email || null, nickname: displayName || undefined, status: 'active', }, }, }, include: { user: true }, }); } return this.buildLoginResponse(account.user); } async refresh(refreshToken: string) { const hash = this.tokenService.hashToken(refreshToken); const stored = await this.prisma.refreshToken.findFirst({ where: { tokenHash: hash, revokedAt: null }, include: { user: true }, }); if (!stored || stored.expiresAt < new Date()) { throw new UnauthorizedException('刷新令牌无效或已过期'); } await this.prisma.refreshToken.update({ where: { id: stored.id }, data: { revokedAt: new Date() }, }); const { token: newRefreshToken, hash: newHash } = this.tokenService.generateRefreshToken(); await this.prisma.refreshToken.create({ data: { userId: stored.userId, tokenHash: newHash, expiresAt: new Date(Date.now() + 7 * 86400000), }, }); const accessToken = await this.tokenService.generateAccessToken( stored.user, ); return { accessToken, refreshToken: newRefreshToken, user: this.serializeUser(stored.user), }; } async logout(userId: bigint | string, refreshToken: string) { const hash = this.tokenService.hashToken(refreshToken); const stored = await this.prisma.refreshToken.findFirst({ where: { tokenHash: hash, userId: BigInt(userId), revokedAt: null, }, }); if (stored) { await this.prisma.refreshToken.update({ where: { id: stored.id }, data: { revokedAt: new Date() }, }); } } private async buildLoginResponse(user: { id: bigint; email: string | null; nickname: string | null; avatarUrl: string | null; role: string; status: string; onboardingCompleted: boolean; }) { const accessToken = await this.tokenService.generateAccessToken(user); const { token: refreshToken, hash } = this.tokenService.generateRefreshToken(); await this.prisma.refreshToken.create({ data: { userId: user.id, tokenHash: hash, expiresAt: new Date(Date.now() + 7 * 86400000), }, }); return { accessToken, refreshToken, user: this.serializeUser(user), }; } private serializeUser(user: { id: bigint; email: string | null; nickname: string | null; avatarUrl: string | null; role: string; status: string; onboardingCompleted: boolean; }) { return { id: String(user.id), email: user.email, nickname: user.nickname, avatarUrl: user.avatarUrl, role: user.role, status: user.status, onboardingCompleted: user.onboardingCompleted, }; } }