From f8f25c82896de5b78fb067bf6ee1e102b6466bfa Mon Sep 17 00:00:00 2001 From: lendry Date: Wed, 1 Jul 2026 11:32:53 +0300 Subject: [PATCH] fix and update --- apps/frontend/app/admin/users/page.tsx | 5 +-- apps/frontend/components/id/auth-provider.tsx | 15 ++++++++- apps/frontend/lib/api.ts | 33 +++++++++++-------- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/apps/frontend/app/admin/users/page.tsx b/apps/frontend/app/admin/users/page.tsx index a355a56..e9c7d11 100644 --- a/apps/frontend/app/admin/users/page.tsx +++ b/apps/frontend/app/admin/users/page.tsx @@ -12,7 +12,7 @@ import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Table, TableBody, TableCell, TableContainer, TableHead, TableHeader, TableRow } from '@/components/ui/table'; -import { AdminPermission, AdminRole, AdminUser, adminDisableUserTotp, apiFetch, fetchUserTotpStatus } from '@/lib/api'; +import { AdminPermission, AdminRole, AdminUser, adminDisableUserTotp, apiFetch, fetchUserTotpStatus, getApiErrorMessage } from '@/lib/api'; import { getAdminLandingPath } from '@/lib/admin-access'; import { DEFAULT_VERIFICATION_ICON, VERIFICATION_ICON_OPTIONS } from '@/lib/verification-icons'; @@ -56,7 +56,8 @@ export default function AdminUsersPage() { const response = await apiFetch<{ users: AdminUser[] }>(`/admin/users${search ? `?search=${encodeURIComponent(search)}` : ''}`, {}, token); setUsers(response.users ?? []); } catch (error) { - showToast(error instanceof Error ? error.message : 'Не удалось загрузить пользователей'); + const message = getApiErrorMessage(error, 'Не удалось загрузить пользователей'); + if (message) showToast(message); } finally { setLoading(false); } diff --git a/apps/frontend/components/id/auth-provider.tsx b/apps/frontend/components/id/auth-provider.tsx index f2b223d..6b09585 100644 --- a/apps/frontend/components/id/auth-provider.tsx +++ b/apps/frontend/components/id/auth-provider.tsx @@ -16,6 +16,7 @@ import { buildAuthDevicePayload, IdentifyResponse, getApiErrorMessage, + ensureApiGatewayReady, invalidateApiGatewayReady, isApiGatewayReady, isGatewayUnavailableError, @@ -25,6 +26,7 @@ import { PinVerificationResponse, PublicUser, refreshAuthSession, + resetGatewayCircuit, resetPinRequiredNotification, scheduleFedcmSessionSync, setPinRequiredHandler, @@ -200,6 +202,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setIsApiReady(false); try { + await ensureApiGatewayReady(); const session = await fetchAuthSession(accessToken); applySessionState(session, { setUser, setToken, activatePinLock, clearPinLock }); initialBootstrapDoneRef.current = true; @@ -321,6 +324,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { try { for (let bootstrapAttempt = 0; bootstrapAttempt < 4; bootstrapAttempt += 1) { + if (isInitialBootstrap) { + try { + await ensureApiGatewayReady(bootstrapAttempt > 0); + } catch { + // цикл bootstrap повторит после паузы + } + } + try { if (currentToken) { setToken(currentToken); @@ -375,6 +386,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } catch (error) { if (isGatewayUnavailableError(error) && isInitialBootstrap && bootstrapAttempt + 1 < 4) { invalidateApiGatewayReady(); + resetGatewayCircuit(); setBootstrapError( 'Не удалось подключиться к серверу API (502). Подождите несколько секунд и нажмите «Повторить».' ); @@ -697,7 +709,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { isLoading, isApiReady, isSessionReady, - isContentReady: isSessionReady, + isContentReady: isSessionReady && isApiReady, isPinLocked, hasStoredSession, login, @@ -723,6 +735,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setBootstrapError(null); initialBootstrapDoneRef.current = false; invalidateApiGatewayReady(); + resetGatewayCircuit(); setIsLoading(true); setIsApiReady(false); setIsSessionReady(false); diff --git a/apps/frontend/lib/api.ts b/apps/frontend/lib/api.ts index 20450f6..f048c6a 100644 --- a/apps/frontend/lib/api.ts +++ b/apps/frontend/lib/api.ts @@ -479,7 +479,7 @@ function notifyPinRequired(sessionId: string) { } export async function fetchAuthSession(token?: string | null): Promise { - return apiFetch('/auth/session', {}, token); + return apiFetch('/auth/session', {}, token, true, { critical: true }); } export async function approveOAuthAuthorization(params: URLSearchParams, token: string) { @@ -688,7 +688,7 @@ export function buildSystemSettingPayload(setting: Pick { gatewayCircuitOpenUntil = 0; } - const probes = force ? 2 : 1; + const probes = force ? 2 : 2; gatewayReadyPromise = waitForStableGateway(probes).finally(() => { gatewayReadyPromise = null; }); @@ -900,9 +903,6 @@ async function fetchWithGatewayRetry( try { assertGatewayCircuitClosed(behavior); const response = await fetch(url, init); - if (response.ok) { - markApiGatewayReady(); - } if (GATEWAY_RETRY_STATUS.has(response.status) && attempt + 1 < maxAttempts) { await sleep(gatewayRetryDelay(attempt)); continue; @@ -935,6 +935,14 @@ export async function apiFetch( allowRetry = true, behavior: ApiFetchBehavior = {} ): Promise { + if (!gatewayReadyResolved && !behavior.silent) { + try { + await ensureApiGatewayReady(); + } catch { + // fetchWithGatewayRetry попробует сам после кратковременного 502 + } + } + await acquireApiSlot(); const resolvedToken = resolveAccessToken(token); @@ -1000,7 +1008,6 @@ export async function apiFetch( throw error; } - markApiGatewayReady(); return response.json() as Promise; } finally { releaseCurrentApiSlot(); @@ -1263,11 +1270,11 @@ export async function fetchFamilyGroups(userId: string, token?: string | null) { if (!accessToken) { throw new ApiError('Не авторизован', 401, 'NO_TOKEN'); } - return apiFetch<{ groups?: FamilyGroup[] }>(`/family/users/${userId}/groups`, {}, accessToken); + return apiFetch<{ groups?: FamilyGroup[] }>(`/family/users/${userId}/groups`, {}, accessToken, true, { silent: true }); } export async function fetchFamilyGroup(groupId: string, token?: string | null) { - return apiFetch(`/family/groups/${groupId}`, {}, token); + return apiFetch(`/family/groups/${groupId}`, {}, token, true, { silent: true }); } export async function updateFamilyGroup(groupId: string, name: string, token?: string | null) { @@ -1325,7 +1332,7 @@ export async function fetchNotifications(token?: string | null, unreadOnly = fal } export async function fetchUnreadNotificationCount(token?: string | null) { - return apiFetch<{ count: number }>('/notifications/unread-count', {}, token); + return apiFetch<{ count: number }>('/notifications/unread-count', {}, token, true, { silent: true }); } export async function markNotificationRead(notificationId: string, token?: string | null) { @@ -1584,7 +1591,7 @@ export async function fetchFamilyPresence(groupId: string, token?: string | null if (!accessToken) { throw new ApiError('Не авторизован', 401, 'NO_TOKEN'); } - return apiFetch(`/family/groups/${groupId}/presence`, {}, accessToken); + return apiFetch(`/family/groups/${groupId}/presence`, {}, accessToken, true, { silent: true }); } /** @deprecated Используйте getWsUrl() — URL может переключаться на same-origin /idp-api/ws в браузере */