diff --git a/apps/api-gateway/src/controllers/fedcm.controller.ts b/apps/api-gateway/src/controllers/fedcm.controller.ts index 504aad9..139d055 100644 --- a/apps/api-gateway/src/controllers/fedcm.controller.ts +++ b/apps/api-gateway/src/controllers/fedcm.controller.ts @@ -40,6 +40,7 @@ type FedcmAccountsResponse = { given_name?: string; email?: string; picture?: string; + tel?: string; approved_clients?: string[]; }>; }; diff --git a/apps/api-gateway/src/lib/fedcm-config.ts b/apps/api-gateway/src/lib/fedcm-config.ts index 225fb16..0761f58 100644 --- a/apps/api-gateway/src/lib/fedcm-config.ts +++ b/apps/api-gateway/src/lib/fedcm-config.ts @@ -105,15 +105,20 @@ export async function resolveFedcmEndpoints(core: CoreGrpcService, _req?: Reques export function buildFedcmProviderConfig(endpoints: FedcmEndpoints) { const base = normalizePublicBaseUrl(endpoints.issuer); + const front = normalizePublicBaseUrl(endpoints.frontendUrl); return { accounts_endpoint: `${base}/fedcm/accounts`, client_metadata_endpoint: `${base}/fedcm/client_metadata`, id_assertion_endpoint: `${base}/fedcm/id_assertion`, login_url: endpoints.loginUrl, branding: { + name: endpoints.projectName, background_color: '#ffffff', color: '#1f2430', - icons: [{ url: `${base}/favicon.ico`, size: 32 }] + icons: [ + { url: `${front}/icon.svg`, size: 40 }, + { url: `${base}/favicon.ico`, size: 32 } + ] } }; } @@ -135,7 +140,9 @@ export function buildFedcmDiscoverPayload(endpoints: FedcmEndpoints, enabled = t configUrl: endpoints.configUrl, webIdentityUrl: endpoints.webIdentityUrl, accountsEndpoint: endpoints.accountsEndpoint, - loginUrl: endpoints.loginUrl + loginUrl: endpoints.loginUrl, + // Поля для navigator.credentials.get({ identity: { fields: [...] } }) на стороне RP (Chrome 132+). + suggestedFields: ['name', 'email', 'picture', 'tel'] }; } diff --git a/apps/docs/lib/one-tap-examples.ts b/apps/docs/lib/one-tap-examples.ts index 1a4812f..302f615 100644 --- a/apps/docs/lib/one-tap-examples.ts +++ b/apps/docs/lib/one-tap-examples.ts @@ -71,7 +71,8 @@ async function loginWithFedCM() { identity: { providers: [{ configURL: '${fedcmConfigUrl}', - clientId: '${clientIdPlaceholder}' + clientId: '${clientIdPlaceholder}', + fields: ['name', 'email', 'picture', 'tel'] }] }, mediation: 'optional' diff --git a/apps/frontend/components/id/oauth-client-one-tap-section.tsx b/apps/frontend/components/id/oauth-client-one-tap-section.tsx index 4c93861..8eef2c8 100644 --- a/apps/frontend/components/id/oauth-client-one-tap-section.tsx +++ b/apps/frontend/components/id/oauth-client-one-tap-section.tsx @@ -104,6 +104,10 @@ export function OAuthClientOneTapSection({ configURL для FedCM: {endpoints.fedcmConfigUrl} (не{' '} {frontendBase}/idp-api/fedcm/config.json). +
  • + Для disclosure (имя, email, аватар, телефон) RP должен передавать{' '} + {`fields: ['name', 'email', 'picture', 'tel']`} в вызове FedCM (Chrome 132+). +
  • При ошибке NetworkError проверьте, что PUBLIC_API_URL = {endpoints.issuer}.
  • diff --git a/apps/frontend/lib/one-tap-examples.ts b/apps/frontend/lib/one-tap-examples.ts index d4d7c92..c732c1e 100644 --- a/apps/frontend/lib/one-tap-examples.ts +++ b/apps/frontend/lib/one-tap-examples.ts @@ -95,7 +95,9 @@ export function buildOneTapExamples(urls: OneTapUrls, clientId: string, redirect identity: { providers: [{ configURL: '${fedcmConfigUrl}', - clientId: '${clientId}' + clientId: '${clientId}', + // Chrome 132+: disclosure в диалоге FedCM (имя, email, аватар, телефон) + fields: ['name', 'email', 'picture', 'tel'] }] }, mediation: 'optional' diff --git a/apps/frontend/public/sso-widget.js b/apps/frontend/public/sso-widget.js index 855c97d..faeaf54 100644 --- a/apps/frontend/public/sso-widget.js +++ b/apps/frontend/public/sso-widget.js @@ -95,7 +95,8 @@ { configURL: configUrl, configUrl: configUrl, - clientId: clientId + clientId: clientId, + fields: ['name', 'email', 'picture', 'tel'] } ] }, diff --git a/apps/sso-core/src/domain/fedcm.service.ts b/apps/sso-core/src/domain/fedcm.service.ts index ab5a1e6..363d3f1 100644 --- a/apps/sso-core/src/domain/fedcm.service.ts +++ b/apps/sso-core/src/domain/fedcm.service.ts @@ -1,5 +1,6 @@ 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'; @@ -11,7 +12,8 @@ export class FedcmService { private readonly prisma: PrismaService, private readonly session: SessionService, private readonly oauthCore: OAuthCoreService, - private readonly settings: SettingsService + private readonly settings: SettingsService, + private readonly media: MediaService ) {} async getAccounts(userId: string, sessionId: string) { @@ -35,6 +37,9 @@ export class FedcmService { .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: [ @@ -42,8 +47,9 @@ export class FedcmService { id: user.id, name: user.displayName, given_name: givenName, - email: user.email ?? undefined, - picture: user.avatarUrl ?? undefined, + email, + picture, + tel, approved_clients: approvedClients } ] diff --git a/apps/sso-core/src/domain/media.service.ts b/apps/sso-core/src/domain/media.service.ts index 826fea6..6f91708 100644 --- a/apps/sso-core/src/domain/media.service.ts +++ b/apps/sso-core/src/domain/media.service.ts @@ -68,6 +68,25 @@ export class MediaService { return { userId, hasAvatar: true }; } + async createFedcmAvatarUrl(userId: string): Promise { + const user = await this.prisma.user.findUnique({ where: { id: userId } }); + if (!user) { + return undefined; + } + + const legacyUrl = user.avatarUrl?.trim(); + if (legacyUrl) { + return legacyUrl; + } + + if (!user.avatarStorageKey) { + return undefined; + } + + const { accessUrl } = await this.createStreamAccessUrl(user.avatarStorageKey, userId); + return accessUrl; + } + async getAvatarAccessUrl(requesterId: string, targetUserId: string) { const user = await this.prisma.user.findUnique({ where: { id: targetUserId } }); if (!user?.avatarStorageKey) { diff --git a/shared/proto/identity.proto b/shared/proto/identity.proto index af21d59..e71c6a3 100644 --- a/shared/proto/identity.proto +++ b/shared/proto/identity.proto @@ -125,6 +125,7 @@ message FedcmAccount { optional string email = 4; optional string picture = 5; repeated string approved_clients = 6; + optional string tel = 7; } message FedcmAccountsResponse {