fix and update
This commit is contained in:
@@ -1,13 +1,18 @@
|
||||
import type { Request } from 'express';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { CoreGrpcService } from '../core-grpc.service';
|
||||
import { resolveFrontendUrl, resolveOAuthIssuer } from './oauth-issuer';
|
||||
import {
|
||||
resolveFrontendUrl,
|
||||
resolveOAuthIssuer,
|
||||
resolveProjectDomainApiUrl,
|
||||
resolveProjectDomainFrontendUrl
|
||||
} from './oauth-issuer';
|
||||
import {
|
||||
isLocalDevBaseUrl,
|
||||
isValidFedcmEndpointUrl,
|
||||
normalizePublicBaseUrl,
|
||||
preferCanonicalBase,
|
||||
resolveFedcmWebIdentityOrigin,
|
||||
resolvePublicApiBaseFromRequest,
|
||||
resolvePublicFrontendBaseFromRequest
|
||||
pickBestPublicBase,
|
||||
resolveFedcmWebIdentityOrigin
|
||||
} from './public-url';
|
||||
|
||||
export interface FedcmEndpoints {
|
||||
@@ -39,17 +44,44 @@ function buildFedcmEndpointUrls(issuer: string, frontendUrl: string) {
|
||||
};
|
||||
}
|
||||
|
||||
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));
|
||||
async function resolveCanonicalFedcmIssuer(core: CoreGrpcService): Promise<string> {
|
||||
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<string> {
|
||||
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<FedcmEndpoints> {
|
||||
// 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 {
|
||||
|
||||
@@ -51,7 +51,8 @@ export async function resolveOAuthIssuer(core: CoreGrpcService, fallback = 'http
|
||||
}
|
||||
|
||||
const fromDomain = await resolveProjectDomainUrl(core, true);
|
||||
return pickBestPublicBase([fromDb, envIssuer, fromDomain], fallback);
|
||||
// env первым: в Docker .env перекрывает seed localhost в БД после первого деплоя.
|
||||
return pickBestPublicBase([envIssuer, fromDb, fromDomain], fallback);
|
||||
}
|
||||
|
||||
export async function resolveFrontendUrl(core: CoreGrpcService, fallback = 'http://localhost:3002'): Promise<string> {
|
||||
@@ -65,7 +66,15 @@ export async function resolveFrontendUrl(core: CoreGrpcService, fallback = 'http
|
||||
}
|
||||
|
||||
const fromDomain = await resolveProjectDomainUrl(core, false);
|
||||
return pickBestPublicBase([fromDb, envFrontend, fromDomain], fallback);
|
||||
return pickBestPublicBase([envFrontend, fromDb, fromDomain], fallback);
|
||||
}
|
||||
|
||||
export async function resolveProjectDomainFrontendUrl(core: CoreGrpcService): Promise<string | undefined> {
|
||||
return resolveProjectDomainUrl(core, false);
|
||||
}
|
||||
|
||||
export async function resolveProjectDomainApiUrl(core: CoreGrpcService): Promise<string | undefined> {
|
||||
return resolveProjectDomainUrl(core, true);
|
||||
}
|
||||
|
||||
export function buildOpenIdConfiguration(issuer: string) {
|
||||
|
||||
@@ -47,6 +47,25 @@ export function isBrowserReachableBaseUrl(url: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Абсолютный HTTPS (или localhost HTTP) URL для FedCM login_url / configUrl. */
|
||||
export function isValidFedcmEndpointUrl(url: string) {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||
return false;
|
||||
}
|
||||
if (isInternalHostname(parsed.hostname)) {
|
||||
return false;
|
||||
}
|
||||
if (isLocalDevBaseUrl(url)) {
|
||||
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
||||
}
|
||||
return parsed.protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function readRequestHost(req?: Request): string | undefined {
|
||||
if (!req) {
|
||||
return undefined;
|
||||
|
||||
@@ -83,6 +83,12 @@ export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const finishLoginRedirect = useCallback(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const fedcmLoginApi = (navigator as Navigator & { login?: { setStatus?: (status: string) => void } }).login;
|
||||
if (window.opener && typeof fedcmLoginApi?.setStatus === 'function') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const redirectAfterLogin =
|
||||
typeof window !== 'undefined' ? new URLSearchParams(window.location.search).get('redirect') : null;
|
||||
if (redirectAfterLogin && redirectAfterLogin.startsWith('/')) {
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
subscribeApiReady,
|
||||
subscribeApiNotReady,
|
||||
} from '@/lib/api';
|
||||
import { finalizeFedcmLogin, shouldFinalizeFedcmLogin } from '@/lib/fedcm-login-bridge';
|
||||
import { useToast } from './toast-provider';
|
||||
import { PinLockModal } from './pin-lock-modal';
|
||||
import { AppBootstrapScreen } from './app-bootstrap-screen';
|
||||
@@ -254,8 +255,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
});
|
||||
|
||||
if (auth.pinVerified !== false) {
|
||||
if (typeof window !== 'undefined' && shouldFinalizeFedcmLogin(window.location.pathname)) {
|
||||
void finalizeFedcmLogin(auth.accessToken);
|
||||
} else {
|
||||
scheduleFedcmSessionSync(auth.accessToken, 8000);
|
||||
}
|
||||
}
|
||||
},
|
||||
[activatePinLock, clearPinLock, showToast]
|
||||
);
|
||||
|
||||
47
apps/frontend/lib/fedcm-login-bridge.ts
Normal file
47
apps/frontend/lib/fedcm-login-bridge.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { syncFedcmSession } from '@/lib/api';
|
||||
|
||||
type FedcmLoginNavigator = Navigator & {
|
||||
login?: {
|
||||
setStatus: (status: 'logged-in' | 'logged-out') => void;
|
||||
};
|
||||
};
|
||||
|
||||
type FedcmIdentityProviderGlobal = {
|
||||
close?: () => void;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
IdentityProvider?: FedcmIdentityProviderGlobal;
|
||||
}
|
||||
}
|
||||
|
||||
/** Завершение входа в popup FedCM: cookie + Set-Login для браузера. */
|
||||
export async function finalizeFedcmLogin(accessToken: string) {
|
||||
if (typeof window === 'undefined' || !accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
await syncFedcmSession(accessToken);
|
||||
|
||||
const loginApi = (navigator as FedcmLoginNavigator).login;
|
||||
if (loginApi?.setStatus) {
|
||||
try {
|
||||
loginApi.setStatus('logged-in');
|
||||
} catch {
|
||||
// FedCM API может быть недоступен вне popup-контекста
|
||||
}
|
||||
}
|
||||
|
||||
if (window.IdentityProvider?.close) {
|
||||
try {
|
||||
window.IdentityProvider.close();
|
||||
} catch {
|
||||
// закрытие необязательно — браузер сам закроет popup после setStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldFinalizeFedcmLogin(pathname: string) {
|
||||
return pathname.startsWith('/auth/login');
|
||||
}
|
||||
Reference in New Issue
Block a user