fix and update

This commit is contained in:
lendry
2026-06-29 15:08:26 +03:00
parent 01e4917acf
commit e127df3d6d
8 changed files with 57 additions and 16 deletions

View File

@@ -20,17 +20,17 @@ SSL_CERT_FULLCHAIN=
SSL_CERT_KEY= SSL_CERT_KEY=
# Домены без протокола (install.sh подставит PUBLIC_* URL) # Домены без протокола (install.sh подставит PUBLIC_* URL)
DOMAIN_API= DOMAIN_API=api.idpmvk.lpr
DOMAIN_FRONTEND= DOMAIN_FRONTEND=sso.idpmvk.lpr
DOMAIN_DOCS= DOMAIN_DOCS=
# Пусто = WebSocket на DOMAIN_API по пути /ws # Пусто = WebSocket на DOMAIN_API по пути /ws
DOMAIN_WS= DOMAIN_WS=
DOMAIN_MINIO= DOMAIN_MINIO=
DOMAIN_MINIO_CONSOLE= DOMAIN_MINIO_CONSOLE=
# Публичные URL (генерируются install.sh) # Публичные URL (генерируются install.sh; split-domain: API и SSO отдельно)
PUBLIC_API_URL=http://localhost:3002/idp-api PUBLIC_API_URL=https://api.idpmvk.lpr
PUBLIC_FRONTEND_URL=http://localhost:3002 PUBLIC_FRONTEND_URL=https://sso.idpmvk.lpr
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 # Docker: http://api-gateway:3000 | локальный npm run dev: http://localhost:3000

View File

