fix and update

This commit is contained in:
lendry
2026-06-30 16:42:48 +03:00
parent 2eeb928a72
commit 31251be877
4 changed files with 113 additions and 97 deletions

View File

@@ -18,11 +18,6 @@ export function AppBootstrapScreen({
<p className="mt-2 max-w-sm text-center text-sm text-[#667085]"> <p className="mt-2 max-w-sm text-center text-sm text-[#667085]">
Сервер авторизации временно недоступен. Обычно помогает повтор через несколько секунд. Сервер авторизации временно недоступен. Обычно помогает повтор через несколько секунд.
</p> </p>
{onRetry ? (
<Button className="mt-6 rounded-xl" onClick={onRetry}>
Повторить подключение
</Button>
) : null}
</> </>
) : ( ) : (
<> <>
@@ -33,6 +28,11 @@ export function AppBootstrapScreen({
</p> </p>
</> </>
)} )}
{onRetry ? (
<Button className="mt-6 rounded-xl" variant={error ? 'default' : 'secondary'} onClick={onRetry}>
Повторить подключение
</Button>
) : null}
</div> </div>
); );
} }

View File

@@ -200,7 +200,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setIsApiReady(false); setIsApiReady(false);
try { try {
await waitForStableGateway(3); await waitForStableGateway(1);
const session = await fetchAuthSession(accessToken); const session = await fetchAuthSession(accessToken);
applySessionState(session, { setUser, setToken, activatePinLock, clearPinLock }); applySessionState(session, { setUser, setToken, activatePinLock, clearPinLock });
initialBootstrapDoneRef.current = true; initialBootstrapDoneRef.current = true;
@@ -287,7 +287,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
for (let bootstrapAttempt = 0; bootstrapAttempt < 4; bootstrapAttempt += 1) { for (let bootstrapAttempt = 0; bootstrapAttempt < 4; bootstrapAttempt += 1) {
try { try {
if (isInitialBootstrap) { if (isInitialBootstrap) {
await waitForStableGateway(3); await waitForStableGateway(1);
} }
if (currentToken) { if (currentToken) {
@@ -343,11 +343,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
} catch (error) { } catch (error) {
if (isGatewayUnavailableError(error) && isInitialBootstrap && bootstrapAttempt + 1 < 4) { if (isGatewayUnavailableError(error) && isInitialBootstrap && bootstrapAttempt + 1 < 4) {
invalidateApiGatewayReady(); invalidateApiGatewayReady();
if (bootstrapAttempt >= 1) { setBootstrapError(
setBootstrapError( 'Не удалось подключиться к серверу API (502). Подождите несколько секунд и нажмите «Повторить».'
'Не удалось подключиться к серверу API (502). Подождите несколько секунд и нажмите «Повторить».' );
);
}
await new Promise((resolve) => setTimeout(resolve, 1200)); await new Promise((resolve) => setTimeout(resolve, 1200));
continue; continue;
} }
@@ -372,6 +370,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setBootstrapError('Не удалось подключиться к серверу API. Подождите несколько секунд и нажмите «Повторить».'); setBootstrapError('Не удалось подключиться к серверу API. Подождите несколько секунд и нажмите «Повторить».');
setIsApiReady(false); setIsApiReady(false);
setIsSessionReady(false); setIsSessionReady(false);
setIsLoading(false);
return; return;
} }
if (message) showToast(message); if (message) showToast(message);
@@ -632,6 +631,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
Boolean(hasStoredSession || token) && Boolean(hasStoredSession || token) &&
(isLoading || !isApiReady || !isSessionReady); (isLoading || !isApiReady || !isSessionReady);
React.useEffect(() => {
if (!isBootstrapping || bootstrapError) return;
const timer = window.setTimeout(() => {
setBootstrapError(
'Сервер API не отвечает. Если недавно перезапускали контейнеры — подождите 1020 секунд и нажмите «Повторить».'
);
}, 18000);
return () => window.clearTimeout(timer);
}, [bootstrapError, isBootstrapping]);
return ( return (
<AuthContext.Provider <AuthContext.Provider
value={{ value={{
@@ -661,16 +670,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
{isBootstrapping ? ( {isBootstrapping ? (
<AppBootstrapScreen <AppBootstrapScreen
error={bootstrapError} error={bootstrapError}
onRetry={ onRetry={() => {
bootstrapError setBootstrapError(null);
? () => { initialBootstrapDoneRef.current = false;
setBootstrapError(null); invalidateApiGatewayReady();
initialBootstrapDoneRef.current = false; setIsLoading(true);
invalidateApiGatewayReady(); setIsApiReady(false);
void refreshProfile(); setIsSessionReady(false);
} void refreshProfile();
: undefined }}
}
/> />
) : ( ) : (
children children

View File

@@ -684,7 +684,7 @@ export function buildSystemSettingPayload(setting: Pick<SystemSetting, 'key' | '
const GATEWAY_RETRY_STATUS = new Set([502, 503, 504]); const GATEWAY_RETRY_STATUS = new Set([502, 503, 504]);
const GATEWAY_RETRY_ATTEMPTS = 4; const GATEWAY_RETRY_ATTEMPTS = 4;
const GATEWAY_STABLE_PROBE_INTERVAL_MS = 800; const GATEWAY_STABLE_PROBE_INTERVAL_MS = 800;
const GATEWAY_STABLE_MAX_ROUNDS = 12; const GATEWAY_STABLE_MAX_ROUNDS = 8;
const API_MAX_CONCURRENT = 6; const API_MAX_CONCURRENT = 6;
const API_BURST_MAX_CONCURRENT = 2; const API_BURST_MAX_CONCURRENT = 2;
const API_BURST_WINDOW_MS = 5000; const API_BURST_WINDOW_MS = 5000;

View File

@@ -428,12 +428,42 @@ sync_nginx_after_api_gateway_restart() {
recreate_docker_nginx() { recreate_docker_nginx() {
compose_uses_proxy_profile || return 0 compose_uses_proxy_profile || return 0
ensure_nginx_gateway_watch_script
build_compose_stack_cmd build_compose_stack_cmd
log "Пересоздание контейнера lendry-id-nginx (подхват конфигов из ./nginx/conf.d)..." log "Пересоздание контейнера lendry-id-nginx (подхват конфигов из ./nginx/conf.d)..."
docker_cmd "${COMPOSE_STACK_CMD[@]}" up -d --force-recreate nginx docker_cmd "${COMPOSE_STACK_CMD[@]}" up -d --force-recreate nginx
wait_for_service "nginx" 60 wait_for_service "nginx" 60
} }
ensure_nginx_gateway_watch_script() {
local watch_script="${ROOT_DIR}/infra/nginx/99-idp-gateway-watch.sh"
[[ -f "$watch_script" ]] || return 0
chmod +x "$watch_script" 2>/dev/null || true
}
# Проба /idp-api/health изнутри nginx (HTTPS при SSL — иначе HTTP даёт 301 на :80).
nginx_probe_idp_api_health_from_container() {
local frontend_domain="$1"
local use_ssl ssl_type
load_env
ssl_type="$(env_get SSL_TYPE none)"
use_ssl="$(env_get USE_NGINX_SSL false)"
build_compose_stack_cmd
if [[ "$use_ssl" == "true" || "$ssl_type" != "none" ]]; then
docker_cmd "${COMPOSE_STACK_CMD[@]}" exec -T nginx \
wget -qO- --timeout=6 --no-check-certificate \
--header="Host: ${frontend_domain}" \
"https://127.0.0.1/idp-api/health" 2>/dev/null || true
return 0
fi
docker_cmd "${COMPOSE_STACK_CMD[@]}" exec -T nginx \
wget -qO- --timeout=6 \
--header="Host: ${frontend_domain}" \
"http://127.0.0.1/idp-api/health" 2>/dev/null || true
}
post_deploy_proxy_verify() { post_deploy_proxy_verify() {
local strict="${1:-0}" local strict="${1:-0}"
compose_uses_proxy_profile || return 0 compose_uses_proxy_profile || return 0
@@ -483,6 +513,7 @@ deploy_phased_stack() {
wait_for_service "docs" 60 wait_for_service "docs" 60
log "Фаза 5/5: Docker Nginx (после готовности backend)..." log "Фаза 5/5: Docker Nginx (после готовности backend)..."
ensure_nginx_gateway_watch_script
compose_build_and_up nginx compose_build_and_up nginx
post_deploy_proxy_verify 1 post_deploy_proxy_verify 1
} }
@@ -2062,61 +2093,41 @@ nginx_proxy_headers=" proxy_set_header Host \$host;
proxy_set_header X-Forwarded-Proto \$scheme; proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header X-Forwarded-Host \$host; proxy_set_header X-Forwarded-Host \$host;
proxy_set_header Authorization \$http_authorization; proxy_set_header Authorization \$http_authorization;
proxy_set_header Connection \"close\";
proxy_read_timeout 300s; proxy_read_timeout 300s;
proxy_connect_timeout 75s;" proxy_connect_timeout 75s;"
# Встроенный DNS Docker. valid=1s — быстрее подхватывает новый IP после recreate api-gateway. # API upstream: keepalive вместо Connection: close — иначе лавина параллельных запросов SPA даёт 502.
NGINX_DOCKER_RESOLVER=" resolver 127.0.0.11 ipv6=off valid=1s; nginx_proxy_headers_api=" proxy_set_header Host \$host;
resolver_timeout 3s;" proxy_set_header X-Real-IP \$remote_addr;
NGINX_DOCKER_SERVER_DNS=" resolver 127.0.0.11 ipv6=off valid=1s; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
resolver_timeout 3s;" proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header X-Forwarded-Host \$host;
# Имя nginx-переменной из ключа апстрима (только [A-Za-z0-9_]). proxy_set_header Authorization \$http_authorization;
nginx_upstream_var() { proxy_http_version 1.1;
printf 'idp_up_%s' "$(printf '%s' "$1" | tr -c 'A-Za-z0-9' '_')" proxy_set_header Connection \"\";
} proxy_read_timeout 300s;
proxy_connect_timeout 75s;"
# proxy_pass к КОРНЮ апстрима ($request_uri сохраняет путь и query). # proxy_pass к КОРНЮ апстрима ($request_uri сохраняет путь и query).
# docker: resolver + переменная (перерезолв DNS после recreate api-gateway). # docker/host: статический proxy_pass; после recreate api-gateway — nginx -s reload (sync_nginx_after_api_gateway_restart).
# host: статический proxy_pass на 127.0.0.1.
render_proxy_root() { render_proxy_root() {
local key="$1" up var local key="$1" up
up="$(nginx_upstream "$key")" up="$(nginx_upstream "$key")"
if [[ "$NGINX_MODE" == "docker" ]]; then printf ' proxy_pass %s$request_uri;' "$up"
var="$(nginx_upstream_var "$key")"
printf '%s\n set $%s %s;\n proxy_pass $%s$request_uri;' \
"$NGINX_DOCKER_RESOLVER" "$var" "$up" "$var"
else
printf ' proxy_pass %s;' "$up"
fi
} }
# proxy_pass с заменой пути на фиксированный ($2, например /ws или /oauth/). # proxy_pass с заменой пути на фиксированный ($2, например /ws или /oauth/).
# Используется там, где префикс локейшна нужно «срезать»/заменить.
render_proxy_rewrite() { render_proxy_rewrite() {
local key="$1" target="$2" up var local key="$1" target="$2" up
up="$(nginx_upstream "$key")" up="$(nginx_upstream "$key")"
if [[ "$NGINX_MODE" == "docker" ]]; then printf ' rewrite ^.*$ %s break;\n proxy_pass %s;' "$target" "$up"
var="$(nginx_upstream_var "$key")"
printf '%s\n set $%s %s;\n rewrite ^.*$ %s break;\n proxy_pass $%s;' \
"$NGINX_DOCKER_RESOLVER" "$var" "$up" "$target" "$var"
else
printf ' proxy_pass %s%s;' "$up" "$target"
fi
} }
# proxy_pass для /idp-api/ — prefix ^~ выше regex; rewrite снимает префикс перед api-gateway. # proxy_pass для /idp-api/ — prefix ^~ выше regex; rewrite снимает префикс перед api-gateway.
render_proxy_idp_api() { render_proxy_idp_api() {
local key="api" up var local up
up="$(nginx_upstream "$key")" up="$(nginx_upstream api)"
if [[ "$NGINX_MODE" == "docker" ]]; then printf ' rewrite ^/idp-api/?(.*)$ /$1 break;\n proxy_pass %s;' "$up"
var="$(nginx_upstream_var "$key")"
printf '%s\n set $%s %s;\n rewrite ^/idp-api/?(.*)$ /$1 break;\n proxy_pass $%s;' \
"$NGINX_DOCKER_RESOLVER" "$var" "$up" "$var"
else
printf ' rewrite ^/idp-api/?(.*)$ /$1 break;\n proxy_pass %s;' "$up"
fi
} }
nginx_write_conf() { nginx_write_conf() {
@@ -2231,8 +2242,7 @@ build_nginx_api_location_block() {
cat <<EOF cat <<EOF
location / { location / {
${proxy} ${proxy}
proxy_http_version 1.1; ${nginx_proxy_headers_api}
${nginx_proxy_headers}
} }
EOF EOF
return 0 return 0
@@ -2312,33 +2322,29 @@ ${proxy_ws}
location ^~ /idp-api/ { location ^~ /idp-api/ {
${proxy_idp} ${proxy_idp}
proxy_http_version 1.1; ${nginx_proxy_headers_api}
${nginx_proxy_headers}
} }
location /oauth/ { location /oauth/ {
${proxy_oauth} ${proxy_oauth}
proxy_http_version 1.1; ${nginx_proxy_headers_api}
${nginx_proxy_headers}
} }
location /.well-known/ { location /.well-known/ {
${proxy_wk} ${proxy_wk}
proxy_http_version 1.1; ${nginx_proxy_headers_api}
${nginx_proxy_headers}
} }
location /fedcm/ { location /fedcm/ {
${proxy_fedcm} ${proxy_fedcm}
proxy_http_version 1.1; ${nginx_proxy_headers_api}
${nginx_proxy_headers}
} }
EOF EOF
} }
nginx_docker_server_dns_block() { nginx_docker_server_dns_block() {
[[ "$NGINX_MODE" == "docker" ]] && echo "$NGINX_DOCKER_SERVER_DNS" || true return 0
} }
write_nginx_site_frontend() { write_nginx_site_frontend() {
@@ -2618,13 +2624,10 @@ validate_docker_api_nginx_config() {
fail "Конфиг ${conf} содержит legacy upstream{} — выполните ./install.sh --fix-all" fail "Конфиг ${conf} содержит legacy upstream{} — выполните ./install.sh --fix-all"
fi fi
if ! grep -q 'resolver 127.0.0.11' "$conf" 2>/dev/null; then if ! grep -q 'lendry-id-api-gateway:3000' "$conf" 2>/dev/null; then
fail "Конфиг ${conf} без resolver 127.0.0.11 — API-домен не переживёт пересборку api-gateway" fail "Конфиг ${conf} без proxy_pass на lendry-id-api-gateway:3000 — выполните ./install.sh --fix-all"
fi fi
if ! grep -q 'idp_up_api' "$conf" 2>/dev/null; then ok "Конфиг API-домена: proxy_pass → lendry-id-api-gateway:3000"
fail "Конфиг ${conf} без динамического proxy_pass — выполните ./install.sh --fix-all"
fi
ok "Конфиг API-домена: динамический DNS (resolver + idp_up_api)"
} }
validate_docker_frontend_nginx_config() { validate_docker_frontend_nginx_config() {
@@ -2636,16 +2639,13 @@ validate_docker_frontend_nginx_config() {
fail "Конфиг ${conf} без location /idp-api — браузер SPA получит 502. Выполните ./install.sh --fix-all" fail "Конфиг ${conf} без location /idp-api — браузер SPA получит 502. Выполните ./install.sh --fix-all"
fi fi
if ! grep -q 'resolver 127.0.0.11' "$conf" 2>/dev/null; then if ! grep -q 'lendry-id-api-gateway:3000' "$conf" 2>/dev/null; then
fail "Конфиг ${conf} без resolver 127.0.0.11 — /idp-api на SSO не переживёт пересборку api-gateway" fail "Конфиг ${conf} без proxy_pass на lendry-id-api-gateway:3000 — выполните ./install.sh --fix-all"
fi
if ! grep -q 'idp_up_api' "$conf" 2>/dev/null; then
fail "Конфиг ${conf} без idp_up_api — /idp-api проксируется некорректно. Выполните ./install.sh --fix-all"
fi fi
if ! grep -q 'location \^~ /idp-api/ws' "$conf" 2>/dev/null; then if ! grep -q 'location \^~ /idp-api/ws' "$conf" 2>/dev/null; then
warn "Конфиг ${conf}: location ^~ /idp-api/ws отсутствует — WebSocket может идти на api-gateway вместо media-ws" warn "Конфиг ${conf}: location ^~ /idp-api/ws отсутствует — WebSocket может идти на api-gateway вместо media-ws"
fi fi
ok "Конфиг SSO-домена: /idp-api с динамическим DNS (resolver + idp_up_api)" ok "Конфиг SSO-домена: /idp-api → lendry-id-api-gateway:3000"
} }
verify_nginx_reaches_api_gateway() { verify_nginx_reaches_api_gateway() {
@@ -2679,15 +2679,13 @@ verify_nginx_reaches_api_gateway() {
# Тот же путь, что в браузере: location ^~ /idp-api/ + rewrite + proxy_pass $idp_up_api # Тот же путь, что в браузере: location ^~ /idp-api/ + rewrite + proxy_pass $idp_up_api
verify_nginx_idp_api_location() { verify_nginx_idp_api_location() {
docker_nginx_container_running || return 0 docker_nginx_container_running || return 0
build_compose_stack_cmd
load_env load_env
local frontend_domain body code local frontend_domain body code ssl_type
frontend_domain="$(env_get DOMAIN_FRONTEND "")" frontend_domain="$(env_get DOMAIN_FRONTEND "")"
[[ -n "$frontend_domain" ]] || return 0 [[ -n "$frontend_domain" ]] || return 0
ssl_type="$(env_get SSL_TYPE none)"
body="$(docker_cmd "${COMPOSE_STACK_CMD[@]}" exec -T nginx \ body="$(nginx_probe_idp_api_health_from_container "$frontend_domain")"
wget -qO- --timeout=6 --header="Host: ${frontend_domain}" \
"http://127.0.0.1/idp-api/health" 2>/dev/null || true)"
if [[ "$body" == *'"status":"ok"'* ]] || [[ "$body" == *'"status": "ok"'* ]]; then if [[ "$body" == *'"status":"ok"'* ]] || [[ "$body" == *'"status": "ok"'* ]]; then
ok "Docker Nginx location /idp-api/ → api-gateway OK (Host: ${frontend_domain})" ok "Docker Nginx location /idp-api/ → api-gateway OK (Host: ${frontend_domain})"
return 0 return 0
@@ -2695,16 +2693,26 @@ verify_nginx_idp_api_location() {
reload_docker_nginx reload_docker_nginx
sleep 2 sleep 2
body="$(docker_cmd "${COMPOSE_STACK_CMD[@]}" exec -T nginx \ body="$(nginx_probe_idp_api_health_from_container "$frontend_domain")"
wget -qO- --timeout=6 --header="Host: ${frontend_domain}" \
"http://127.0.0.1/idp-api/health" 2>/dev/null || true)"
if [[ "$body" == *'"status":"ok"'* ]] || [[ "$body" == *'"status": "ok"'* ]]; then if [[ "$body" == *'"status":"ok"'* ]] || [[ "$body" == *'"status": "ok"'* ]]; then
ok "Docker Nginx location /idp-api/ → api-gateway OK после reload" ok "Docker Nginx location /idp-api/ → api-gateway OK после reload"
return 0 return 0
fi fi
warn "Nginx location /idp-api/ не проксирует на api-gateway (Host: ${frontend_domain})" # Fallback: проверка с хоста (как curl из браузера / DevTools)
warn "Проверьте: grep -E 'idp-api|idp_up_api|resolver' nginx/conf.d/${NGINX_PREFIX}-frontend.conf" if [[ "$(env_get USE_NGINX_SSL false)" == "true" || "$ssl_type" != "none" ]]; then
code="$(curl -sk --max-time 6 -o /dev/null -w '%{http_code}' "https://${frontend_domain}/idp-api/health" 2>/dev/null || echo 000)"
else
code="$(curl -fsS --max-time 6 -o /dev/null -w '%{http_code}' "http://${frontend_domain}/idp-api/health" 2>/dev/null || echo 000)"
fi
if [[ "$code" == "200" ]]; then
ok "SSO /idp-api/health → 200 (проверка с хоста; внутри контейнера — см. SSL/301)"
return 0
fi
warn "Nginx location /idp-api/ не проксирует на api-gateway (Host: ${frontend_domain}, HTTP ${code:-?})"
warn "Проверьте: curl -sk https://${frontend_domain}/idp-api/health"
warn " и: docker compose --env-file .env --profile proxy exec nginx wget -qO- --no-check-certificate --header=\"Host: ${frontend_domain}\" https://127.0.0.1/idp-api/health"
return 1 return 1
} }
@@ -3219,8 +3227,8 @@ diagnose_common_issues() {
elif grep -q '172\.28\.50\.10:3000' "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-frontend.conf" 2>/dev/null; then elif grep -q '172\.28\.50\.10:3000' "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-frontend.conf" 2>/dev/null; then
echo -e " ${RED}${NC} Устаревший конфиг (статический IP 172.28.50.10) — выполните ./install.sh --fix-all" echo -e " ${RED}${NC} Устаревший конфиг (статический IP 172.28.50.10) — выполните ./install.sh --fix-all"
issues=$((issues + 1)) issues=$((issues + 1))
elif grep -q 'idp_up_api' "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-frontend.conf" 2>/dev/null; then elif grep -q 'lendry-id-api-gateway:3000' "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-frontend.conf" 2>/dev/null; then
echo -e " ${GREEN}${NC} Nginx /idp-api: динамический DNS (resolver + idp_up_api → lendry-id-api-gateway)" echo -e " ${GREEN}${NC} Nginx /idp-api: proxy_pass → lendry-id-api-gateway:3000"
else else
echo -e " ${YELLOW}!${NC} Конфиг SSO без /idp-api proxy — выполните ./install.sh --fix-all" echo -e " ${YELLOW}!${NC} Конфиг SSO без /idp-api proxy — выполните ./install.sh --fix-all"
issues=$((issues + 1)) issues=$((issues + 1))