fix and update
This commit is contained in:
@@ -107,15 +107,13 @@ function applySessionState(
|
||||
|
||||
function readInitialAuthState() {
|
||||
if (typeof window === 'undefined') {
|
||||
return { token: null as string | null, hasStoredSession: false, cachedUser: null as PublicUser | null };
|
||||
return { token: null as string | null, hasStoredSession: false };
|
||||
}
|
||||
const token = window.localStorage.getItem(AUTH_TOKEN_KEY);
|
||||
const refresh = window.localStorage.getItem(AUTH_REFRESH_KEY);
|
||||
const cachedUser = readCachedUserProfile();
|
||||
return {
|
||||
token,
|
||||
hasStoredSession: Boolean(refresh || token),
|
||||
cachedUser
|
||||
hasStoredSession: Boolean(refresh || token)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,7 +123,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const { showToast } = useToast();
|
||||
const initialAuth = React.useMemo(() => readInitialAuthState(), []);
|
||||
const [token, setToken] = React.useState<string | null>(initialAuth.token);
|
||||
const [user, setUser] = React.useState<PublicUser | null>(initialAuth.cachedUser);
|
||||
const [user, setUser] = React.useState<PublicUser | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [isApiReady, setIsApiReady] = React.useState(false);
|
||||
const [isSessionReady, setIsSessionReady] = React.useState(false);
|
||||
@@ -269,8 +267,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
setHasStoredSession(Boolean(refreshToken));
|
||||
|
||||
if (!currentToken && !refreshToken) {
|
||||
window.localStorage.removeItem(AUTH_USER_CACHE_KEY);
|
||||
setUser(null);
|
||||
setToken(null);
|
||||
initialBootstrapDoneRef.current = true;
|
||||
setIsSessionReady(true);
|
||||
setIsApiReady(true);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -282,7 +284,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
try {
|
||||
for (let bootstrapAttempt = 0; bootstrapAttempt < 6; bootstrapAttempt += 1) {
|
||||
for (let bootstrapAttempt = 0; bootstrapAttempt < 4; bootstrapAttempt += 1) {
|
||||
try {
|
||||
if (isInitialBootstrap) {
|
||||
await waitForStableGateway(3);
|
||||
@@ -293,11 +295,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
try {
|
||||
const session = await fetchAuthSession(currentToken);
|
||||
applySessionState(session, { setUser, setToken, activatePinLock, clearPinLock });
|
||||
setIsApiReady(true);
|
||||
setIsSessionReady(true);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (isPinRequiredError(error)) {
|
||||
activatePinLock(error.sessionId ?? window.localStorage.getItem(AUTH_SESSION_KEY) ?? '');
|
||||
setIsApiReady(true);
|
||||
setIsSessionReady(true);
|
||||
return;
|
||||
}
|
||||
if (!(error instanceof ApiError) || error.code !== 'TOKEN_EXPIRED') {
|
||||
@@ -321,11 +326,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
cacheUserProfile(refreshed.user);
|
||||
}
|
||||
activatePinLock(refreshed.sessionId ?? '');
|
||||
setIsApiReady(true);
|
||||
setIsSessionReady(true);
|
||||
return;
|
||||
}
|
||||
if (refreshed.accessToken) {
|
||||
const session = await fetchAuthSession(refreshed.accessToken);
|
||||
applySessionState(session, { setUser, setToken, activatePinLock, clearPinLock });
|
||||
setIsApiReady(true);
|
||||
setIsSessionReady(true);
|
||||
return;
|
||||
}
|
||||
@@ -333,17 +341,26 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
throw new ApiError('Сессия недействительна', 401);
|
||||
} catch (error) {
|
||||
if (isGatewayUnavailableError(error) && isInitialBootstrap && bootstrapAttempt + 1 < 6) {
|
||||
if (isGatewayUnavailableError(error) && isInitialBootstrap && bootstrapAttempt + 1 < 4) {
|
||||
invalidateApiGatewayReady();
|
||||
if (bootstrapAttempt >= 1) {
|
||||
setBootstrapError(
|
||||
'Не удалось подключиться к серверу API (502). Подождите несколько секунд и нажмите «Повторить».'
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1200));
|
||||
continue;
|
||||
}
|
||||
if (isPinRequiredError(error)) {
|
||||
activatePinLock(error.sessionId ?? window.localStorage.getItem(AUTH_SESSION_KEY) ?? '');
|
||||
setIsApiReady(true);
|
||||
setIsSessionReady(true);
|
||||
return;
|
||||
}
|
||||
if (refreshToken && error instanceof ApiError && error.status === 403) {
|
||||
activatePinLock(window.localStorage.getItem(AUTH_SESSION_KEY) ?? '');
|
||||
setIsApiReady(true);
|
||||
setIsSessionReady(true);
|
||||
return;
|
||||
}
|
||||
if (error instanceof ApiError && (error.code === 'NO_REFRESH' || error.status === 401)) {
|
||||
@@ -612,7 +629,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const isBootstrapping =
|
||||
!isPinLocked &&
|
||||
Boolean(hasStoredSession || token || user) &&
|
||||
Boolean(hasStoredSession || token) &&
|
||||
(isLoading || !isApiReady || !isSessionReady);
|
||||
|
||||
return (
|
||||
@@ -649,6 +666,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
? () => {
|
||||
setBootstrapError(null);
|
||||
initialBootstrapDoneRef.current = false;
|
||||
invalidateApiGatewayReady();
|
||||
void refreshProfile();
|
||||
}
|
||||
: undefined
|
||||
|
||||
@@ -7,14 +7,17 @@ import { useAuth } from '@/components/id/auth-provider';
|
||||
export function useRequireAuth() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { user, isLoading, isApiReady, isSessionReady, isPinLocked, hasStoredSession } = useAuth();
|
||||
const { user, isLoading, isApiReady, isSessionReady, isPinLocked, hasStoredSession, token } = useAuth();
|
||||
|
||||
const authResolved = !isLoading && isSessionReady;
|
||||
const isAuthenticated = Boolean(user || isPinLocked);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || !isApiReady || !isSessionReady) return;
|
||||
if (user || isPinLocked || hasStoredSession) return;
|
||||
if (!authResolved) return;
|
||||
if (isAuthenticated || hasStoredSession || token) return;
|
||||
if (pathname.startsWith('/auth/')) return;
|
||||
router.replace('/auth/login');
|
||||
}, [hasStoredSession, isApiReady, isLoading, isPinLocked, isSessionReady, pathname, router, user]);
|
||||
}, [authResolved, hasStoredSession, isAuthenticated, pathname, router, token]);
|
||||
|
||||
return {
|
||||
user,
|
||||
@@ -22,6 +25,7 @@ export function useRequireAuth() {
|
||||
isPinLocked,
|
||||
isApiReady,
|
||||
isSessionReady,
|
||||
isReady: !isLoading && isApiReady && isSessionReady && Boolean(user || isPinLocked || hasStoredSession)
|
||||
authResolved,
|
||||
isReady: authResolved && isAuthenticated && isApiReady
|
||||
};
|
||||
}
|
||||
|
||||
@@ -449,7 +449,10 @@ export function isPinRequiredError(error: unknown): error is ApiError {
|
||||
}
|
||||
|
||||
export function isGatewayUnavailableError(error: unknown): boolean {
|
||||
return error instanceof ApiError && [502, 503, 504].includes(error.status);
|
||||
return (
|
||||
error instanceof ApiError &&
|
||||
(error.code === 'GATEWAY_UNAVAILABLE' || [502, 503, 504].includes(error.status))
|
||||
);
|
||||
}
|
||||
|
||||
export function getApiErrorMessage(error: unknown, fallback: string): string | null {
|
||||
@@ -681,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 = 18;
|
||||
const GATEWAY_STABLE_MAX_ROUNDS = 12;
|
||||
const API_MAX_CONCURRENT = 6;
|
||||
const API_BURST_MAX_CONCURRENT = 2;
|
||||
const API_BURST_WINDOW_MS = 5000;
|
||||
|
||||
@@ -267,12 +267,17 @@ services:
|
||||
image: nginx:1.27-alpine
|
||||
container_name: lendry-id-nginx
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
IDP_API_GATEWAY_HOST: lendry-id-api-gateway
|
||||
IDP_API_GATEWAY_PORT: "3000"
|
||||
IDP_FRONTEND_DOMAIN: ${DOMAIN_FRONTEND:-}
|
||||
ports:
|
||||
- "${NGINX_HTTP_PORT:-80}:80"
|
||||
- "${NGINX_HTTPS_PORT:-443}:443"
|
||||
volumes:
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
- ./nginx/certs:/etc/nginx/certs:ro
|
||||
- ./infra/nginx/99-idp-gateway-watch.sh:/docker-entrypoint.d/99-idp-gateway-watch.sh:ro
|
||||
depends_on:
|
||||
api-gateway:
|
||||
condition: service_healthy
|
||||
@@ -286,12 +291,12 @@ services:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"wget -qO- --timeout=3 http://lendry-id-api-gateway:3000/health 2>/dev/null | grep -q '\"status\"' || exit 1",
|
||||
"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",
|
||||
]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
timeout: 8s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
start_period: 45s
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
52
infra/nginx/99-idp-gateway-watch.sh
Normal file
52
infra/nginx/99-idp-gateway-watch.sh
Normal file
@@ -0,0 +1,52 @@
|
||||
#!/bin/sh
|
||||
# После recreate api-gateway Docker выдаёт новый IP; без reload nginx шлёт на старый адрес → 502.
|
||||
set -eu
|
||||
|
||||
GW_HOST="${IDP_API_GATEWAY_HOST:-lendry-id-api-gateway}"
|
||||
GW_PORT="${IDP_API_GATEWAY_PORT:-3000}"
|
||||
WATCH_INTERVAL="${IDP_NGINX_WATCH_INTERVAL:-5}"
|
||||
|
||||
log() {
|
||||
echo "[idp-nginx-watch] $*"
|
||||
}
|
||||
|
||||
wait_for_gateway() {
|
||||
attempt=0
|
||||
while [ "$attempt" -lt 120 ]; do
|
||||
if wget -qO- --timeout=3 "http://${GW_HOST}:${GW_PORT}/health" 2>/dev/null | grep -q '"status"'; then
|
||||
log "api-gateway доступен (${GW_HOST}:${GW_PORT})"
|
||||
return 0
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
sleep 1
|
||||
done
|
||||
log "WARN: api-gateway не ответил за 120с — nginx стартует без ожидания"
|
||||
return 0
|
||||
}
|
||||
|
||||
watch_gateway() {
|
||||
last_ip=""
|
||||
while true; do
|
||||
sleep "$WATCH_INTERVAL"
|
||||
current_ip="$(getent hosts "$GW_HOST" 2>/dev/null | awk '{print $1}' | head -n1 || true)"
|
||||
healthy=0
|
||||
if wget -qO- --timeout=3 "http://${GW_HOST}:${GW_PORT}/health" 2>/dev/null | grep -q '"status"'; then
|
||||
healthy=1
|
||||
fi
|
||||
if [ "$healthy" -eq 0 ]; then
|
||||
log "api-gateway недоступен — reload nginx"
|
||||
nginx -s reload 2>/dev/null || true
|
||||
continue
|
||||
fi
|
||||
if [ -n "$current_ip" ] && [ -n "$last_ip" ] && [ "$current_ip" != "$last_ip" ]; then
|
||||
log "IP ${GW_HOST} изменился (${last_ip} → ${current_ip}) — reload nginx"
|
||||
nginx -s reload 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "$current_ip" ]; then
|
||||
last_ip="$current_ip"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_gateway
|
||||
watch_gateway &
|
||||
39
install.sh
39
install.sh
@@ -419,7 +419,11 @@ sync_nginx_after_api_gateway_restart() {
|
||||
wait_for_api_gateway_http 90 || warn "api-gateway /health на :3000 не отвечает"
|
||||
sleep 2
|
||||
reload_docker_nginx
|
||||
verify_nginx_reaches_api_gateway 24 5 || warn "Nginx → api-gateway: проверка не прошла, см. docker compose logs nginx api-gateway"
|
||||
if ! verify_nginx_reaches_api_gateway 24 5; then
|
||||
warn "Reload не помог — пересоздаём lendry-id-nginx..."
|
||||
recreate_docker_nginx
|
||||
verify_nginx_reaches_api_gateway 12 5 || warn "Nginx → api-gateway: проверка не прошла, см. docker compose logs nginx api-gateway"
|
||||
fi
|
||||
}
|
||||
|
||||
recreate_docker_nginx() {
|
||||
@@ -2658,6 +2662,7 @@ verify_nginx_reaches_api_gateway() {
|
||||
wget -qO- --timeout=5 http://lendry-id-api-gateway:3000/health 2>/dev/null || true)"
|
||||
if [[ "$body" == *'"status":"ok"'* ]] || [[ "$body" == *'"status": "ok"'* ]]; then
|
||||
ok "Docker Nginx → lendry-id-api-gateway:3000/health OK (IP api-gateway: ${gw_ip:-?})"
|
||||
verify_nginx_idp_api_location || return 1
|
||||
return 0
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
@@ -2671,6 +2676,38 @@ verify_nginx_reaches_api_gateway() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Тот же путь, что в браузере: 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
|
||||
frontend_domain="$(env_get DOMAIN_FRONTEND "")"
|
||||
[[ -n "$frontend_domain" ]] || return 0
|
||||
|
||||
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)"
|
||||
if [[ "$body" == *'"status":"ok"'* ]] || [[ "$body" == *'"status": "ok"'* ]]; then
|
||||
ok "Docker Nginx location /idp-api/ → api-gateway OK (Host: ${frontend_domain})"
|
||||
return 0
|
||||
fi
|
||||
|
||||
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)"
|
||||
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"
|
||||
return 1
|
||||
}
|
||||
|
||||
verify_nginx_reaches_media_ws() {
|
||||
docker_nginx_container_running || return 0
|
||||
build_compose_stack_cmd
|
||||
|
||||
Reference in New Issue
Block a user