Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 10m5s
161 lines
4.6 KiB
TypeScript
161 lines
4.6 KiB
TypeScript
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 凭证');
|
|
}
|
|
}
|