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

@@ -14,6 +14,7 @@ import { ProfileService } from './profile.service';
import { DocumentsService } from './documents.service';
import { AddressesService } from './addresses.service';
import { OAuthCoreService } from './oauth-core.service';
import { FedcmService } from './fedcm.service';
import { OtpService } from './otp.service';
import { AdvancedAuthService } from './advanced-auth.service';
import { FamilyService } from './family.service';
@@ -42,6 +43,7 @@ export class AuthGrpcController {
private readonly documents: DocumentsService,
private readonly addresses: AddressesService,
private readonly oauthCore: OAuthCoreService,
private readonly fedcm: FedcmService,
private readonly otp: OtpService,
private readonly advancedAuth: AdvancedAuthService,
private readonly family: FamilyService,
@@ -831,6 +833,21 @@ export class AuthGrpcController {
return this.oauthCore.getClientPublicInfo(command.clientId);
}
@GrpcMethod('FedcmService', 'GetAccounts')
fedcmGetAccounts(command: { userId: string; sessionId: string }) {
return this.fedcm.getAccounts(command.userId, command.sessionId);
}
@GrpcMethod('FedcmService', 'IssueIdAssertion')
fedcmIssueIdAssertion(command: { userId: string; sessionId: string; clientId: string; accountId: string }) {
return this.fedcm.issueIdAssertion(command.userId, command.sessionId, command.clientId, command.accountId);
}
@GrpcMethod('FedcmService', 'GetClientMetadata')
fedcmGetClientMetadata(command: { clientId: string }) {
return this.fedcm.getClientMetadata(command.clientId);
}
@GrpcMethod('OtpService', 'SendOtp')
sendOtp(command: { target: string; channel: string; purpose: string; userId?: string }) {
return this.otp.send(command);

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-кода');
}
}
}

View File

@@ -316,6 +316,10 @@ export class OAuthCoreService {
return { clientId: client.clientId, name: client.name };
}
async issueFedcmTokens(userId: string, clientId: string, scopes: string[]) {
return this.issueOAuthTokens(userId, clientId, scopes);
}
private parseScopeSlugs(scope: string) {
return [...new Set(scope.split(/\s+/).map((item) => item.trim()).filter(Boolean))];
}