From 31251be877967261809650a61dd01f427ce60450 Mon Sep 17 00:00:00 2001 From: lendry Date: Tue, 30 Jun 2026 16:42:48 +0300 Subject: [PATCH] fix and update --- .../components/id/app-bootstrap-screen.tsx | 10 +- apps/frontend/components/id/auth-provider.tsx | 42 +++-- apps/frontend/lib/api.ts | 2 +- install.sh | 156 +++++++++--------- 4 files changed, 113 insertions(+), 97 deletions(-) diff --git a/apps/frontend/components/id/app-bootstrap-screen.tsx b/apps/frontend/components/id/app-bootstrap-screen.tsx index 630ec46..182f851 100644 --- a/apps/frontend/components/id/app-bootstrap-screen.tsx +++ b/apps/frontend/components/id/app-bootstrap-screen.tsx @@ -18,11 +18,6 @@ export function AppBootstrapScreen({

Сервер авторизации временно недоступен. Обычно помогает повтор через несколько секунд.

- {onRetry ? ( - - ) : null} ) : ( <> @@ -33,6 +28,11 @@ export function AppBootstrapScreen({

)} + {onRetry ? ( + + ) : null} ); } diff --git a/apps/frontend/components/id/auth-provider.tsx b/apps/frontend/components/id/auth-provider.tsx index d922bdf..42151fc 100644 --- a/apps/frontend/components/id/auth-provider.tsx +++ b/apps/frontend/components/id/auth-provider.tsx @@ -200,7 +200,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setIsApiReady(false); try { - await waitForStableGateway(3); + await waitForStableGateway(1); const session = await fetchAuthSession(accessToken); applySessionState(session, { setUser, setToken, activatePinLock, clearPinLock }); initialBootstrapDoneRef.current = true; @@ -287,7 +287,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { for (let bootstrapAttempt = 0; bootstrapAttempt < 4; bootstrapAttempt += 1) { try { if (isInitialBootstrap) { - await waitForStableGateway(3); + await waitForStableGateway(1); } if (currentToken) { @@ -343,11 +343,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } catch (error) { if (isGatewayUnavailableError(error) && isInitialBootstrap && bootstrapAttempt + 1 < 4) { invalidateApiGatewayReady(); - if (bootstrapAttempt >= 1) { - setBootstrapError( - 'Не удалось подключиться к серверу API (502). Подождите несколько секунд и нажмите «Повторить».' - ); - } + setBootstrapError( + 'Не удалось подключиться к серверу API (502). Подождите несколько секунд и нажмите «Повторить».' + ); await new Promise((resolve) => setTimeout(resolve, 1200)); continue; } @@ -372,6 +370,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setBootstrapError('Не удалось подключиться к серверу API. Подождите несколько секунд и нажмите «Повторить».'); setIsApiReady(false); setIsSessionReady(false); + setIsLoading(false); return; } if (message) showToast(message); @@ -632,6 +631,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { Boolean(hasStoredSession || token) && (isLoading || !isApiReady || !isSessionReady); + React.useEffect(() => { + if (!isBootstrapping || bootstrapError) return; + const timer = window.setTimeout(() => { + setBootstrapError( + 'Сервер API не отвечает. Если недавно перезапускали контейнеры — подождите 10–20 секунд и нажмите «Повторить».' + ); + }, 18000); + return () => window.clearTimeout(timer); + }, [bootstrapError, isBootstrapping]); + return ( { - setBootstrapError(null); - initialBootstrapDoneRef.current = false; - invalidateApiGatewayReady(); - void refreshProfile(); - } - : undefined - } + onRetry={() => { + setBootstrapError(null); + initialBootstrapDoneRef.current = false; + invalidateApiGatewayReady(); + setIsLoading(true); + setIsApiReady(false); + setIsSessionReady(false); + void refreshProfile(); + }} /> ) : ( children diff --git a/apps/frontend/lib/api.ts b/apps/frontend/lib/api.ts index f527c27..ba0054c 100644 --- a/apps/frontend/lib/api.ts +++ b/apps/frontend/lib/api.ts @@ -684,7 +684,7 @@ export function buildSystemSettingPayload(setting: Pick/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() { local strict="${1:-0}" compose_uses_proxy_profile || return 0 @@ -483,6 +513,7 @@ deploy_phased_stack() { wait_for_service "docs" 60 log "Фаза 5/5: Docker Nginx (после готовности backend)..." + ensure_nginx_gateway_watch_script compose_build_and_up nginx 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-Host \$host; proxy_set_header Authorization \$http_authorization; - proxy_set_header Connection \"close\"; proxy_read_timeout 300s; proxy_connect_timeout 75s;" -# Встроенный DNS Docker. valid=1s — быстрее подхватывает новый IP после recreate api-gateway. -NGINX_DOCKER_RESOLVER=" resolver 127.0.0.11 ipv6=off valid=1s; - resolver_timeout 3s;" -NGINX_DOCKER_SERVER_DNS=" resolver 127.0.0.11 ipv6=off valid=1s; - resolver_timeout 3s;" - -# Имя nginx-переменной из ключа апстрима (только [A-Za-z0-9_]). -nginx_upstream_var() { - printf 'idp_up_%s' "$(printf '%s' "$1" | tr -c 'A-Za-z0-9' '_')" -} +# API upstream: keepalive вместо Connection: close — иначе лавина параллельных запросов SPA даёт 502. +nginx_proxy_headers_api=" proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_set_header X-Forwarded-Host \$host; + proxy_set_header Authorization \$http_authorization; + proxy_http_version 1.1; + proxy_set_header Connection \"\"; + proxy_read_timeout 300s; + proxy_connect_timeout 75s;" # proxy_pass к КОРНЮ апстрима ($request_uri сохраняет путь и query). -# docker: resolver + переменная (перерезолв DNS после recreate api-gateway). -# host: статический proxy_pass на 127.0.0.1. +# docker/host: статический proxy_pass; после recreate api-gateway — nginx -s reload (sync_nginx_after_api_gateway_restart). render_proxy_root() { - local key="$1" up var + local key="$1" up up="$(nginx_upstream "$key")" - if [[ "$NGINX_MODE" == "docker" ]]; then - 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 + printf ' proxy_pass %s$request_uri;' "$up" } # proxy_pass с заменой пути на фиксированный ($2, например /ws или /oauth/). -# Используется там, где префикс локейшна нужно «срезать»/заменить. render_proxy_rewrite() { - local key="$1" target="$2" up var + local key="$1" target="$2" up up="$(nginx_upstream "$key")" - if [[ "$NGINX_MODE" == "docker" ]]; then - 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 + printf ' rewrite ^.*$ %s break;\n proxy_pass %s;' "$target" "$up" } # proxy_pass для /idp-api/ — prefix ^~ выше regex; rewrite снимает префикс перед api-gateway. render_proxy_idp_api() { - local key="api" up var - up="$(nginx_upstream "$key")" - if [[ "$NGINX_MODE" == "docker" ]]; then - 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 + local up + up="$(nginx_upstream api)" + printf ' rewrite ^/idp-api/?(.*)$ /$1 break;\n proxy_pass %s;' "$up" } nginx_write_conf() { @@ -2231,8 +2242,7 @@ build_nginx_api_location_block() { cat </dev/null; then - fail "Конфиг ${conf} без resolver 127.0.0.11 — API-домен не переживёт пересборку api-gateway" + if ! grep -q 'lendry-id-api-gateway:3000' "$conf" 2>/dev/null; then + 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} без динамического proxy_pass — выполните ./install.sh --fix-all" - fi - ok "Конфиг API-домена: динамический DNS (resolver + idp_up_api)" + ok "Конфиг API-домена: proxy_pass → lendry-id-api-gateway:3000" } 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" fi - if ! grep -q 'resolver 127.0.0.11' "$conf" 2>/dev/null; then - fail "Конфиг ${conf} без resolver 127.0.0.11 — /idp-api на SSO не переживёт пересборку api-gateway" - fi - if ! grep -q 'idp_up_api' "$conf" 2>/dev/null; then - fail "Конфиг ${conf} без idp_up_api — /idp-api проксируется некорректно. Выполните ./install.sh --fix-all" + if ! grep -q 'lendry-id-api-gateway:3000' "$conf" 2>/dev/null; then + fail "Конфиг ${conf} без proxy_pass на lendry-id-api-gateway:3000 — выполните ./install.sh --fix-all" fi if ! grep -q 'location \^~ /idp-api/ws' "$conf" 2>/dev/null; then warn "Конфиг ${conf}: location ^~ /idp-api/ws отсутствует — WebSocket может идти на api-gateway вместо media-ws" fi - ok "Конфиг SSO-домена: /idp-api с динамическим DNS (resolver + idp_up_api)" + ok "Конфиг SSO-домена: /idp-api → lendry-id-api-gateway:3000" } verify_nginx_reaches_api_gateway() { @@ -2679,15 +2679,13 @@ verify_nginx_reaches_api_gateway() { # Тот же путь, что в браузере: location ^~ /idp-api/ + rewrite + proxy_pass $idp_up_api verify_nginx_idp_api_location() { docker_nginx_container_running || return 0 - build_compose_stack_cmd load_env - local frontend_domain body code + local frontend_domain body code ssl_type frontend_domain="$(env_get DOMAIN_FRONTEND "")" [[ -n "$frontend_domain" ]] || return 0 + ssl_type="$(env_get SSL_TYPE none)" - body="$(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)" + body="$(nginx_probe_idp_api_health_from_container "$frontend_domain")" if [[ "$body" == *'"status":"ok"'* ]] || [[ "$body" == *'"status": "ok"'* ]]; then ok "Docker Nginx location /idp-api/ → api-gateway OK (Host: ${frontend_domain})" return 0 @@ -2695,16 +2693,26 @@ verify_nginx_idp_api_location() { reload_docker_nginx sleep 2 - body="$(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)" + body="$(nginx_probe_idp_api_health_from_container "$frontend_domain")" if [[ "$body" == *'"status":"ok"'* ]] || [[ "$body" == *'"status": "ok"'* ]]; then ok "Docker Nginx location /idp-api/ → api-gateway OK после reload" return 0 fi - warn "Nginx location /idp-api/ не проксирует на api-gateway (Host: ${frontend_domain})" - warn "Проверьте: grep -E 'idp-api|idp_up_api|resolver' nginx/conf.d/${NGINX_PREFIX}-frontend.conf" + # Fallback: проверка с хоста (как curl из браузера / DevTools) + 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 } @@ -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 echo -e " ${RED}✘${NC} Устаревший конфиг (статический IP 172.28.50.10) — выполните ./install.sh --fix-all" issues=$((issues + 1)) - elif grep -q 'idp_up_api' "${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)" + 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: proxy_pass → lendry-id-api-gateway:3000" else echo -e " ${YELLOW}!${NC} Конфиг SSO без /idp-api proxy — выполните ./install.sh --fix-all" issues=$((issues + 1))