api-server/src/modules/auth/auth.service.ts

161 lines
4.6 KiB
TypeScript
Raw Normal View History

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<string> {
if (this.isMockMode()) {
return this.verifyMockApple(identityToken, email);
}
return this.verifyRealApple(identityToken, authorizationCode);
}
private isMockMode(): boolean {
const env = this.configService.get<string>('app.nodeEnv');
const aiProvider = this.configService.get<string>('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<string> {
throw new UnauthorizedException('Apple 登录尚未接入,请先配置 Apple Developer 凭证');
}
}