119 lines
4.2 KiB
TypeScript
119 lines
4.2 KiB
TypeScript
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<FedcmEndpoints> {
|
||
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<boolean> {
|
||
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 };
|