Files
IdP/apps/sso-core/src/domain/fedcm.service.ts
2026-07-07 08:06:55 +03:00

122 lines
4.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.assertExistingSession(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 assertExistingSession(userId: string, sessionId: string) {
const state = await this.session.resolveSessionState(sessionId);
if (!state || state.userId !== userId) {
throw new UnauthorizedException('Сессия недействительна или истекла');
}
}
private async assertUnlockedSession(userId: string, sessionId: string) {
await this.assertExistingSession(userId, sessionId);
const state = await this.session.resolveSessionState(sessionId);
if (state?.requiresPin) {
throw new UnauthorizedException('Требуется подтверждение PIN-кода');
}
}
}