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>
{onRetry ? (
<Button className="mt-6 rounded-xl" onClick={onRetry}>
Повторить подключение
</Button>
) : null}
</>
) : (
<>
@@ -33,6 +28,11 @@ export function AppBootstrapScreen({
</p>
</>
)}
{onRetry ? (
<Button className="mt-6 rounded-xl" variant={error ? 'default' : 'secondary'} onClick={onRetry}>
Повторить подключение
</Button>
) : null}
</div>
);
}

View File

@@ -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). Подождите несколько секунд и нажмите «Повторить».'
);
}
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 не отвечает. Если недавно перезапускали контейнеры — подождите 1020 секунд и нажмите «Повторить».'
);
}, 18000);
return () => window.clearTimeout(timer);
}, [bootstrapError, isBootstrapping]);
return (
<AuthContext.Provider
value={{
@@ -661,16 +670,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
{isBootstrapping ? (
<AppBootstrapScreen
error={bootstrapError}
onRetry={
bootstrapError
? () => {
onRetry={() => {
setBootstrapError(null);
initialBootstrapDoneRef.current = false;
invalidateApiGatewayReady();
setIsLoading(true);
setIsApiReady(false);
setIsSessionReady(false);
void refreshProfile();
}
: undefined
}
}}
/>
) : (
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_ATTEMPTS = 4;
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_BURST_MAX_CONCURRENT = 2;
const API_BURST_WINDOW_MS = 5000;

View File

@@ -428,12 +428,42 @@ sync_nginx_after_api_gateway_restart() {
recreate_docker_nginx() {
compose_uses_proxy_profile || return 0
ensure_nginx_gateway_watch_script
build_compose_stack_cmd
log "Пересоздание контейнера lendry-id-nginx (подхват конфигов из ./nginx/conf.d)..."
docker_cmd "${COMPOSE_STACK_CMD[@]}" up -d --force-recreate nginx
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() {
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
local up
up="$(nginx_upstream api)"
printf ' rewrite ^/idp-api/?(.*)$ /$1 break;\n proxy_pass %s;' "$up"
fi
}
nginx_write_conf() {
@@ -2231,8 +2242,7 @@ build_nginx_api_location_block() {
cat <<EOF
location / {
${proxy}
proxy_http_version 1.1;
${nginx_proxy_headers}
${nginx_proxy_headers_api}
}
EOF
return 0
@@ -2312,33 +2322,29 @@ ${proxy_ws}
location ^~ /idp-api/ {
${proxy_idp}
proxy_http_version 1.1;
${nginx_proxy_headers}
${nginx_proxy_headers_api}
}
location /oauth/ {
${proxy_oauth}
proxy_http_version 1.1;
${nginx_proxy_headers}
${nginx_proxy_headers_api}
}
location /.well-known/ {
${proxy_wk}
proxy_http_version 1.1;
${nginx_proxy_headers}
${nginx_proxy_headers_api}
}
location /fedcm/ {
${proxy_fedcm}
proxy_http_version 1.1;
${nginx_proxy_headers}
${nginx_proxy_headers_api}
}
EOF
}
nginx_docker_server_dns_block() {
[[ "$NGINX_MODE" == "docker" ]] && echo "$NGINX_DOCKER_SERVER_DNS" || true
return 0
}
write_nginx_site_frontend() {
@@ -2618,13 +2624,10 @@ validate_docker_api_nginx_config() {
fail "Конфиг ${conf} содержит legacy upstream{} — выполните ./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 — 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))