fix and update
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -479,7 +479,7 @@ function notifyPinRequired(sessionId: string) {
|
||||
}
|
||||
|
||||
export async function fetchAuthSession(token?: string | null): Promise<AuthSessionResponse> {
|
||||
return apiFetch<AuthSessionResponse>('/auth/session', {}, token);
|
||||
return apiFetch<AuthSessionResponse>('/auth/session', {}, token, true, { critical: true });
|
||||
}
|
||||
|
||||
export async function approveOAuthAuthorization(params: URLSearchParams, token: string) {
|
||||
@@ -688,7 +688,7 @@ export function buildSystemSettingPayload(setting: Pick<SystemSetting, 'key' | '
|
||||
}
|
||||
|
||||
const GATEWAY_RETRY_STATUS = new Set([502, 503, 504]);
|
||||
const GATEWAY_RETRY_ATTEMPTS = 2;
|
||||
const GATEWAY_RETRY_ATTEMPTS = 3;
|
||||
const GATEWAY_CRITICAL_RETRY_ATTEMPTS = 4;
|
||||
|
||||
export interface ApiFetchBehavior {
|
||||
@@ -700,8 +700,8 @@ export interface ApiFetchBehavior {
|
||||
const GATEWAY_STABLE_PROBE_INTERVAL_MS = 800;
|
||||
const GATEWAY_STABLE_MAX_ROUNDS = 8;
|
||||
const API_MAX_CONCURRENT = 6;
|
||||
const API_BURST_MAX_CONCURRENT = 2;
|
||||
const API_BURST_WINDOW_MS = 5000;
|
||||
const API_BURST_MAX_CONCURRENT = 4;
|
||||
const API_BURST_WINDOW_MS = 8000;
|
||||
const API_READY_EVENT = 'idp-api-ready';
|
||||
const API_NOT_READY_EVENT = 'idp-api-not-ready';
|
||||
export const AUTH_SESSION_INVALID_EVENT = 'idp-auth-session-invalid';
|
||||
@@ -746,6 +746,8 @@ function shouldBypassGatewayCircuit(behavior?: ApiFetchBehavior): boolean {
|
||||
|
||||
function openGatewayCircuit(behavior?: ApiFetchBehavior): void {
|
||||
if (shouldBypassGatewayCircuit(behavior)) return;
|
||||
// До стабильного прогрева /health circuit не открываем — иначе каждая перезагрузка ловит ложный 502.
|
||||
if (!gatewayReadyResolved) return;
|
||||
gatewayReadyResolved = false;
|
||||
gatewayReadyAt = 0;
|
||||
stableGatewayPromise = null;
|
||||
@@ -757,6 +759,7 @@ function openGatewayCircuit(behavior?: ApiFetchBehavior): void {
|
||||
|
||||
function assertGatewayCircuitClosed(behavior?: ApiFetchBehavior): void {
|
||||
if (typeof window === 'undefined' || shouldBypassGatewayCircuit(behavior)) return;
|
||||
if (!gatewayReadyAt && !gatewayCircuitOpenUntil) return;
|
||||
if (Date.now() < gatewayCircuitOpenUntil) {
|
||||
throw new ApiError('Сервер API временно недоступен. Запрос отложен, чтобы не перегружать консоль ошибками.', 503, 'GATEWAY_UNAVAILABLE');
|
||||
}
|
||||
@@ -876,7 +879,7 @@ export async function ensureApiGatewayReady(force = false): Promise<void> {
|
||||
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<T>(
|
||||
allowRetry = true,
|
||||
behavior: ApiFetchBehavior = {}
|
||||
): Promise<T> {
|
||||
if (!gatewayReadyResolved && !behavior.silent) {
|
||||
try {
|
||||
await ensureApiGatewayReady();
|
||||
} catch {
|
||||
// fetchWithGatewayRetry попробует сам после кратковременного 502
|
||||
}
|
||||
}
|
||||
|
||||
await acquireApiSlot();
|
||||
|
||||
const resolvedToken = resolveAccessToken(token);
|
||||
@@ -1000,7 +1008,6 @@ export async function apiFetch<T>(
|
||||
throw error;
|
||||
}
|
||||
|
||||
markApiGatewayReady();
|
||||
return response.json() as Promise<T>;
|
||||
} 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<FamilyGroup>(`/family/groups/${groupId}`, {}, token);
|
||||
return apiFetch<FamilyGroup>(`/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<FamilyPresence>(`/family/groups/${groupId}/presence`, {}, accessToken);
|
||||
return apiFetch<FamilyPresence>(`/family/groups/${groupId}/presence`, {}, accessToken, true, { silent: true });
|
||||
}
|
||||
|
||||
/** @deprecated Используйте getWsUrl() — URL может переключаться на same-origin /idp-api/ws в браузере */
|
||||
|
||||
Reference in New Issue
Block a user