import * as crypto from 'crypto'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { PrismaService } from '../../infrastructure/database/prisma.service'; @Injectable() export class AuthService { constructor( private readonly jwtService: JwtService, private readonly prisma: PrismaService, private readonly configService: ConfigService, ) {} async appleLogin(params: { identityToken: string; authorizationCode: string; user?: { name?: { firstName?: string; lastName?: string }; email?: string }; }) { const appleUserId = await this.verifyAppleIdentity( params.identityToken, params.authorizationCode, params.user?.email, ); let account = await this.prisma.authAccount.findUnique({ where: { provider_providerUserId: { provider: 'apple', providerUserId: appleUserId } }, include: { user: true }, }); if (!account) { const displayName = params.user?.name ? `${params.user.name.lastName || ''}${params.user.name.firstName || ''}` : undefined; account = await this.prisma.authAccount.create({ data: { provider: 'apple', providerUserId: appleUserId, email: params.user?.email, user: { create: { email: params.user?.email, nickname: displayName || undefined, status: 'active', }, }, }, include: { user: true }, }); } const userIdStr = String(account.user.id); const accessToken = await this.jwtService.signAsync({ sub: userIdStr, email: account.user.email, }); const refreshToken = crypto.randomBytes(48).toString('hex'); const refreshTokenHash = crypto .createHash('sha256') .update(refreshToken) .digest('hex'); await this.prisma.refreshToken.create({ data: { userId: account.user.id, tokenHash: refreshTokenHash, expiresAt: new Date(Date.now() + 7 * 86400000), }, }); return { accessToken, refreshToken, expiresIn: 3600 }; } async refresh(refreshToken: string) { const hash = crypto.createHash('sha256').update(refreshToken).digest('hex'); 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 newRefreshToken = crypto.randomBytes(48).toString('hex'); const newHash = crypto .createHash('sha256') .update(newRefreshToken) .digest('hex'); await this.prisma.refreshToken.create({ data: { userId: stored.userId, tokenHash: newHash, expiresAt: new Date(Date.now() + 7 * 86400000), }, }); const accessToken = await this.jwtService.signAsync({ sub: String(stored.user.id), email: stored.user.email, }); return { accessToken, refreshToken: newRefreshToken, expiresIn: 3600, }; } async logout(userId: number) { await this.prisma.refreshToken.updateMany({ where: { userId, revokedAt: null }, data: { revokedAt: new Date() }, }); } private async verifyAppleIdentity( identityToken: string, authorizationCode: string, email?: string | null, ): Promise { if (this.isMockMode()) { return this.verifyMockApple(identityToken, email); } return this.verifyRealApple(identityToken, authorizationCode); } private isMockMode(): boolean { const env = this.configService.get('app.nodeEnv'); const aiProvider = this.configService.get('ai.provider'); return env !== 'production' || aiProvider === 'mock'; } private verifyMockApple(identityToken: string, email?: string | null): string { if (!identityToken || identityToken.trim().length < 4) { throw new UnauthorizedException('identityToken 无效'); } return crypto .createHash('sha256') .update(`apple-mock:${identityToken}:${email || 'no-email'}`) .digest('hex') .slice(0, 64); } private async verifyRealApple( identityToken: string, authorizationCode: string, ): Promise { throw new UnauthorizedException('Apple 登录尚未接入,请先配置 Apple Developer 凭证'); } }