fix and update
This commit is contained in:
@@ -77,7 +77,7 @@ export function buildFedcmProviderConfig(endpoints: FedcmEndpoints) {
|
|||||||
login_url: endpoints.loginUrl,
|
login_url: endpoints.loginUrl,
|
||||||
branding: {
|
branding: {
|
||||||
background_color: '#ffffff',
|
background_color: '#ffffff',
|
||||||
color: '#3390ec',
|
color: '#1f2430',
|
||||||
icons: [{ url: `${endpoints.frontendUrl}/favicon.ico`, size: 32 }]
|
icons: [{ url: `${endpoints.frontendUrl}/favicon.ico`, size: 32 }]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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';
|
import { normalizePublicBaseUrl, pickBestPublicBase, isInternalHostname } from './public-url';
|
||||||
|
|
||||||
function normalizeBaseUrl(url: string) {
|
function normalizeBaseUrl(url: string) {
|
||||||
return normalizePublicBaseUrl(url);
|
return normalizePublicBaseUrl(url);
|
||||||
@@ -23,68 +23,49 @@ function sanitizeStoredPublicUrl(url: string) {
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveOAuthIssuer(core: CoreGrpcService, fallback = 'http://localhost:3000'): Promise<string> {
|
async function resolveProjectDomainUrl(core: CoreGrpcService, withIdpApiPath = false): Promise<string | undefined> {
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = (await firstValueFrom(core.settings.GetSetting({ key: 'PROJECT_DOMAIN' }))) as { value?: string };
|
const response = (await firstValueFrom(core.settings.GetSetting({ key: 'PROJECT_DOMAIN' }))) as { value?: string };
|
||||||
const domain = response.value?.trim();
|
const domain = response.value?.trim();
|
||||||
if (domain) {
|
if (!domain) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
if (domain.startsWith('http://') || domain.startsWith('https://')) {
|
if (domain.startsWith('http://') || domain.startsWith('https://')) {
|
||||||
return appendIdpApiPath(domain);
|
return withIdpApiPath ? appendIdpApiPath(domain) : normalizeBaseUrl(domain);
|
||||||
}
|
}
|
||||||
return appendIdpApiPath(`https://${domain.replace(/^\/+/, '')}`);
|
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 {
|
} catch {
|
||||||
// fallback ниже
|
// 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> {
|
export async function resolveFrontendUrl(core: CoreGrpcService, fallback = 'http://localhost:3002'): Promise<string> {
|
||||||
const envFrontend = process.env.PUBLIC_FRONTEND_URL?.trim();
|
const envFrontend = sanitizeStoredPublicUrl(process.env.PUBLIC_FRONTEND_URL?.trim() ?? '');
|
||||||
if (envFrontend) {
|
let fromDb: string | undefined;
|
||||||
return normalizeBaseUrl(envFrontend);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = sanitizeStoredPublicUrl(response.value ?? '');
|
fromDb = sanitizeStoredPublicUrl(response.value ?? '');
|
||||||
if (fromDb) {
|
|
||||||
return fromDb;
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// fallback ниже
|
// fallback ниже
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const fromDomain = await resolveProjectDomainUrl(core, false);
|
||||||
const response = (await firstValueFrom(core.settings.GetSetting({ key: 'PROJECT_DOMAIN' }))) as { value?: string };
|
return pickBestPublicBase([fromDb, envFrontend, fromDomain], fallback);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildOpenIdConfiguration(issuer: string) {
|
export function buildOpenIdConfiguration(issuer: string) {
|
||||||
|
|||||||
@@ -103,14 +103,47 @@ export function resolvePublicFrontendBaseFromRequest(req?: Request): string | nu
|
|||||||
* из настроек (PUBLIC_API_URL / PUBLIC_FRONTEND_URL), а на хост запроса
|
* из настроек (PUBLIC_API_URL / PUBLIC_FRONTEND_URL), а на хост запроса
|
||||||
* переключаемся только если в настройках указан внутренний Docker-хост.
|
* переключаемся только если в настройках указан внутренний 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) {
|
export function preferCanonicalBase(stored: string, fromRequest: string | null) {
|
||||||
if (stored && isBrowserReachableBaseUrl(stored)) {
|
return pickBestPublicBase([stored, fromRequest], stored || fromRequest || '');
|
||||||
return normalizePublicBaseUrl(stored);
|
|
||||||
}
|
|
||||||
if (fromRequest && isBrowserReachableBaseUrl(fromRequest)) {
|
|
||||||
return normalizePublicBaseUrl(fromRequest);
|
|
||||||
}
|
|
||||||
return normalizePublicBaseUrl(stored);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function preferBrowserReachableBase(stored: string, fromRequest: string | null) {
|
export function preferBrowserReachableBase(stored: string, fromRequest: string | null) {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function deliverOAuthPopupResult(
|
|||||||
const delivered = postOneTapResult(popupContext.popupOrigin, {
|
const delivered = postOneTapResult(popupContext.popupOrigin, {
|
||||||
code: parsed.code ?? undefined,
|
code: parsed.code ?? undefined,
|
||||||
state: parsed.state ?? undefined,
|
state: parsed.state ?? undefined,
|
||||||
token: parsed.code ?? undefined,
|
grantType: parsed.code ? 'authorization_code' : undefined,
|
||||||
error: parsed.error ?? undefined,
|
error: parsed.error ?? undefined,
|
||||||
errorDescription: parsed.errorDescription ?? undefined
|
errorDescription: parsed.errorDescription ?? undefined
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -158,17 +158,45 @@
|
|||||||
return requestFedCM(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.debug === 'function') {
|
||||||
global.console.warn(
|
global.console.debug(
|
||||||
'[MVK ID] FedCM недоступен:',
|
'[MVK ID] FedCM недоступен (будет использован popup):',
|
||||||
error,
|
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;
|
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 = {
|
var SIZE_PRESETS = {
|
||||||
s: { height: 32, font: 13, padX: 12, gap: 8, badge: 20, radius: 16 },
|
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 },
|
m: { height: 40, font: 14, padX: 16, gap: 10, badge: 24, radius: 20 },
|
||||||
@@ -406,6 +434,8 @@
|
|||||||
dispatchSuccess(
|
dispatchSuccess(
|
||||||
{
|
{
|
||||||
token: result.token,
|
token: result.token,
|
||||||
|
idToken: result.token,
|
||||||
|
grantType: 'id_token',
|
||||||
method: 'fedcm'
|
method: 'fedcm'
|
||||||
},
|
},
|
||||||
script
|
script
|
||||||
@@ -420,11 +450,14 @@
|
|||||||
openPopup(popupUrl, frontendBase, function (payload) {
|
openPopup(popupUrl, frontendBase, function (payload) {
|
||||||
dispatchSuccess(
|
dispatchSuccess(
|
||||||
{
|
{
|
||||||
token: payload.token,
|
|
||||||
accessToken: payload.accessToken,
|
|
||||||
idToken: payload.idToken,
|
|
||||||
refreshToken: payload.refreshToken,
|
|
||||||
code: payload.code,
|
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'
|
method: 'popup'
|
||||||
},
|
},
|
||||||
script
|
script
|
||||||
@@ -439,6 +472,7 @@
|
|||||||
global.LendryIdOneTap = {
|
global.LendryIdOneTap = {
|
||||||
init: initOneTap,
|
init: initOneTap,
|
||||||
loginWithFedCM: loginWithFedCM,
|
loginWithFedCM: loginWithFedCM,
|
||||||
|
exchangeAuthorizationCode: exchangeAuthorizationCode,
|
||||||
MESSAGE_TYPE: MESSAGE_TYPE
|
MESSAGE_TYPE: MESSAGE_TYPE
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ services:
|
|||||||
MINIO_BUCKET: ${MINIO_BUCKET:-lendry-id}
|
MINIO_BUCKET: ${MINIO_BUCKET:-lendry-id}
|
||||||
MINIO_USE_SSL: ${MINIO_USE_SSL:-false}
|
MINIO_USE_SSL: ${MINIO_USE_SSL:-false}
|
||||||
PUBLIC_API_URL: ${PUBLIC_API_URL:-http://localhost:3002/idp-api}
|
PUBLIC_API_URL: ${PUBLIC_API_URL:-http://localhost:3002/idp-api}
|
||||||
|
PUBLIC_FRONTEND_URL: ${PUBLIC_FRONTEND_URL:-http://localhost:3002}
|
||||||
FEDCM_COOKIE_SECURE: ${FEDCM_COOKIE_SECURE:-false}
|
FEDCM_COOKIE_SECURE: ${FEDCM_COOKIE_SECURE:-false}
|
||||||
TRUST_PROXY: ${TRUST_PROXY:-true}
|
TRUST_PROXY: ${TRUST_PROXY:-true}
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
41
install.sh
41
install.sh
@@ -3046,7 +3046,11 @@ verify_fedcm_endpoints() {
|
|||||||
return 0
|
return 0
|
||||||
elif [[ "$code" == "000" ]]; then
|
elif [[ "$code" == "000" ]]; then
|
||||||
echo -e " ${RED}✘${NC} FedCM web-identity недоступен (${apex_domain}) — ERR_CERT или DNS"
|
echo -e " ${RED}✘${NC} FedCM web-identity недоступен (${apex_domain}) — ERR_CERT или DNS"
|
||||||
echo " Добавьте ${apex_domain} в hosts/DNS и примите сертификат (FedCM всегда запрашивает eTLD+1)."
|
echo " Добавьте ${apex_domain} в hosts/DNS и примите сертификат в браузере на каждом клиенте:"
|
||||||
|
echo " откройте https://${apex_domain}/.well-known/web-identity → «Дополнительно» → принять риск."
|
||||||
|
if [[ "$(env_get SSL_TYPE none)" == "selfsigned" ]]; then
|
||||||
|
echo " Надёжнее: импортируйте ${LOCAL_CERTS_DIR}/shared-intranet/fullchain.pem в «Доверенные корневые центры»."
|
||||||
|
fi
|
||||||
return 1
|
return 1
|
||||||
elif [[ "$code" == "502" ]]; then
|
elif [[ "$code" == "502" ]]; then
|
||||||
echo -e " ${RED}✘${NC} FedCM web-identity → 502 (${apex_domain}) — проверьте nginx/conf.d/${NGINX_PREFIX}-fedcm-apex.conf"
|
echo -e " ${RED}✘${NC} FedCM web-identity → 502 (${apex_domain}) — проверьте nginx/conf.d/${NGINX_PREFIX}-fedcm-apex.conf"
|
||||||
@@ -3084,6 +3088,40 @@ validate_ssl_cert_covers_apex() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Синхронизирует PUBLIC_* из .env в SystemSetting — иначе FedCM login_url остаётся localhost:3002.
|
||||||
|
sync_public_system_settings() {
|
||||||
|
load_env
|
||||||
|
[[ -n "${PUBLIC_API_URL:-}" || -n "${PUBLIC_FRONTEND_URL:-}" ]] || return 0
|
||||||
|
if ! docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -q '^lendry-id-postgres$'; then
|
||||||
|
warn "Postgres недоступен — PUBLIC_* в БД не обновлены (FedCM login_url может указывать на localhost)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local pg_user pg_pass pg_db sql
|
||||||
|
pg_user="$(env_get POSTGRES_USER lendry)"
|
||||||
|
pg_pass="$(env_get POSTGRES_PASSWORD lendry_password)"
|
||||||
|
pg_db="$(env_get POSTGRES_DB lendry_id)"
|
||||||
|
sql=""
|
||||||
|
|
||||||
|
if [[ -n "${PUBLIC_API_URL:-}" ]]; then
|
||||||
|
sql+="UPDATE \"SystemSetting\" SET value='${PUBLIC_API_URL}', \"updatedAt\"=NOW() WHERE key='PUBLIC_API_URL';"
|
||||||
|
fi
|
||||||
|
if [[ -n "${PUBLIC_FRONTEND_URL:-}" ]]; then
|
||||||
|
sql+="UPDATE \"SystemSetting\" SET value='${PUBLIC_FRONTEND_URL}', \"updatedAt\"=NOW() WHERE key='PUBLIC_FRONTEND_URL';"
|
||||||
|
fi
|
||||||
|
if [[ -n "${DOMAIN_FRONTEND:-}" ]]; then
|
||||||
|
sql+="UPDATE \"SystemSetting\" SET value='${DOMAIN_FRONTEND}', \"updatedAt\"=NOW() WHERE key='PROJECT_DOMAIN';"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -n "$sql" ]] || return 0
|
||||||
|
if docker_cmd exec -e PGPASSWORD="$pg_pass" lendry-id-postgres \
|
||||||
|
psql -U "$pg_user" -d "$pg_db" -v ON_ERROR_STOP=1 -c "$sql" >/dev/null 2>&1; then
|
||||||
|
ok "SystemSetting: PUBLIC_API_URL / PUBLIC_FRONTEND_URL синхронизированы из .env"
|
||||||
|
else
|
||||||
|
warn "Не удалось обновить PUBLIC_* в SystemSetting — проверьте значения в админке → Настройки"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
write_all_nginx_configs() {
|
write_all_nginx_configs() {
|
||||||
ensure_nginx_mode_for_config_write
|
ensure_nginx_mode_for_config_write
|
||||||
local ssl_type="${1:-$(env_get SSL_TYPE none)}"
|
local ssl_type="${1:-$(env_get SSL_TYPE none)}"
|
||||||
@@ -4018,6 +4056,7 @@ action_fix_all_errors() {
|
|||||||
check_sso_idp_api_proxy || fail "SSO /idp-api/health недоступен — см. nginx/conf.d/${NGINX_PREFIX}-frontend.conf"
|
check_sso_idp_api_proxy || fail "SSO /idp-api/health недоступен — см. nginx/conf.d/${NGINX_PREFIX}-frontend.conf"
|
||||||
check_sso_frontend_root || fail "SSO / → 502 — главная страница недоступна (502 Bad Gateway в браузере)"
|
check_sso_frontend_root || fail "SSO / → 502 — главная страница недоступна (502 Bad Gateway в браузере)"
|
||||||
verify_sso_idp_api_burst || warn "Нестабильный /idp-api/health — при 502 в браузере: docker compose --profile proxy exec nginx nginx -s reload"
|
verify_sso_idp_api_burst || warn "Нестабильный /idp-api/health — при 502 в браузере: docker compose --profile proxy exec nginx nginx -s reload"
|
||||||
|
sync_public_system_settings || true
|
||||||
validate_ssl_cert_covers_apex || true
|
validate_ssl_cert_covers_apex || true
|
||||||
verify_fedcm_endpoints || warn "FedCM endpoints недоступны — см. DNS/SSL для eTLD+1 и nginx/conf.d/${NGINX_PREFIX}-fedcm-apex.conf"
|
verify_fedcm_endpoints || warn "FedCM endpoints недоступны — см. DNS/SSL для eTLD+1 и nginx/conf.d/${NGINX_PREFIX}-fedcm-apex.conf"
|
||||||
if ! docker_nginx_container_running; then
|
if ! docker_nginx_container_running; then
|
||||||
|
|||||||
Reference in New Issue
Block a user