fix and update

This commit is contained in:
lendry
2026-06-30 17:57:09 +03:00
parent 521de7ea00
commit 2ea790d21d
5 changed files with 68 additions and 46 deletions

View File

@@ -29,7 +29,6 @@ import {
setPinRequiredHandler, setPinRequiredHandler,
subscribeApiReady, subscribeApiReady,
subscribeApiNotReady, subscribeApiNotReady,
waitForStableGateway,
} from '@/lib/api'; } from '@/lib/api';
import { useToast } from './toast-provider'; import { useToast } from './toast-provider';
import { PinLockModal } from './pin-lock-modal'; import { PinLockModal } from './pin-lock-modal';
@@ -200,7 +199,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setIsApiReady(false); setIsApiReady(false);
try { try {
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;
@@ -230,14 +228,32 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
cacheUserProfile(auth.user); cacheUserProfile(auth.user);
setHasStoredSession(true); setHasStoredSession(true);
clearPinLock(); clearPinLock();
setBootstrapError(null);
initialBootstrapDoneRef.current = true;
setIsApiReady(true);
setIsSessionReady(true);
setIsLoading(false);
await warmUpAndApplySession(auth.accessToken); void fetchAuthSession(auth.accessToken)
.then((session) => {
applySessionState(session, { setUser, setToken, activatePinLock, clearPinLock });
})
.catch((error) => {
if (isPinRequiredError(error)) {
activatePinLock(error.sessionId ?? auth.sessionId);
return;
}
if (!isGatewayUnavailableError(error)) {
const message = getApiErrorMessage(error, 'Не удалось обновить профиль после входа');
if (message) showToast(message);
}
});
if (auth.pinVerified !== false) { if (auth.pinVerified !== false) {
scheduleFedcmSessionSync(auth.accessToken, 8000); scheduleFedcmSessionSync(auth.accessToken, 8000);
} }
}, },
[clearPinLock, warmUpAndApplySession] [activatePinLock, clearPinLock, showToast]
); );
const logout = React.useCallback(() => { const logout = React.useCallback(() => {
@@ -286,10 +302,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
try { try {
for (let bootstrapAttempt = 0; bootstrapAttempt < 4; bootstrapAttempt += 1) { for (let bootstrapAttempt = 0; bootstrapAttempt < 4; bootstrapAttempt += 1) {
try { try {
if (isInitialBootstrap) {
await waitForStableGateway(1);
}
if (currentToken) { if (currentToken) {
setToken(currentToken); setToken(currentToken);
try { try {
@@ -580,13 +592,29 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
window.localStorage.setItem(AUTH_SESSION_KEY, sessionId); window.localStorage.setItem(AUTH_SESSION_KEY, sessionId);
setToken(response.accessToken); setToken(response.accessToken);
await warmUpAndApplySession(response.accessToken); try {
await warmUpAndApplySession(response.accessToken);
} catch (error) {
if (!isGatewayUnavailableError(error)) {
throw error;
}
const cachedUser = readCachedUserProfile();
if (cachedUser) {
setUser(cachedUser);
}
clearPinLock();
setBootstrapError(null);
initialBootstrapDoneRef.current = true;
setIsApiReady(true);
setIsSessionReady(true);
setIsLoading(false);
}
if (pathname.startsWith('/auth/')) { if (pathname.startsWith('/auth/')) {
router.replace('/'); router.replace('/');
} }
}, },
[pathname, router, warmUpAndApplySession] [clearPinLock, pathname, router, warmUpAndApplySession]
); );
const handlePinUnlock = React.useCallback( const handlePinUnlock = React.useCallback(

View File

@@ -523,8 +523,6 @@ export async function refreshAuthSession(): Promise<RefreshSessionResponse> {
throw new ApiError('Refresh token недоступен на сервере', 401, 'NO_REFRESH'); throw new ApiError('Refresh token недоступен на сервере', 401, 'NO_REFRESH');
} }
await ensureApiGatewayReady();
const refreshToken = window.localStorage.getItem(AUTH_REFRESH_KEY); const refreshToken = window.localStorage.getItem(AUTH_REFRESH_KEY);
const sessionId = window.localStorage.getItem(AUTH_SESSION_KEY); const sessionId = window.localStorage.getItem(AUTH_SESSION_KEY);
if (!refreshToken) { if (!refreshToken) {
@@ -682,7 +680,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 = 6;
const GATEWAY_STABLE_PROBE_INTERVAL_MS = 800; const GATEWAY_STABLE_PROBE_INTERVAL_MS = 800;
const GATEWAY_STABLE_MAX_ROUNDS = 8; const GATEWAY_STABLE_MAX_ROUNDS = 8;
const API_MAX_CONCURRENT = 6; const API_MAX_CONCURRENT = 6;
@@ -872,10 +870,6 @@ async function fetchWithGatewayRetry(url: string, init: RequestInit): Promise<Re
} }
export async function apiFetch<T>(path: string, options: RequestInit = {}, token?: string | null, allowRetry = true): Promise<T> { export async function apiFetch<T>(path: string, options: RequestInit = {}, token?: string | null, allowRetry = true): Promise<T> {
if (typeof window !== 'undefined' && !gatewayReadyResolved && !path.startsWith('/health')) {
await ensureApiGatewayReady();
}
await acquireApiSlot(); await acquireApiSlot();
const resolvedToken = resolveAccessToken(token); const resolvedToken = resolveAccessToken(token);

View File

@@ -268,7 +268,7 @@ services:
container_name: lendry-id-nginx container_name: lendry-id-nginx
restart: unless-stopped restart: unless-stopped
environment: environment:
IDP_API_GATEWAY_HOST: lendry-id-api-gateway IDP_API_GATEWAY_HOST: api-gateway
IDP_API_GATEWAY_PORT: "3000" IDP_API_GATEWAY_PORT: "3000"
IDP_FRONTEND_DOMAIN: ${DOMAIN_FRONTEND:-} IDP_FRONTEND_DOMAIN: ${DOMAIN_FRONTEND:-}
ports: ports:
@@ -291,7 +291,7 @@ services:
test: test:
[ [
"CMD-SHELL", "CMD-SHELL",
"if [ -n \"$IDP_FRONTEND_DOMAIN\" ]; then wget -qO- --timeout=4 --no-check-certificate --header=\"Host: $IDP_FRONTEND_DOMAIN\" \"https://127.0.0.1/idp-api/health\" 2>/dev/null | grep -q '\"status\"' || wget -qO- --timeout=4 --header=\"Host: $IDP_FRONTEND_DOMAIN\" \"http://127.0.0.1/idp-api/health\" 2>/dev/null | grep -q '\"status\"' || exit 1; else wget -qO- --timeout=4 http://lendry-id-api-gateway:3000/health 2>/dev/null | grep -q '\"status\"' || exit 1; fi", "if [ -n \"$IDP_FRONTEND_DOMAIN\" ]; then wget -qO- --timeout=4 --no-check-certificate --header=\"Host: $IDP_FRONTEND_DOMAIN\" \"https://127.0.0.1/idp-api/health\" 2>/dev/null | grep -q '\"status\"' || wget -qO- --timeout=4 --header=\"Host: $IDP_FRONTEND_DOMAIN\" \"http://127.0.0.1/idp-api/health\" 2>/dev/null | grep -q '\"status\"' || exit 1; else wget -qO- --timeout=4 http://api-gateway:3000/health 2>/dev/null | grep -q '\"status\"' || exit 1; fi",
] ]
interval: 15s interval: 15s
timeout: 8s timeout: 8s

View File

@@ -4,7 +4,7 @@ set -eu
WATCH_INTERVAL="${IDP_NGINX_WATCH_INTERVAL:-5}" WATCH_INTERVAL="${IDP_NGINX_WATCH_INTERVAL:-5}"
WATCH_HOSTS="${IDP_NGINX_WATCH_HOSTS:-lendry-id-api-gateway:3000/health lendry-id-frontend:3000/}" WATCH_HOSTS="${IDP_NGINX_WATCH_HOSTS:-api-gateway:3000/health frontend:3000/}"
log() { log() {
echo "[idp-nginx-watch] $*" echo "[idp-nginx-watch] $*"

View File

@@ -1705,19 +1705,19 @@ nginx_upstream() {
case "$1" in case "$1" in
api) api)
if [[ "$NGINX_MODE" == "docker" ]]; then if [[ "$NGINX_MODE" == "docker" ]]; then
echo "http://lendry-id-api-gateway:3000" echo "http://api-gateway:3000"
else else
echo "http://127.0.0.1:3000" echo "http://127.0.0.1:3000"
fi fi
;; ;;
frontend) frontend)
[[ "$NGINX_MODE" == "docker" ]] && echo "http://lendry-id-frontend:3000" || echo "http://127.0.0.1:3002" [[ "$NGINX_MODE" == "docker" ]] && echo "http://frontend:3000" || echo "http://127.0.0.1:3002"
;; ;;
docs) docs)
[[ "$NGINX_MODE" == "docker" ]] && echo "http://lendry-id-docs:3000" || echo "http://127.0.0.1:3003" [[ "$NGINX_MODE" == "docker" ]] && echo "http://docs:3000" || echo "http://127.0.0.1:3003"
;; ;;
ws) ws)
[[ "$NGINX_MODE" == "docker" ]] && echo "http://lendry-id-media-ws:8085" || echo "http://127.0.0.1:8085" [[ "$NGINX_MODE" == "docker" ]] && echo "http://media-ws:8085" || echo "http://127.0.0.1:8085"
;; ;;
minio) minio)
[[ "$NGINX_MODE" == "docker" ]] && echo "http://minio:9000" || echo "http://127.0.0.1:9000" [[ "$NGINX_MODE" == "docker" ]] && echo "http://minio:9000" || echo "http://127.0.0.1:9000"
@@ -2684,13 +2684,13 @@ 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 'lendry-id-api-gateway:3000' "$conf" 2>/dev/null; then if ! grep -q 'api-gateway:3000' "$conf" 2>/dev/null; then
fail "Конфиг ${conf} без proxy_pass на lendry-id-api-gateway:3000 — выполните ./install.sh --fix-all" fail "Конфиг ${conf} без proxy_pass на api-gateway:3000 — выполните ./install.sh --fix-all"
fi fi
if grep -q 'proxy_pass.*\$request_uri' "$conf" 2>/dev/null; then if grep -q 'proxy_pass.*\$request_uri' "$conf" 2>/dev/null; then
fail "Конфиг ${conf} содержит proxy_pass \$request_uri — нужен ./install.sh --fix-all (502 на / без resolver)" fail "Конфиг ${conf} содержит proxy_pass \$request_uri — нужен ./install.sh --fix-all (502 на / без resolver)"
fi fi
ok "Конфиг API-домена: proxy_pass → lendry-id-api-gateway:3000" ok "Конфиг API-домена: proxy_pass → api-gateway:3000"
} }
validate_docker_frontend_nginx_config() { validate_docker_frontend_nginx_config() {
@@ -2702,13 +2702,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 'lendry-id-api-gateway:3000' "$conf" 2>/dev/null; then if ! grep -q 'api-gateway:3000' "$conf" 2>/dev/null; then
fail "Конфиг ${conf} без proxy_pass на lendry-id-api-gateway:3000 — выполните ./install.sh --fix-all" fail "Конфиг ${conf} без proxy_pass на api-gateway:3000 — выполните ./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 → lendry-id-api-gateway:3000" ok "Конфиг SSO-домена: /idp-api → api-gateway:3000"
} }
validate_all_docker_nginx_proxy_pass() { validate_all_docker_nginx_proxy_pass() {
@@ -2735,9 +2735,9 @@ verify_nginx_reaches_api_gateway() {
while [[ $attempt -lt $max_attempts ]]; do while [[ $attempt -lt $max_attempts ]]; do
gw_ip="$(docker_cmd inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' lendry-id-api-gateway 2>/dev/null || true)" gw_ip="$(docker_cmd inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' lendry-id-api-gateway 2>/dev/null || true)"
body="$(docker_cmd "${COMPOSE_STACK_CMD[@]}" exec -T nginx \ body="$(docker_cmd "${COMPOSE_STACK_CMD[@]}" exec -T nginx \
wget -qO- --timeout=5 http://lendry-id-api-gateway:3000/health 2>/dev/null || true)" wget -qO- --timeout=5 http://api-gateway:3000/health 2>/dev/null || true)"
if [[ "$body" == *'"status":"ok"'* ]] || [[ "$body" == *'"status": "ok"'* ]]; then if [[ "$body" == *'"status":"ok"'* ]] || [[ "$body" == *'"status": "ok"'* ]]; then
ok "Docker Nginx → lendry-id-api-gateway:3000/health OK (IP api-gateway: ${gw_ip:-?})" ok "Docker Nginx → api-gateway:3000/health OK (IP api-gateway: ${gw_ip:-?})"
verify_nginx_idp_api_location || return 1 verify_nginx_idp_api_location || return 1
return 0 return 0
fi fi
@@ -2747,7 +2747,7 @@ verify_nginx_reaches_api_gateway() {
fi fi
done done
warn "Docker Nginx не достучался до lendry-id-api-gateway:3000/health (IP: ${gw_ip:-?}, попыток: ${max_attempts})" warn "Docker Nginx не достучался до api-gateway:3000/health (IP: ${gw_ip:-?}, попыток: ${max_attempts})"
warn "Проверьте: docker compose --profile proxy ps api-gateway nginx && docker compose logs --tail=40 api-gateway nginx" warn "Проверьте: docker compose --profile proxy ps api-gateway nginx && docker compose logs --tail=40 api-gateway nginx"
return 1 return 1
} }
@@ -2806,9 +2806,9 @@ verify_nginx_reaches_frontend() {
while [[ $attempt -lt $max_attempts ]]; do while [[ $attempt -lt $max_attempts ]]; do
fe_ip="$(docker_cmd inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' lendry-id-frontend 2>/dev/null || true)" fe_ip="$(docker_cmd inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' lendry-id-frontend 2>/dev/null || true)"
body="$(docker_cmd "${COMPOSE_STACK_CMD[@]}" exec -T nginx \ body="$(docker_cmd "${COMPOSE_STACK_CMD[@]}" exec -T nginx \
wget -qO- --timeout=6 http://lendry-id-frontend:3000/ 2>/dev/null | head -c 256 || true)" wget -qO- --timeout=6 http://frontend:3000/ 2>/dev/null | head -c 256 || true)"
if [[ -n "$body" ]]; then if [[ -n "$body" ]]; then
ok "Docker Nginx → lendry-id-frontend:3000/ OK (IP frontend: ${fe_ip:-?})" ok "Docker Nginx → frontend:3000/ OK (IP frontend: ${fe_ip:-?})"
if [[ -n "$frontend_domain" ]]; then if [[ -n "$frontend_domain" ]]; then
verify_nginx_frontend_root_location "$frontend_domain" || return 1 verify_nginx_frontend_root_location "$frontend_domain" || return 1
fi fi
@@ -2820,7 +2820,7 @@ verify_nginx_reaches_frontend() {
fi fi
done done
warn "Docker Nginx не достучался до lendry-id-frontend:3000/ (IP: ${fe_ip:-?}, попыток: ${max_attempts})" warn "Docker Nginx не достучался до frontend:3000/ (IP: ${fe_ip:-?}, попыток: ${max_attempts})"
warn "Проверьте: docker compose --env-file .env --profile proxy logs --tail=40 frontend nginx" warn "Проверьте: docker compose --env-file .env --profile proxy logs --tail=40 frontend nginx"
return 1 return 1
} }
@@ -2873,7 +2873,7 @@ check_sso_frontend_root() {
return 0 return 0
elif [[ "$code" == "502" ]]; then elif [[ "$code" == "502" ]]; then
echo -e " ${RED}${NC} SSO / → 502 — Nginx не проксирует / на frontend (502 Bad Gateway в браузере)" echo -e " ${RED}${NC} SSO / → 502 — Nginx не проксирует / на frontend (502 Bad Gateway в браузере)"
echo " Проверьте: docker compose --env-file .env --profile proxy exec nginx wget -qO- http://lendry-id-frontend:3000/" echo " Проверьте: docker compose --env-file .env --profile proxy exec nginx wget -qO- http://frontend:3000/"
echo " Решение: docker compose --env-file .env --profile proxy restart nginx frontend" echo " Решение: docker compose --env-file .env --profile proxy restart nginx frontend"
return 1 return 1
else else
@@ -2887,12 +2887,12 @@ verify_nginx_reaches_media_ws() {
build_compose_stack_cmd build_compose_stack_cmd
local body local body
body="$(docker_cmd "${COMPOSE_STACK_CMD[@]}" exec -T nginx \ body="$(docker_cmd "${COMPOSE_STACK_CMD[@]}" exec -T nginx \
wget -qO- --timeout=5 http://lendry-id-media-ws:8085/health 2>/dev/null || true)" wget -qO- --timeout=5 http://media-ws:8085/health 2>/dev/null || true)"
if [[ "$body" == *ok* ]] || [[ "$body" == *healthy* ]]; then if [[ "$body" == *ok* ]] || [[ "$body" == *healthy* ]]; then
ok "Docker Nginx → lendry-id-media-ws:8085/health OK" ok "Docker Nginx → media-ws:8085/health OK"
return 0 return 0
fi fi
warn "Docker Nginx не достучался до lendry-id-media-ws:8085" warn "Docker Nginx не достучался до media-ws:8085"
warn "Проверьте: docker compose ps media-ws && docker compose logs --tail=40 media-ws" warn "Проверьте: docker compose ps media-ws && docker compose logs --tail=40 media-ws"
return 1 return 1
} }
@@ -2906,12 +2906,12 @@ check_sso_idp_api_proxy() {
if docker_nginx_container_running; then if docker_nginx_container_running; then
body="$(docker_cmd exec lendry-id-frontend \ body="$(docker_cmd exec lendry-id-frontend \
wget -qO- --timeout=5 http://lendry-id-api-gateway:3000/health 2>/dev/null || true)" wget -qO- --timeout=5 http://api-gateway:3000/health 2>/dev/null || true)"
if [[ "$body" == *'"status":"ok"'* ]] || [[ "$body" == *'"status": "ok"'* ]]; then if [[ "$body" == *'"status":"ok"'* ]] || [[ "$body" == *'"status": "ok"'* ]]; then
echo -e " ${GREEN}${NC} frontend → api-gateway:3000/health OK (fallback через Next.js rewrites)" echo -e " ${GREEN}${NC} frontend → api-gateway:3000/health OK (fallback через Next.js rewrites)"
else else
echo -e " ${RED}${NC} frontend → api-gateway:3000 недоступен (Next.js rewrites тоже дадут 502)" echo -e " ${RED}${NC} frontend → api-gateway:3000 недоступен (Next.js rewrites тоже дадут 502)"
echo " Проверьте: docker compose exec frontend wget -qO- http://lendry-id-api-gateway:3000/health" echo " Проверьте: docker compose exec frontend wget -qO- http://api-gateway:3000/health"
return 1 return 1
fi fi
fi fi
@@ -2928,7 +2928,7 @@ check_sso_idp_api_proxy() {
elif [[ "$code" == "502" ]]; then elif [[ "$code" == "502" ]]; then
echo -e " ${RED}${NC} SSO /idp-api/health → 502 — Nginx не проксирует /idp-api на api-gateway" echo -e " ${RED}${NC} SSO /idp-api/health → 502 — Nginx не проксирует /idp-api на api-gateway"
echo " Проверьте: grep -E 'idp-api|idp_up_api|resolver' nginx/conf.d/${NGINX_PREFIX}-frontend.conf" echo " Проверьте: grep -E 'idp-api|idp_up_api|resolver' nginx/conf.d/${NGINX_PREFIX}-frontend.conf"
echo " docker compose --profile proxy exec nginx wget -qO- http://lendry-id-api-gateway:3000/health" echo " docker compose --profile proxy exec nginx wget -qO- http://api-gateway:3000/health"
return 1 return 1
else else
echo -e " ${YELLOW}!${NC} SSO /idp-api/health → HTTP ${code} (ожидалось 200)" echo -e " ${YELLOW}!${NC} SSO /idp-api/health → HTTP ${code} (ожидалось 200)"
@@ -3353,7 +3353,7 @@ check_https_api_with_origin() {
elif [[ "$code" == "502" ]]; then elif [[ "$code" == "502" ]]; then
echo -e " ${RED}${NC} HTTPS API preflight (OPTIONS) → 502 — Nginx не достучался до api-gateway" echo -e " ${RED}${NC} HTTPS API preflight (OPTIONS) → 502 — Nginx не достучался до api-gateway"
echo " Проверьте: grep resolver nginx/conf.d/lendry-id-api.conf" echo " Проверьте: grep resolver nginx/conf.d/lendry-id-api.conf"
echo " docker compose --profile proxy exec nginx wget -qO- http://lendry-id-api-gateway:3000/health" echo " docker compose --profile proxy exec nginx wget -qO- http://api-gateway:3000/health"
return 1 return 1
else else
echo -e " ${YELLOW}!${NC} HTTPS API preflight (OPTIONS) → HTTP ${code} (ожидалось 204)" echo -e " ${YELLOW}!${NC} HTTPS API preflight (OPTIONS) → HTTP ${code} (ожидалось 204)"
@@ -3435,8 +3435,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 'lendry-id-api-gateway:3000' "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-frontend.conf" 2>/dev/null; then elif grep -q '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" echo -e " ${GREEN}${NC} Nginx /idp-api: proxy_pass → 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))