117 lines
4.2 KiB
TypeScript
117 lines
4.2 KiB
TypeScript
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
|
||
import { PrismaService } from '../infra/prisma.service';
|
||
import { MediaService } from './media.service';
|
||
import { OAuthCoreService } from './oauth-core.service';
|
||
import { SessionService } from './session.service';
|
||
import { SettingsService } from './settings.service';
|
||
import { assertHumanAccount, HUMAN_USER_WHERE } from './system-account.util';
|
||
|
||
@Injectable()
|
||
export class FedcmService {
|
||
constructor(
|
||
private readonly prisma: PrismaService,
|
||
private readonly session: SessionService,
|
||
private readonly oauthCore: OAuthCoreService,
|
||
private readonly settings: SettingsService,
|
||
private readonly media: MediaService
|
||
) {}
|
||
|
||
async getAccounts(userId: string, sessionId: string) {
|
||
await this.assertUnlockedSession(userId, sessionId);
|
||
|
||
const user = await this.prisma.user.findFirst({
|
||
where: { id: userId, deletedAt: null, status: 'ACTIVE', ...HUMAN_USER_WHERE }
|
||
});
|
||
if (!user) {
|
||
return { accounts: [] };
|
||
}
|
||
assertHumanAccount(user, { asAuth: true });
|
||
|
||
const consents = await this.prisma.oAuthConsent.findMany({
|
||
where: { userId },
|
||
include: { client: { select: { clientId: true, isActive: true } } }
|
||
});
|
||
|
||
const approvedClients = consents
|
||
.filter((item) => item.client.isActive)
|
||
.map((item) => item.client.clientId);
|
||
|
||
const givenName = user.displayName.trim().split(/\s+/)[0] || user.displayName;
|
||
const picture = await this.media.createFedcmAvatarUrl(user.id);
|
||
const email = user.email?.trim() || undefined;
|
||
const tel = user.phone?.trim() || undefined;
|
||
|
||
return {
|
||
accounts: [
|
||
{
|
||
id: user.id,
|
||
name: user.displayName,
|
||
given_name: givenName,
|
||
email,
|
||
picture,
|
||
tel,
|
||
approved_clients: approvedClients
|
||
}
|
||
]
|
||
};
|
||
}
|
||
|
||
async issueIdAssertion(userId: string, sessionId: string, clientId: string, accountId: string) {
|
||
if (accountId !== userId) {
|
||
throw new BadRequestException('account_id не совпадает с активной сессией');
|
||
}
|
||
|
||
await this.assertUnlockedSession(userId, sessionId);
|
||
|
||
const client = await this.prisma.oAuthClient.findUnique({
|
||
where: { clientId },
|
||
include: { scopes: { include: { scope: true } } }
|
||
});
|
||
if (!client?.isActive) {
|
||
throw new BadRequestException('OAuth-приложение не найдено или отключено');
|
||
}
|
||
|
||
const scopeSlugs = client.scopes.map((item) => item.scope.slug);
|
||
const scope = scopeSlugs.length > 0 ? scopeSlugs.join(' ') : 'openid profile email';
|
||
|
||
const consent = await this.oauthCore.checkConsent(userId, clientId, scope);
|
||
if (!consent.granted) {
|
||
await this.oauthCore.grantConsent(userId, clientId, scope);
|
||
}
|
||
|
||
const tokens = await this.oauthCore.issueFedcmTokens(userId, clientId, scopeSlugs.length > 0 ? scopeSlugs : ['openid', 'profile', 'email']);
|
||
return { token: tokens.idToken };
|
||
}
|
||
|
||
async getClientMetadata(clientId: string) {
|
||
const client = await this.prisma.oAuthClient.findUnique({ where: { clientId } });
|
||
if (!client?.isActive) {
|
||
throw new BadRequestException('OAuth-приложение не найдено или отключено');
|
||
}
|
||
|
||
const frontendUrl = (await this.settings.getValue('PUBLIC_FRONTEND_URL', 'http://localhost:3002')).replace(/\/+$/, '');
|
||
const privacyFallback = `${frontendUrl}/data`;
|
||
const privacy = (await this.settings.getValue('FEDCM_PRIVACY_POLICY_URL', '')).trim();
|
||
const terms = (await this.settings.getValue('FEDCM_TERMS_URL', '')).trim();
|
||
|
||
return {
|
||
privacy_policy_url: privacy || privacyFallback,
|
||
terms_of_service_url: terms || privacyFallback
|
||
};
|
||
}
|
||
|
||
async isOneTapEnabled() {
|
||
return this.settings.getBoolean('ONE_TAP_ENABLED', true);
|
||
}
|
||
|
||
private async assertUnlockedSession(userId: string, sessionId: string) {
|
||
const state = await this.session.resolveSessionState(sessionId);
|
||
if (!state || state.userId !== userId) {
|
||
throw new UnauthorizedException('Сессия недействительна или истекла');
|
||
}
|
||
if (state.requiresPin) {
|
||
throw new UnauthorizedException('Требуется подтверждение PIN-кода');
|
||
}
|
||
}
|
||
}
|