fix and update

This commit is contained in:
lendry
2026-07-01 12:14:50 +03:00
parent dce16af5a5
commit 6929fb41fc
7 changed files with 152 additions and 64 deletions

View File

@@ -77,7 +77,7 @@ export function buildFedcmProviderConfig(endpoints: FedcmEndpoints) {
login_url: endpoints.loginUrl,
branding: {
background_color: '#ffffff',
color: '#3390ec',
color: '#1f2430',
icons: [{ url: `${endpoints.frontendUrl}/favicon.ico`, size: 32 }]
}
};

View File

@@ -1,6 +1,6 @@
import { firstValueFrom } from 'rxjs';
import { CoreGrpcService } from '../core-grpc.service';
import { normalizePublicBaseUrl, isInternalHostname } from './public-url';
import { normalizePublicBaseUrl, pickBestPublicBase, isInternalHostname } from './public-url';
function normalizeBaseUrl(url: string) {
return normalizePublicBaseUrl(url);
@@ -23,68 +23,49 @@ function sanitizeStoredPublicUrl(url: string) {
return normalized;
}
export async function resolveOAuthIssuer(core: CoreGrpcService, fallback = 'http://localhost:3000'): Promise<string> {
const envIssuer = process.env.PUBLIC_API_URL?.trim();
try {
const response = (await firstValueFrom(core.settings.GetSetting({ key: 'PUBLIC_API_URL' }))) as { value?: string };
const fromDb = sanitizeStoredPublicUrl(response.value ?? '');
if (fromDb) {
return fromDb;
}
} catch {
// fallback ниже
}
if (envIssuer) {
return normalizeBaseUrl(envIssuer);
}
async function resolveProjectDomainUrl(core: CoreGrpcService, withIdpApiPath = false): Promise<string | undefined> {
try {
const response = (await firstValueFrom(core.settings.GetSetting({ key: 'PROJECT_DOMAIN' }))) as { value?: string };
const domain = response.value?.trim();
if (domain) {
if (domain.startsWith('http://') || domain.startsWith('https://')) {
return appendIdpApiPath(domain);
}
return appendIdpApiPath(`https://${domain.replace(/^\/+/, '')}`);
if (!domain) {
return undefined;
}
if (domain.startsWith('http://') || domain.startsWith('https://')) {
return withIdpApiPath ? appendIdpApiPath(domain) : normalizeBaseUrl(domain);
}
const base = `https://${domain.replace(/^\/+/, '')}`;
return withIdpApiPath ? appendIdpApiPath(base) : base;
} catch {
return undefined;
}
}
export async function resolveOAuthIssuer(core: CoreGrpcService, fallback = 'http://localhost:3000'): Promise<string> {
const envIssuer = sanitizeStoredPublicUrl(process.env.PUBLIC_API_URL?.trim() ?? '');
let fromDb: string | undefined;
try {
const response = (await firstValueFrom(core.settings.GetSetting({ key: 'PUBLIC_API_URL' }))) as { value?: string };
fromDb = sanitizeStoredPublicUrl(response.value ?? '');
} catch {
// fallback ниже
}
return normalizeBaseUrl(fallback);
const fromDomain = await resolveProjectDomainUrl(core, true);
return pickBestPublicBase([fromDb, envIssuer, fromDomain], fallback);
}
export async function resolveFrontendUrl(core: CoreGrpcService, fallback = 'http://localhost:3002'): Promise<string> {
const envFrontend = process.env.PUBLIC_FRONTEND_URL?.trim();
if (envFrontend) {
return normalizeBaseUrl(envFrontend);
}
const envFrontend = sanitizeStoredPublicUrl(process.env.PUBLIC_FRONTEND_URL?.trim() ?? '');
let fromDb: string | undefined;
try {
const response = (await firstValueFrom(core.settings.GetSetting({ key: 'PUBLIC_FRONTEND_URL' }))) as { value?: string };
const fromDb = sanitizeStoredPublicUrl(response.value ?? '');
if (fromDb) {
return fromDb;
}
fromDb = sanitizeStoredPublicUrl(response.value ?? '');
} catch {
// fallback ниже
}
try {
const response = (await firstValueFrom(core.settings.GetSetting({ key: 'PROJECT_DOMAIN' }))) as { value?: string };
const domain = response.value?.trim();
if (domain) {
if (domain.startsWith('http://') || domain.startsWith('https://')) {
return normalizeBaseUrl(domain);
}
return `https://${domain.replace(/^\/+/, '')}`;
}
} catch {
// fallback ниже
}
return normalizeBaseUrl(fallback);
const fromDomain = await resolveProjectDomainUrl(core, false);
return pickBestPublicBase([fromDb, envFrontend, fromDomain], fallback);
}
export function buildOpenIdConfiguration(issuer: string) {

View File

@@ -103,14 +103,47 @@ export function resolvePublicFrontendBaseFromRequest(req?: Request): string | nu
* из настроек (PUBLIC_API_URL / PUBLIC_FRONTEND_URL), а на хост запроса
* переключаемся только если в настройках указан внутренний Docker-хост.
*/
export function isLocalDevHostname(hostname: string) {
const host = hostname.trim().toLowerCase();
return host === 'localhost' || host === '127.0.0.1' || host === '[::1]';
}
export function isLocalDevBaseUrl(url: string) {
try {
return isLocalDevHostname(new URL(url).hostname);
} catch {
return false;
}
}
/** Выбирает публичный URL: приоритет у реального домена, не localhost из .env/seed. */
export function pickBestPublicBase(candidates: Array<string | null | undefined>, fallback: string) {
const normalized = candidates
.map((candidate) => candidate?.trim())
.filter((candidate): candidate is string => Boolean(candidate))
.map((candidate) => normalizePublicBaseUrl(candidate));
const productionReachable = normalized.find(
(candidate) => isBrowserReachableBaseUrl(candidate) && !isLocalDevBaseUrl(candidate)
);
if (productionReachable) {
return productionReachable;
}
const reachable = normalized.find((candidate) => isBrowserReachableBaseUrl(candidate));
if (reachable) {
return reachable;
}
if (normalized[0]) {
return normalized[0];
}
return normalizePublicBaseUrl(fallback);
}
export function preferCanonicalBase(stored: string, fromRequest: string | null) {
if (stored && isBrowserReachableBaseUrl(stored)) {
return normalizePublicBaseUrl(stored);
}
if (fromRequest && isBrowserReachableBaseUrl(fromRequest)) {
return normalizePublicBaseUrl(fromRequest);
}
return normalizePublicBaseUrl(stored);
return pickBestPublicBase([stored, fromRequest], stored || fromRequest || '');
}
export function preferBrowserReachableBase(stored: string, fromRequest: string | null) {

View File

@@ -40,7 +40,7 @@ export function deliverOAuthPopupResult(
const delivered = postOneTapResult(popupContext.popupOrigin, {
code: parsed.code ?? undefined,
state: parsed.state ?? undefined,
token: parsed.code ?? undefined,
grantType: parsed.code ? 'authorization_code' : undefined,
error: parsed.error ?? undefined,
errorDescription: parsed.errorDescription ?? undefined
});

View File

@@ -158,17 +158,45 @@
return requestFedCM(resolved.value);
})
.catch(function (error) {
if (global.console && typeof global.console.warn === 'function') {
global.console.warn(
'[MVK ID] FedCM недоступен:',
if (global.console && typeof global.console.debug === 'function') {
global.console.debug(
'[MVK ID] FedCM недоступен (будет использован popup):',
error,
'Проверьте публичный URL IdP в data-idp-url и доступность /.well-known/web-identity на домене IdP (не /idp-api/.well-known).'
'Для One Tap без popup: примите SSL для корневого домена IdP (например idpmvk.lpr) и проверьте /.well-known/web-identity.'
);
}
return null;
});
}
function exchangeAuthorizationCode(idpApiBase, clientId, clientSecret, code, redirectUri) {
if (!code || !clientId || !clientSecret) {
return Promise.reject(new Error('Для обмена code нужны clientId, clientSecret и authorization code'));
}
var body = new URLSearchParams();
body.set('grant_type', 'authorization_code');
body.set('code', code);
body.set('client_id', clientId);
body.set('client_secret', clientSecret);
body.set('redirect_uri', redirectUri);
return global.fetch(trimSlash(idpApiBase) + '/oauth/token', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: body.toString()
}).then(function (response) {
return response.json().then(function (payload) {
if (!response.ok) {
var message = payload && (payload.message || payload.error_description || payload.error);
throw new Error(message || 'Не удалось обменять authorization code');
}
return payload;
});
});
}
var SIZE_PRESETS = {
s: { height: 32, font: 13, padX: 12, gap: 8, badge: 20, radius: 16 },
m: { height: 40, font: 14, padX: 16, gap: 10, badge: 24, radius: 20 },
@@ -406,6 +434,8 @@
dispatchSuccess(
{
token: result.token,
idToken: result.token,
grantType: 'id_token',
method: 'fedcm'
},
script
@@ -420,11 +450,14 @@
openPopup(popupUrl, frontendBase, function (payload) {
dispatchSuccess(
{
token: payload.token,
accessToken: payload.accessToken,
idToken: payload.idToken,
refreshToken: payload.refreshToken,
code: payload.code,
state: payload.state,
grantType: payload.grantType || (payload.code ? 'authorization_code' : undefined),
idToken: payload.idToken,
accessToken: payload.accessToken,
refreshToken: payload.refreshToken,
tokenEndpoint: trimSlash(idpApiBase) + '/oauth/token',
redirectUri: redirectUri,
method: 'popup'
},
script
@@ -439,6 +472,7 @@
global.LendryIdOneTap = {
init: initOneTap,
loginWithFedCM: loginWithFedCM,
exchangeAuthorizationCode: exchangeAuthorizationCode,
MESSAGE_TYPE: MESSAGE_TYPE
};