fix and update
This commit is contained in:
@@ -1,13 +1,18 @@
|
|||||||
import type { Request } from 'express';
|
import type { Request } from 'express';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
import { CoreGrpcService } from '../core-grpc.service';
|
import { CoreGrpcService } from '../core-grpc.service';
|
||||||
import { resolveFrontendUrl, resolveOAuthIssuer } from './oauth-issuer';
|
|
||||||
import {
|
import {
|
||||||
|
resolveFrontendUrl,
|
||||||
|
resolveOAuthIssuer,
|
||||||
|
resolveProjectDomainApiUrl,
|
||||||
|
resolveProjectDomainFrontendUrl
|
||||||
|
} from './oauth-issuer';
|
||||||
|
import {
|
||||||
|
isLocalDevBaseUrl,
|
||||||
|
isValidFedcmEndpointUrl,
|
||||||
normalizePublicBaseUrl,
|
normalizePublicBaseUrl,
|
||||||
preferCanonicalBase,
|
pickBestPublicBase,
|
||||||
resolveFedcmWebIdentityOrigin,
|
resolveFedcmWebIdentityOrigin
|
||||||
resolvePublicApiBaseFromRequest,
|
|
||||||
resolvePublicFrontendBaseFromRequest
|
|
||||||
} from './public-url';
|
} from './public-url';
|
||||||
|
|
||||||
export interface FedcmEndpoints {
|
export interface FedcmEndpoints {
|
||||||
@@ -39,17 +44,44 @@ function buildFedcmEndpointUrls(issuer: string, frontendUrl: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveFedcmEndpoints(core: CoreGrpcService, req?: Request): Promise<FedcmEndpoints> {
|
async function resolveCanonicalFedcmIssuer(core: CoreGrpcService): Promise<string> {
|
||||||
const storedIssuer = await resolveOAuthIssuer(core);
|
const stored = await resolveOAuthIssuer(core);
|
||||||
const storedFrontend = await resolveFrontendUrl(core);
|
const fromProject = await resolveProjectDomainApiUrl(core);
|
||||||
// ВАЖНО для FedCM: configUrl должен быть стабильным независимо от того, с какого
|
const issuer = pickBestPublicBase(
|
||||||
// хоста пришёл запрос. well-known запрашивается браузером всегда с eTLD+1
|
[stored, fromProject],
|
||||||
// (например https://idpmvk.lpr/.well-known/web-identity), а discover.json — с
|
stored
|
||||||
// поддомена (https://sso.idpmvk.lpr/idp-api/...). Оба обязаны вернуть ОДИН и тот
|
);
|
||||||
// же configUrl, иначе браузер отвергнет провайдера. Поэтому берём канонический
|
if (isValidFedcmEndpointUrl(issuer)) {
|
||||||
// публичный URL из настроек, а не из текущего хоста запроса.
|
return issuer;
|
||||||
const issuer = preferCanonicalBase(storedIssuer, resolvePublicApiBaseFromRequest(req));
|
}
|
||||||
const frontendUrl = preferCanonicalBase(storedFrontend, resolvePublicFrontendBaseFromRequest(req));
|
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';
|
let projectName = 'MVK ID';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ export async function resolveOAuthIssuer(core: CoreGrpcService, fallback = 'http
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fromDomain = await resolveProjectDomainUrl(core, true);
|
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> {
|
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);
|
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) {
|
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 {
|
export function readRequestHost(req?: Request): string | undefined {
|
||||||
if (!req) {
|
if (!req) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ export default function LoginPage() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const finishLoginRedirect = useCallback(() => {
|
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 =
|
const redirectAfterLogin =
|
||||||
typeof window !== 'undefined' ? new URLSearchParams(window.location.search).get('redirect') : null;
|
typeof window !== 'undefined' ? new URLSearchParams(window.location.search).get('redirect') : null;
|
||||||
if (redirectAfterLogin && redirectAfterLogin.startsWith('/')) {
|
if (redirectAfterLogin && redirectAfterLogin.startsWith('/')) {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
subscribeApiReady,
|
subscribeApiReady,
|
||||||
subscribeApiNotReady,
|
subscribeApiNotReady,
|
||||||
} from '@/lib/api';
|
} from '@/lib/api';
|
||||||
|
import { finalizeFedcmLogin, shouldFinalizeFedcmLogin } from '@/lib/fedcm-login-bridge';
|
||||||
import { useToast } from './toast-provider';
|
import { useToast } from './toast-provider';
|
||||||
import { PinLockModal } from './pin-lock-modal';
|
import { PinLockModal } from './pin-lock-modal';
|
||||||
import { AppBootstrapScreen } from './app-bootstrap-screen';
|
import { AppBootstrapScreen } from './app-bootstrap-screen';
|
||||||
@@ -254,7 +255,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (auth.pinVerified !== false) {
|
if (auth.pinVerified !== false) {
|
||||||
scheduleFedcmSessionSync(auth.accessToken, 8000);
|
if (typeof window !== 'undefined' && shouldFinalizeFedcmLogin(window.location.pathname)) {
|
||||||
|
void finalizeFedcmLogin(auth.accessToken);
|
||||||
|
} else {
|
||||||
|
scheduleFedcmSessionSync(auth.accessToken, 8000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activatePinLock, clearPinLock, showToast]
|
[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