import type { Request } from 'express'; import { firstValueFrom } from 'rxjs'; import { CoreGrpcService } from '../core-grpc.service'; import { resolveFrontendUrl, resolveOAuthIssuer } from './oauth-issuer'; import { normalizePublicBaseUrl, preferCanonicalBase, resolveFedcmWebIdentityOrigin, resolvePublicApiBaseFromRequest, resolvePublicFrontendBaseFromRequest } from './public-url'; export interface FedcmEndpoints { issuer: string; frontendUrl: string; projectName: string; configUrl: string; accountsEndpoint: string; clientMetadataEndpoint: string; idAssertionEndpoint: string; loginUrl: string; signupUrl: string; webIdentityUrl: string; } function buildFedcmEndpointUrls(issuer: string, frontendUrl: string) { const base = normalizePublicBaseUrl(issuer); const front = normalizePublicBaseUrl(frontendUrl); const webIdentityOrigin = resolveFedcmWebIdentityOrigin(base, front); return { configUrl: `${base}/fedcm/config.json`, accountsEndpoint: `${base}/fedcm/accounts`, clientMetadataEndpoint: `${base}/fedcm/client_metadata`, idAssertionEndpoint: `${base}/fedcm/id_assertion`, loginUrl: `${front}/auth/login`, signupUrl: `${front}/auth/register`, webIdentityUrl: `${webIdentityOrigin}/.well-known/web-identity` }; } export async function resolveFedcmEndpoints(core: CoreGrpcService, req?: Request): Promise { const storedIssuer = await resolveOAuthIssuer(core); const storedFrontend = await resolveFrontendUrl(core); // ВАЖНО для FedCM: configUrl должен быть стабильным независимо от того, с какого // хоста пришёл запрос. well-known запрашивается браузером всегда с eTLD+1 // (например https://idpmvk.lpr/.well-known/web-identity), а discover.json — с // поддомена (https://sso.idpmvk.lpr/idp-api/...). Оба обязаны вернуть ОДИН и тот // же configUrl, иначе браузер отвергнет провайдера. Поэтому берём канонический // публичный URL из настроек, а не из текущего хоста запроса. const issuer = preferCanonicalBase(storedIssuer, resolvePublicApiBaseFromRequest(req)); const frontendUrl = preferCanonicalBase(storedFrontend, resolvePublicFrontendBaseFromRequest(req)); let projectName = 'MVK ID'; try { const setting = (await firstValueFrom(core.settings.GetSetting({ key: 'PROJECT_NAME' }))) as { value?: string }; if (setting.value?.trim()) { projectName = setting.value.trim(); } } catch { // fallback } return { issuer, frontendUrl, projectName, ...buildFedcmEndpointUrls(issuer, frontendUrl) }; } export function buildFedcmProviderConfig(endpoints: FedcmEndpoints) { return { accounts_endpoint: endpoints.accountsEndpoint, client_metadata_endpoint: endpoints.clientMetadataEndpoint, id_assertion_endpoint: endpoints.idAssertionEndpoint, login_url: endpoints.loginUrl, branding: { background_color: '#ffffff', color: '#1f2430', icons: [{ url: `${endpoints.frontendUrl}/favicon.ico`, size: 32 }] } }; } export function buildFedcmWebIdentityManifest(endpoints: FedcmEndpoints) { return { provider_urls: [endpoints.configUrl], accounts_endpoint: endpoints.accountsEndpoint, login_url: endpoints.loginUrl }; } export function buildFedcmDiscoverPayload(endpoints: FedcmEndpoints, enabled = true) { return { enabled, apiBase: endpoints.issuer, frontendUrl: endpoints.frontendUrl, projectName: endpoints.projectName, configUrl: endpoints.configUrl, webIdentityUrl: endpoints.webIdentityUrl, accountsEndpoint: endpoints.accountsEndpoint, loginUrl: endpoints.loginUrl }; } async function readOneTapEnabled(core: CoreGrpcService): Promise { try { const setting = (await firstValueFrom(core.settings.GetSetting({ key: 'ONE_TAP_ENABLED' }))) as { value?: string }; const raw = setting.value?.trim().toLowerCase(); if (!raw) return true; return ['true', '1', 'yes'].includes(raw); } catch { return true; } } export { readOneTapEnabled };