fix and update

This commit is contained in:
lendry
2026-06-29 12:17:25 +03:00
parent 923a028cdd
commit 75ccbe5fc4
39 changed files with 1619 additions and 63 deletions

View File

@@ -0,0 +1,99 @@
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
import { PrismaService } from '../infra/prisma.service';
import { OAuthCoreService } from './oauth-core.service';
import { SessionService } from './session.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
) {}
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;
return {
accounts: [
{
id: user.id,
name: user.displayName,
given_name: givenName,
email: user.email ?? undefined,
picture: user.avatarUrl ?? undefined,
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-приложение не найдено или отключено');
}
return {
privacy_policy_url: 'https://id.lendry.ru/data',
terms_of_service_url: 'https://id.lendry.ru/data'
};
}
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-кода');
}
}
}