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

203 lines
5.2 KiB
TypeScript
Raw Normal View History

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,
};
}
}