fix and update
This commit is contained in:
@@ -33,6 +33,7 @@ PUBLIC_API_URL=http://localhost:3002/idp-api
|
|||||||
PUBLIC_FRONTEND_URL=http://localhost:3002
|
PUBLIC_FRONTEND_URL=http://localhost:3002
|
||||||
PUBLIC_DOCS_URL=http://localhost:3003
|
PUBLIC_DOCS_URL=http://localhost:3003
|
||||||
PUBLIC_WS_URL=ws://localhost:8085/ws
|
PUBLIC_WS_URL=ws://localhost:8085/ws
|
||||||
|
# Docker: http://api-gateway:3000 | локальный npm run dev: http://localhost:3000
|
||||||
INTERNAL_API_URL=http://api-gateway:3000
|
INTERNAL_API_URL=http://api-gateway:3000
|
||||||
INTERNAL_WS_URL=http://media-ws:8085
|
INTERNAL_WS_URL=http://media-ws:8085
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get, ServiceUnavailableException } from '@nestjs/common';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import { CoreGrpcService } from '../core-grpc.service';
|
||||||
|
|
||||||
@Controller('health')
|
@Controller('health')
|
||||||
export class HealthController {
|
export class HealthController {
|
||||||
|
constructor(private readonly core: CoreGrpcService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
check() {
|
async check() {
|
||||||
return { status: 'ok', service: 'api-gateway' };
|
try {
|
||||||
|
await firstValueFrom(this.core.settings.GetSetting({ key: 'PROJECT_NAME' }));
|
||||||
|
return { status: 'ok', service: 'api-gateway', grpc: 'ok' };
|
||||||
|
} catch {
|
||||||
|
throw new ServiceUnavailableException({
|
||||||
|
status: 'error',
|
||||||
|
service: 'api-gateway',
|
||||||
|
grpc: 'unavailable',
|
||||||
|
message: 'Не удалось связаться с sso-core по gRPC'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,15 @@ 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 { resolveFrontendUrl, resolveOAuthIssuer } from './oauth-issuer';
|
||||||
|
import {
|
||||||
|
hostsMatch,
|
||||||
|
normalizePublicBaseUrl,
|
||||||
|
preferBrowserReachableBase,
|
||||||
|
readRequestHost,
|
||||||
|
readRequestProto,
|
||||||
|
resolvePublicApiBaseFromRequest,
|
||||||
|
resolvePublicFrontendBaseFromRequest
|
||||||
|
} from './public-url';
|
||||||
|
|
||||||
export interface FedcmEndpoints {
|
export interface FedcmEndpoints {
|
||||||
issuer: string;
|
issuer: string;
|
||||||
@@ -16,94 +25,9 @@ export interface FedcmEndpoints {
|
|||||||
webIdentityUrl: string;
|
webIdentityUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeBaseUrl(url: string) {
|
|
||||||
return url.trim().replace(/\/+$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function readRequestHost(req?: Request): string | undefined {
|
|
||||||
if (!req) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const forwarded = req.headers['x-forwarded-host'];
|
|
||||||
const host = (Array.isArray(forwarded) ? forwarded[0] : forwarded) ?? req.headers.host;
|
|
||||||
return host?.split(',')[0]?.trim() || undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readRequestProto(req?: Request): string {
|
|
||||||
if (!req) {
|
|
||||||
return 'https';
|
|
||||||
}
|
|
||||||
|
|
||||||
const forwarded = req.headers['x-forwarded-proto'];
|
|
||||||
const proto = (Array.isArray(forwarded) ? forwarded[0] : forwarded) ?? req.protocol;
|
|
||||||
return proto?.split(',')[0]?.trim() || 'https';
|
|
||||||
}
|
|
||||||
|
|
||||||
function hostsMatch(a: string, b: string) {
|
|
||||||
if (a === b) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return a === 'localhost' && b === 'localhost';
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveSameOriginFrontend(storedFrontend: string, req?: Request): string {
|
|
||||||
const host = readRequestHost(req);
|
|
||||||
if (!host) {
|
|
||||||
return storedFrontend;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestOrigin = normalizeBaseUrl(`${readRequestProto(req)}://${host}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const frontendUrl = new URL(storedFrontend);
|
|
||||||
const requestUrl = new URL(requestOrigin);
|
|
||||||
|
|
||||||
if (hostsMatch(frontendUrl.hostname, requestUrl.hostname)) {
|
|
||||||
return requestOrigin;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return storedFrontend;
|
|
||||||
}
|
|
||||||
|
|
||||||
return storedFrontend;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveSameOriginIssuer(storedIssuer: string, storedFrontend: string, req?: Request): string {
|
|
||||||
const host = readRequestHost(req);
|
|
||||||
if (!host) {
|
|
||||||
return storedIssuer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestOrigin = normalizeBaseUrl(`${readRequestProto(req)}://${host}`);
|
|
||||||
const sameOriginApi = `${requestOrigin}/idp-api`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const issuerUrl = new URL(storedIssuer);
|
|
||||||
const frontendUrl = new URL(storedFrontend);
|
|
||||||
const requestUrl = new URL(requestOrigin);
|
|
||||||
|
|
||||||
const hostMatches =
|
|
||||||
hostsMatch(issuerUrl.hostname, requestUrl.hostname) || hostsMatch(frontendUrl.hostname, requestUrl.hostname);
|
|
||||||
|
|
||||||
if (!hostMatches) {
|
|
||||||
return storedIssuer;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalizeBaseUrl(storedIssuer) !== sameOriginApi) {
|
|
||||||
return sameOriginApi;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return storedIssuer;
|
|
||||||
}
|
|
||||||
|
|
||||||
return storedIssuer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildFedcmEndpointUrls(issuer: string, frontendUrl: string) {
|
function buildFedcmEndpointUrls(issuer: string, frontendUrl: string) {
|
||||||
const base = normalizeBaseUrl(issuer);
|
const base = normalizePublicBaseUrl(issuer);
|
||||||
const front = normalizeBaseUrl(frontendUrl);
|
const front = normalizePublicBaseUrl(frontendUrl);
|
||||||
|
|
||||||
let webIdentityOrigin = front;
|
let webIdentityOrigin = front;
|
||||||
try {
|
try {
|
||||||
@@ -123,6 +47,71 @@ function buildFedcmEndpointUrls(issuer: string, frontendUrl: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveSameOriginFrontend(storedFrontend: string, req?: Request): string {
|
||||||
|
const fromRequest = resolvePublicFrontendBaseFromRequest(req);
|
||||||
|
const preferred = preferBrowserReachableBase(storedFrontend, fromRequest);
|
||||||
|
if (preferred !== storedFrontend) {
|
||||||
|
return preferred;
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = readRequestHost(req);
|
||||||
|
if (!host) {
|
||||||
|
return storedFrontend;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOrigin = normalizePublicBaseUrl(`${readRequestProto(req)}://${host}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const frontendUrl = new URL(storedFrontend);
|
||||||
|
const requestUrl = new URL(requestOrigin);
|
||||||
|
|
||||||
|
if (hostsMatch(frontendUrl.hostname, requestUrl.hostname)) {
|
||||||
|
return requestOrigin;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return storedFrontend;
|
||||||
|
}
|
||||||
|
|
||||||
|
return storedFrontend;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveSameOriginIssuer(storedIssuer: string, storedFrontend: string, req?: Request): string {
|
||||||
|
const fromRequest = resolvePublicApiBaseFromRequest(req);
|
||||||
|
const preferred = preferBrowserReachableBase(storedIssuer, fromRequest);
|
||||||
|
if (preferred !== storedIssuer) {
|
||||||
|
return preferred;
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = readRequestHost(req);
|
||||||
|
if (!host) {
|
||||||
|
return storedIssuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOrigin = normalizePublicBaseUrl(`${readRequestProto(req)}://${host}`);
|
||||||
|
const sameOriginApi = `${requestOrigin}/idp-api`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const issuerUrl = new URL(storedIssuer);
|
||||||
|
const frontendUrl = new URL(storedFrontend);
|
||||||
|
const requestUrl = new URL(requestOrigin);
|
||||||
|
|
||||||
|
const hostMatches =
|
||||||
|
hostsMatch(issuerUrl.hostname, requestUrl.hostname) || hostsMatch(frontendUrl.hostname, requestUrl.hostname);
|
||||||
|
|
||||||
|
if (!hostMatches) {
|
||||||
|
return storedIssuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizePublicBaseUrl(storedIssuer) !== sameOriginApi) {
|
||||||
|
return sameOriginApi;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return fromRequest ?? storedIssuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return storedIssuer;
|
||||||
|
}
|
||||||
|
|
||||||
export async function resolveFedcmEndpoints(core: CoreGrpcService, req?: Request): Promise<FedcmEndpoints> {
|
export async function resolveFedcmEndpoints(core: CoreGrpcService, req?: Request): Promise<FedcmEndpoints> {
|
||||||
const storedIssuer = await resolveOAuthIssuer(core);
|
const storedIssuer = await resolveOAuthIssuer(core);
|
||||||
const storedFrontend = await resolveFrontendUrl(core);
|
const storedFrontend = await resolveFrontendUrl(core);
|
||||||
|
|||||||
@@ -1,17 +1,35 @@
|
|||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
import { CoreGrpcService } from '../core-grpc.service';
|
import { CoreGrpcService } from '../core-grpc.service';
|
||||||
|
import { normalizePublicBaseUrl, isInternalHostname } from './public-url';
|
||||||
|
|
||||||
function normalizeBaseUrl(url: string) {
|
function normalizeBaseUrl(url: string) {
|
||||||
return url.trim().replace(/\/+$/, '');
|
return normalizePublicBaseUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendIdpApiPath(base: string) {
|
||||||
|
const normalized = normalizeBaseUrl(base);
|
||||||
|
return normalized.endsWith('/idp-api') ? normalized : `${normalized}/idp-api`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeStoredPublicUrl(url: string) {
|
||||||
|
const normalized = normalizeBaseUrl(url);
|
||||||
|
try {
|
||||||
|
if (isInternalHostname(new URL(normalized).hostname)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveOAuthIssuer(core: CoreGrpcService, fallback = 'http://localhost:3000'): Promise<string> {
|
export async function resolveOAuthIssuer(core: CoreGrpcService, fallback = 'http://localhost:3000'): Promise<string> {
|
||||||
const envIssuer = process.env.PUBLIC_API_URL?.trim();
|
const envIssuer = process.env.PUBLIC_API_URL?.trim();
|
||||||
try {
|
try {
|
||||||
const response = (await firstValueFrom(core.settings.GetSetting({ key: 'PUBLIC_API_URL' }))) as { value?: string };
|
const response = (await firstValueFrom(core.settings.GetSetting({ key: 'PUBLIC_API_URL' }))) as { value?: string };
|
||||||
const fromDb = response.value?.trim();
|
const fromDb = sanitizeStoredPublicUrl(response.value ?? '');
|
||||||
if (fromDb) {
|
if (fromDb) {
|
||||||
return normalizeBaseUrl(fromDb);
|
return fromDb;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// fallback ниже
|
// fallback ниже
|
||||||
@@ -26,9 +44,9 @@ export async function resolveOAuthIssuer(core: CoreGrpcService, fallback = 'http
|
|||||||
const domain = response.value?.trim();
|
const domain = response.value?.trim();
|
||||||
if (domain) {
|
if (domain) {
|
||||||
if (domain.startsWith('http://') || domain.startsWith('https://')) {
|
if (domain.startsWith('http://') || domain.startsWith('https://')) {
|
||||||
return normalizeBaseUrl(domain);
|
return appendIdpApiPath(domain);
|
||||||
}
|
}
|
||||||
return `https://${domain.replace(/^\/+/, '')}`;
|
return appendIdpApiPath(`https://${domain.replace(/^\/+/, '')}`);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// fallback ниже
|
// fallback ниже
|
||||||
@@ -45,9 +63,9 @@ export async function resolveFrontendUrl(core: CoreGrpcService, fallback = 'http
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = (await firstValueFrom(core.settings.GetSetting({ key: 'PUBLIC_FRONTEND_URL' }))) as { value?: string };
|
const response = (await firstValueFrom(core.settings.GetSetting({ key: 'PUBLIC_FRONTEND_URL' }))) as { value?: string };
|
||||||
const fromDb = response.value?.trim();
|
const fromDb = sanitizeStoredPublicUrl(response.value ?? '');
|
||||||
if (fromDb) {
|
if (fromDb) {
|
||||||
return normalizeBaseUrl(fromDb);
|
return fromDb;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// fallback ниже
|
// fallback ниже
|
||||||
|
|||||||
122
apps/api-gateway/src/lib/public-url.ts
Normal file
122
apps/api-gateway/src/lib/public-url.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import type { Request } from 'express';
|
||||||
|
|
||||||
|
const INTERNAL_HOSTNAMES = new Set([
|
||||||
|
'api-gateway',
|
||||||
|
'sso-core',
|
||||||
|
'frontend',
|
||||||
|
'docs',
|
||||||
|
'media-ws',
|
||||||
|
'minio',
|
||||||
|
'postgres',
|
||||||
|
'redis',
|
||||||
|
'rabbitmq',
|
||||||
|
'ldap-auth'
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function normalizePublicBaseUrl(url: string) {
|
||||||
|
return url.trim().replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isInternalHostname(hostname: string) {
|
||||||
|
const host = hostname.trim().toLowerCase();
|
||||||
|
if (!host) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (INTERNAL_HOSTNAMES.has(host)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^\d+\.\d+\.\d+\.\d+$/.test(host) || host === 'localhost') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Одно слово без точки — типичное имя Docker-сервиса (api-gateway, sso-core).
|
||||||
|
return !host.includes('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBrowserReachableBaseUrl(url: string) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !isInternalHostname(parsed.hostname);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readRequestHost(req?: Request): string | undefined {
|
||||||
|
if (!req) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const forwarded = req.headers['x-forwarded-host'];
|
||||||
|
const host = (Array.isArray(forwarded) ? forwarded[0] : forwarded) ?? req.headers.host;
|
||||||
|
return host?.split(',')[0]?.trim() || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readRequestProto(req?: Request): string {
|
||||||
|
if (!req) {
|
||||||
|
return 'https';
|
||||||
|
}
|
||||||
|
|
||||||
|
const forwarded = req.headers['x-forwarded-proto'];
|
||||||
|
const proto = (Array.isArray(forwarded) ? forwarded[0] : forwarded) ?? req.protocol;
|
||||||
|
return proto?.split(',')[0]?.trim() || 'https';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePublicOriginFromRequest(req?: Request): string | null {
|
||||||
|
const host = readRequestHost(req);
|
||||||
|
if (!host) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = host.split(':')[0];
|
||||||
|
if (isInternalHostname(hostname)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizePublicBaseUrl(`${readRequestProto(req)}://${host}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePublicApiBaseFromRequest(req?: Request): string | null {
|
||||||
|
const origin = resolvePublicOriginFromRequest(req);
|
||||||
|
if (!origin) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${origin}/idp-api`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePublicFrontendBaseFromRequest(req?: Request): string | null {
|
||||||
|
return resolvePublicOriginFromRequest(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function preferBrowserReachableBase(stored: string, fromRequest: string | null) {
|
||||||
|
if (fromRequest && isBrowserReachableBaseUrl(fromRequest)) {
|
||||||
|
try {
|
||||||
|
const storedUrl = new URL(stored);
|
||||||
|
if (isInternalHostname(storedUrl.hostname)) {
|
||||||
|
return fromRequest;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return fromRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizePublicBaseUrl(stored) !== normalizePublicBaseUrl(fromRequest)) {
|
||||||
|
return fromRequest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hostsMatch(a: string, b: string) {
|
||||||
|
if (a === b) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a === 'localhost' && b === 'localhost';
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import type { NextConfig } from 'next';
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
const internalApiUrl = (
|
function resolveInternalApiUrl(fallback = 'http://localhost:3000') {
|
||||||
process.env.INTERNAL_API_URL ??
|
const explicit = process.env.INTERNAL_API_URL?.trim();
|
||||||
process.env.NEXT_PUBLIC_API_URL ??
|
if (explicit) {
|
||||||
'http://localhost:3000'
|
return explicit.replace(/\/+$/, '');
|
||||||
).replace(/\/$/, '');
|
}
|
||||||
|
return fallback.replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const internalApiUrl = resolveInternalApiUrl('http://localhost:3000');
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
|
|||||||
@@ -7,16 +7,25 @@ function resolveBrowserApiBaseUrl(): string {
|
|||||||
return configuredApiUrl;
|
return configuredApiUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (configuredApiUrl.startsWith('/')) {
|
||||||
const configured = new URL(configuredApiUrl);
|
return configuredApiUrl.replace(/\/+$/, '') || API_ORIGIN_PROXY_PREFIX;
|
||||||
if (configured.hostname !== window.location.hostname) {
|
|
||||||
return API_ORIGIN_PROXY_PREFIX;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// keep configured URL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return configuredApiUrl;
|
try {
|
||||||
|
const configured = new URL(configuredApiUrl);
|
||||||
|
const sameHost =
|
||||||
|
configured.hostname === window.location.hostname &&
|
||||||
|
(configured.port || (configured.protocol === 'https:' ? '443' : '80')) ===
|
||||||
|
(window.location.port || (window.location.protocol === 'https:' ? '443' : '80'));
|
||||||
|
|
||||||
|
if (sameHost) {
|
||||||
|
return API_ORIGIN_PROXY_PREFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
return configuredApiUrl.replace(/\/+$/, '');
|
||||||
|
} catch {
|
||||||
|
return API_ORIGIN_PROXY_PREFIX;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveBrowserWsBaseUrl(): string {
|
function resolveBrowserWsBaseUrl(): string {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const SYSTEM_SETTING_CATALOG: SystemSettingMeta[] = [
|
|||||||
label: 'URL API (OAuth issuer)',
|
label: 'URL API (OAuth issuer)',
|
||||||
group: 'general',
|
group: 'general',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
hint: 'Базовый URL API для OAuth/OIDC и FedCM. При same-origin деплое: https://ваш-домен/idp-api'
|
hint: 'Публичный URL, доступный из браузера пользователя. Не используйте api-gateway или другие Docker-имена. Пример: https://id.lendry.ru/idp-api'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'PUBLIC_FRONTEND_URL',
|
key: 'PUBLIC_FRONTEND_URL',
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import type { NextConfig } from 'next';
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
const internalApiUrl = (
|
function resolveInternalApiUrl(fallback = 'http://localhost:3000') {
|
||||||
process.env.INTERNAL_API_URL ??
|
const explicit = process.env.INTERNAL_API_URL?.trim();
|
||||||
process.env.NEXT_PUBLIC_API_URL ??
|
if (explicit) {
|
||||||
'http://localhost:3000'
|
return explicit.replace(/\/+$/, '');
|
||||||
).replace(/\/$/, '');
|
}
|
||||||
|
return fallback.replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const internalApiUrl = resolveInternalApiUrl('http://localhost:3000');
|
||||||
|
|
||||||
const internalWsUrl = (process.env.INTERNAL_WS_URL ?? 'http://media-ws:8085').replace(/\/$/, '');
|
const internalWsUrl = (process.env.INTERNAL_WS_URL ?? 'http://media-ws:8085').replace(/\/$/, '');
|
||||||
|
|
||||||
|
|||||||
@@ -66,19 +66,35 @@
|
|||||||
return 'IdentityCredential' in global && global.navigator && typeof global.navigator.credentials !== 'undefined';
|
return 'IdentityCredential' in global && global.navigator && typeof global.navigator.credentials !== 'undefined';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isBrowserReachableBaseUrl(url) {
|
||||||
|
try {
|
||||||
|
var parsed = new URL(url);
|
||||||
|
var host = parsed.hostname.toLowerCase();
|
||||||
|
if (host === 'api-gateway' || host === 'sso-core' || host === 'frontend' || host === 'docs') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (host.indexOf('.') === -1 && host !== 'localhost') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
||||||
|
} catch (_error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function loginWithFedCM(idpApiBase, clientId) {
|
function loginWithFedCM(idpApiBase, clientId) {
|
||||||
if (!supportsFedCM()) {
|
if (!supportsFedCM()) {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestFedCM(configBase) {
|
function requestFedCMConfig(configUrl) {
|
||||||
return global.navigator.credentials
|
return global.navigator.credentials
|
||||||
.get({
|
.get({
|
||||||
identity: {
|
identity: {
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
configURL: configBase + '/fedcm/config.json',
|
configURL: configUrl,
|
||||||
configUrl: configBase + '/fedcm/config.json',
|
configUrl: configUrl,
|
||||||
clientId: clientId
|
clientId: clientId
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -96,40 +112,57 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requestFedCM(configBase) {
|
||||||
|
return requestFedCMConfig(configBase + '/fedcm/config.json');
|
||||||
|
}
|
||||||
|
|
||||||
return global.fetch(idpApiBase + '/fedcm/discover.json', { credentials: 'omit' })
|
return global.fetch(idpApiBase + '/fedcm/discover.json', { credentials: 'omit' })
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return idpApiBase;
|
return { mode: 'apiBase', value: idpApiBase };
|
||||||
}
|
}
|
||||||
return response.json().then(function (payload) {
|
return response.json().then(function (payload) {
|
||||||
if (payload && payload.enabled === false) {
|
if (payload && payload.enabled === false) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (payload && payload.apiBase) {
|
if (payload && payload.configUrl && isBrowserReachableBaseUrl(payload.configUrl)) {
|
||||||
return trimSlash(payload.apiBase);
|
return { mode: 'configUrl', value: payload.configUrl, webIdentityUrl: payload.webIdentityUrl };
|
||||||
}
|
}
|
||||||
return idpApiBase;
|
if (payload && payload.apiBase && isBrowserReachableBaseUrl(payload.apiBase)) {
|
||||||
|
return { mode: 'apiBase', value: trimSlash(payload.apiBase), webIdentityUrl: payload.webIdentityUrl };
|
||||||
|
}
|
||||||
|
if (isBrowserReachableBaseUrl(idpApiBase)) {
|
||||||
|
return { mode: 'apiBase', value: idpApiBase };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
return idpApiBase;
|
if (isBrowserReachableBaseUrl(idpApiBase)) {
|
||||||
|
return { mode: 'apiBase', value: idpApiBase };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
})
|
})
|
||||||
.then(function (resolvedBase) {
|
.then(function (resolved) {
|
||||||
if (!resolvedBase) {
|
if (!resolved) {
|
||||||
|
if (global.console && typeof global.console.warn === 'function') {
|
||||||
|
global.console.warn(
|
||||||
|
'[MVK ID] FedCM: URL IdP недоступен из браузера. Укажите публичный data-idp-url (например https://id.lendry.ru/idp-api), не внутренний Docker-хост.'
|
||||||
|
);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return requestFedCM(resolvedBase);
|
if (resolved.mode === 'configUrl') {
|
||||||
|
return requestFedCMConfig(resolved.value);
|
||||||
|
}
|
||||||
|
return requestFedCM(resolved.value);
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
if (global.console && typeof global.console.warn === 'function') {
|
if (global.console && typeof global.console.warn === 'function') {
|
||||||
global.console.warn(
|
global.console.warn(
|
||||||
'[MVK ID] FedCM недоступен:',
|
'[MVK ID] FedCM недоступен:',
|
||||||
error,
|
error,
|
||||||
'Проверьте доступность',
|
'Проверьте публичный URL IdP в data-idp-url и доступность /.well-known/web-identity на домене IdP (не /idp-api/.well-known).'
|
||||||
idpApiBase + '/fedcm/config.json',
|
|
||||||
'и',
|
|
||||||
idpApiBase.replace(/\/idp-api$/, '') + '/.well-known/web-identity',
|
|
||||||
'(PUBLIC_API_URL должен совпадать с data-idp-url виджета)'
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user