@@ -14,6 +14,7 @@ import { Input } from '@/components/ui/input';
import { OAuthClient, OAuthScope, apiFetch } from '@/lib/api'; import { OAuthClient, OAuthScope, apiFetch } from '@/lib/api';
import { getAdminLandingPath } from '@/lib/admin-access'; import { getAdminLandingPath } from '@/lib/admin-access';
import { buildAuthorizeUrl, buildOAuthEndpoints, resolveFrontendBase, resolveOAuthApiBase } from '@/lib/oauth-url'; import { buildAuthorizeUrl, buildOAuthEndpoints, resolveFrontendBase, resolveOAuthApiBase } from '@/lib/oauth-url';
import { DEFAULT_PUBLIC_API_URL, DEFAULT_PUBLIC_FRONTEND_URL } from '@/lib/project-domains';
export default function AdminOAuthPage() { export default function AdminOAuthPage() {
const router = useRouter(); const router = useRouter();
@@ -26,8 +27,8 @@ export default function AdminOAuthPage() {
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [selectedClient, setSelectedClient] = useState<OAuthClient | null>(null); const [selectedClient, setSelectedClient] = useState<OAuthClient | null>(null);
const [secretDialog, setSecretDialog] = useState<{ clientId: string; clientSecret: string } | null>(null); const [secretDialog, setSecretDialog] = useState<{ clientId: string; clientSecret: string } | null>(null);
const [oauthApiBase, setOauthApiBase] = useState('http://localhost:3002/idp-api'); const [oauthApiBase, setOauthApiBase] = useState(DEFAULT_PUBLIC_API_URL);
const [frontendBase, setFrontendBase] = useState('http://localhost:3002'); const [frontendBase, setFrontendBase] = useState(DEFAULT_PUBLIC_FRONTEND_URL);
const [projectName, setProjectName] = useState('MVK ID'); const [projectName, setProjectName] = useState('MVK ID');
const [form, setForm] = useState({ name: '', redirectUris: '', type: 'CONFIDENTIAL', selectedScopes: ['openid', 'profile', 'email'] as string[] }); const [form, setForm] = useState({ name: '', redirectUris: '', type: 'CONFIDENTIAL', selectedScopes: ['openid', 'profile', 'email'] as string[] });

View File

@@ -6,7 +6,7 @@ export interface ParsedAddress {
} }
const NOMINATIM_HEADERS = { const NOMINATIM_HEADERS = {
'User-Agent': 'LendryID/1.0 (contact@lendry.ru)', 'User-Agent': 'MVKID/1.0 (sso@idpmvk.lpr)',
Accept: 'application/json' Accept: 'application/json'
}; };

View File

@@ -1,8 +1,14 @@
import {
DEFAULT_PUBLIC_API_URL,
DEFAULT_PUBLIC_FRONTEND_URL,
inferApiBaseFromProjectDomain
} from './project-domains';
export function normalizeBaseUrl(url: string) { export function normalizeBaseUrl(url: string) {
return url.trim().replace(/\/+$/, ''); return url.trim().replace(/\/+$/, '');
} }
export function resolveOAuthApiBase(settings: Record<string, string>, fallback = 'http://localhost:3002/idp-api') { export function resolveOAuthApiBase(settings: Record<string, string>, fallback = DEFAULT_PUBLIC_API_URL) {
const publicApi = settings.PUBLIC_API_URL?.trim(); const publicApi = settings.PUBLIC_API_URL?.trim();
if (publicApi) { if (publicApi) {
return normalizeBaseUrl(publicApi); return normalizeBaseUrl(publicApi);
@@ -10,16 +16,22 @@ export function resolveOAuthApiBase(settings: Record<string, string>, fallback =
const domain = settings.PROJECT_DOMAIN?.trim(); const domain = settings.PROJECT_DOMAIN?.trim();
if (domain) { if (domain) {
const inferred = inferApiBaseFromProjectDomain(domain);
if (inferred) {
return inferred;
}
if (domain.startsWith('http://') || domain.startsWith('https://')) { if (domain.startsWith('http://') || domain.startsWith('https://')) {
return normalizeBaseUrl(domain); return normalizeBaseUrl(domain);
} }
return `https://${domain.replace(/^\/+/, '')}/idp-api`; return `https://${domain.replace(/^\/+/, '')}/idp-api`;
} }
return normalizeBaseUrl(fallback); return normalizeBaseUrl(fallback);
} }
export function resolveFrontendBase(settings: Record<string, string>, fallback = 'http://localhost:3002') { export function resolveFrontendBase(settings: Record<string, string>, fallback = DEFAULT_PUBLIC_FRONTEND_URL) {
const publicFrontend = settings.PUBLIC_FRONTEND_URL?.trim(); const publicFrontend = settings.PUBLIC_FRONTEND_URL?.trim();
if (publicFrontend) { if (publicFrontend) {
return normalizeBaseUrl(publicFrontend); return normalizeBaseUrl(publicFrontend);

View File

@@ -0,0 +1,28 @@
/** Публичные домены IdP (split-domain: API и SSO на разных хостах). */
export const PROJECT_API_HOST = 'api.idpmvk.lpr';
export const PROJECT_SSO_HOST = 'sso.idpmvk.lpr';
export const DEFAULT_PUBLIC_API_URL = `https://${PROJECT_API_HOST}`;
export const DEFAULT_PUBLIC_FRONTEND_URL = `https://${PROJECT_SSO_HOST}`;
export function inferApiBaseFromProjectDomain(domain: string): string | null {
const host = domain
.trim()
.replace(/^https?:\/\//i, '')
.split('/')[0]
.toLowerCase();
if (!host) {
return null;
}
if (host.startsWith('sso.')) {
return `https://${host.replace(/^sso\./, 'api.')}`;
}
if (host.startsWith('api.')) {
return `https://${host}`;
}
return null;
}

View File

@@ -25,20 +25,20 @@ export const SYSTEM_SETTING_GROUPS: Record<string, string> = {
export const SYSTEM_SETTING_CATALOG: SystemSettingMeta[] = [ export const SYSTEM_SETTING_CATALOG: SystemSettingMeta[] = [
{ key: 'PROJECT_NAME', label: 'Название проекта', group: 'general', type: 'text', hint: 'Отображается в меню, заголовке и боковой панели' }, { key: 'PROJECT_NAME', label: 'Название проекта', group: 'general', type: 'text', hint: 'Отображается в меню, заголовке и боковой панели' },
{ key: 'PROJECT_TAGLINE', label: 'Слоган проекта', group: 'general', type: 'text' }, { key: 'PROJECT_TAGLINE', label: 'Слоган проекта', group: 'general', type: 'text' },
{ key: 'PROJECT_DOMAIN', label: 'Домен IdP', group: 'general', type: 'text', hint: 'Домен без пути, например sso.example.ru' }, { key: 'PROJECT_DOMAIN', label: 'Домен IdP (SSO)', group: 'general', type: 'text', hint: 'Frontend-домен без пути, например sso.idpmvk.lpr' },
{ {
key: 'PUBLIC_API_URL', key: 'PUBLIC_API_URL',
label: 'URL API (OAuth issuer)', label: 'URL API (OAuth issuer)',
group: 'general', group: 'general',
type: 'text', type: 'text',
hint: 'Публичный URL, доступный из браузера пользователя. Не используйте api-gateway или другие Docker-имена. Пример: https://id.lendry.ru/idp-api' hint: 'Публичный URL, доступный из браузера. Пример: https://api.idpmvk.lpr (отдельный домен API) или https://sso.idpmvk.lpr/idp-api (same-origin)'
}, },
{ {
key: 'PUBLIC_FRONTEND_URL', key: 'PUBLIC_FRONTEND_URL',
label: 'URL Frontend IdP', label: 'URL Frontend IdP',
group: 'general', group: 'general',
type: 'text', type: 'text',
hint: 'Публичный адрес интерфейса IdP. Используется для login_url FedCM и popup OAuth' hint: 'Публичный адрес UI IdP. Пример: https://sso.idpmvk.lpr'
}, },
{ {
key: 'ONE_TAP_ENABLED', key: 'ONE_TAP_ENABLED',

View File

@@ -147,7 +147,7 @@
if (!resolved) { if (!resolved) {
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: URL IdP недоступен из браузера. Укажите публичный data-idp-url (например https://id.lendry.ru/idp-api), не внутренний Docker-хост.' '[MVK ID] FedCM: URL IdP недоступен из браузера. Укажите публичный data-idp-url (например https://api.idpmvk.lpr), не внутренний Docker-хост.'
); );
} }
return null; return null;

View File

@@ -4,11 +4,11 @@ import { PrismaService } from '../infra/prisma.service';
export const DEFAULT_SYSTEM_SETTINGS = [ export const DEFAULT_SYSTEM_SETTINGS = [
{ key: 'PROJECT_NAME', value: 'MVK ID', description: 'Название проекта в интерфейсе и заголовке страницы' }, { key: 'PROJECT_NAME', value: 'MVK ID', description: 'Название проекта в интерфейсе и заголовке страницы' },
{ key: 'PROJECT_TAGLINE', value: 'Единый аккаунт для сервисов Lendry', description: 'Краткий слоган под названием проекта' }, { key: 'PROJECT_TAGLINE', value: 'Единый аккаунт для сервисов Lendry', description: 'Краткий слоган под названием проекта' },
{ key: 'PROJECT_DOMAIN', value: 'id.lendry.ru', description: 'Основной домен IdP (без пути)' }, { key: 'PROJECT_DOMAIN', value: 'sso.idpmvk.lpr', description: 'Основной домен IdP / frontend (без пути)' },
{ {
key: 'PUBLIC_API_URL', key: 'PUBLIC_API_URL',
value: 'http://localhost:3002/idp-api', value: 'http://localhost:3002/idp-api',
description: 'Публичный базовый URL API для OAuth и OpenID Connect (issuer). Пример: https://sso.example.ru/idp-api' description: 'Публичный базовый URL API для OAuth и OpenID Connect (issuer). Пример: https://api.idpmvk.lpr'
}, },
{ {
key: 'PUBLIC_FRONTEND_URL', key: 'PUBLIC_FRONTEND_URL',