fix and update
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 не отвечает. Если недавно перезапускали контейнеры — подождите 10–20 секунд и нажмите «Повторить».'
|
||||
);
|
||||
}, 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
|
||||
|
||||
@@ -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;
|
||||
|
||||
154
install.sh
154
install.sh
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user