import type { Request } from 'express'; import { firstValueFrom } from 'rxjs'; import { CoreGrpcService } from '../core-grpc.service'; import { resolveFrontendUrl, resolveOAuthIssuer, resolveProjectDomainApiUrl, resolveProjectDomainFrontendUrl } from './oauth-issuer'; import { isLocalDevBaseUrl, isValidFedcmEndpointUrl, normalizePublicBaseUrl, pickBestPublicBase, resolveFedcmWebIdentityOrigin } 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` }; } async function resolveCanonicalFedcmIssuer(core: CoreGrpcService): Promise { const stored = await resolveOAuthIssuer(core); const fromProject = await resolveProjectDomainApiUrl(core); const issuer = pickBestPublicBase( [stored, fromProject], stored ); if (isValidFedcmEndpointUrl(issuer)) { return issuer; } if (fromProject && isValidFedcmEndpointUrl(fromProject)) { return fromProject; } return issuer; } async function resolveCanonicalFedcmFrontend(core: CoreGrpcService): Promise { const stored = await resolveFrontendUrl(core); const fromProject = await resolveProjectDomainFrontendUrl(core); const frontend = pickBestPublicBase( [stored, fromProject], stored ); if (isValidFedcmEndpointUrl(frontend) && !isLocalDevBaseUrl(frontend)) { return frontend; } if (fromProject && isValidFedcmEndpointUrl(fromProject)) { return fromProject; } return frontend; } export async function resolveFedcmEndpoints(core: CoreGrpcService, _req?: Request): Promise { // FedCM: web-identity (idpmvk.lpr) и config.json (api.idpmvk.lpr) ОБЯЗАНЫ // возвращать одинаковые login_url / accounts_endpoint / configUrl. // Хост запроса разный — никогда не выводим URL из req.headers.host. const issuer = await resolveCanonicalFedcmIssuer(core); const frontendUrl = await resolveCanonicalFedcmFrontend(core); 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 };