fix and update

This commit is contained in:
lendry
2026-06-30 11:28:10 +03:00
parent c082b087c5
commit df4bbba133
6 changed files with 139 additions and 69 deletions

View File

@@ -11,7 +11,6 @@ import {
AUTH_USER_CACHE_KEY,
AuthSessionResponse,
AuthTokens,
ensureApiGatewayReady,
fetchAuthSession,
buildAuthDevicePayload,
IdentifyResponse,
@@ -29,6 +28,8 @@ import {
scheduleFedcmSessionSync,
setPinRequiredHandler,
subscribeApiReady,
subscribeApiNotReady,
waitForStableGateway,
} from '@/lib/api';
import { useToast } from './toast-provider';
import { PinLockModal } from './pin-lock-modal';
@@ -40,7 +41,9 @@ interface AuthContextValue {
token: string | null;
isLoading: boolean;
isApiReady: boolean;
/** false первые ~600 ms после bootstrap — блокирует шторм API сразу после логина */
/** true после стабильного /health и успешной проверки сессии */
isSessionReady: boolean;
/** @deprecated используйте isSessionReady */
isContentReady: boolean;
isPinLocked: boolean;
hasStoredSession: boolean;
@@ -53,7 +56,7 @@ interface AuthContextValue {
beginTotpLogin: (recipient: string) => Promise<string>;
verifyTotpLogin: (totpChallengeToken: string, code: string) => Promise<AuthTokens>;
completePin: (sessionId: string, pin: string) => Promise<void>;
applyLoginAuth: (auth: AuthTokens) => AuthTokens;
applyLoginAuth: (auth: AuthTokens) => Promise<AuthTokens>;
register: (data: { displayName: string; login: string; password: string }) => Promise<void>;
refreshProfile: () => Promise<void>;
logout: () => void;
@@ -111,7 +114,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = React.useState<PublicUser | null>(null);
const [isLoading, setIsLoading] = React.useState(true);
const [isApiReady, setIsApiReady] = React.useState(false);
const [isContentReady, setIsContentReady] = React.useState(false);
const [isSessionReady, setIsSessionReady] = React.useState(false);
const [isPinLocked, setIsPinLocked] = React.useState(false);
const [hasStoredSession, setHasStoredSession] = React.useState(false);
const [lockedSessionId, setLockedSessionId] = React.useState<string | null>(null);
@@ -125,9 +128,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
if (isApiGatewayReady()) {
setIsApiReady(true);
}
return subscribeApiReady(() => {
setIsApiReady(true);
});
const onReady = () => setIsApiReady(true);
const onNotReady = () => {
setIsApiReady(false);
setIsSessionReady(false);
};
const unsubReady = subscribeApiReady(onReady);
const unsubNotReady = subscribeApiNotReady(onNotReady);
return () => {
unsubReady();
unsubNotReady();
};
}, []);
React.useEffect(() => {
@@ -167,8 +178,29 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}
}, []);
const saveSession = React.useCallback(
(auth: AuthTokens) => {
const warmUpAndApplySession = React.useCallback(
async (accessToken: string) => {
setIsSessionReady(false);
setIsLoading(true);
invalidateApiGatewayReady();
setIsApiReady(false);
try {
await waitForStableGateway(3);
const session = await fetchAuthSession(accessToken);
applySessionState(session, { setUser, setToken, activatePinLock, clearPinLock });
initialBootstrapDoneRef.current = true;
setIsApiReady(true);
setIsSessionReady(true);
} finally {
setIsLoading(false);
}
},
[activatePinLock, clearPinLock]
);
const establishSession = React.useCallback(
async (auth: AuthTokens) => {
window.localStorage.setItem(AUTH_TOKEN_KEY, auth.accessToken);
window.localStorage.setItem(AUTH_REFRESH_KEY, auth.refreshToken);
window.localStorage.setItem(AUTH_SESSION_KEY, auth.sessionId);
@@ -177,15 +209,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
cacheUserProfile(auth.user);
setHasStoredSession(true);
clearPinLock();
initialBootstrapDoneRef.current = true;
setIsApiReady(true);
setIsLoading(false);
setIsContentReady(false);
await warmUpAndApplySession(auth.accessToken);
if (auth.pinVerified !== false) {
scheduleFedcmSessionSync(auth.accessToken, 2500);
}
},
[clearPinLock]
[clearPinLock, warmUpAndApplySession]
);
const logout = React.useCallback(() => {
@@ -197,6 +228,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setToken(null);
setUser(null);
setHasStoredSession(false);
setIsSessionReady(false);
setIsApiReady(false);
invalidateApiGatewayReady();
clearPinLock();
router.push('/auth/login');
}, [clearPinLock, router]);
@@ -213,6 +247,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
if (!currentToken && !refreshToken) {
initialBootstrapDoneRef.current = true;
setIsSessionReady(true);
setIsLoading(false);
return;
}
@@ -226,7 +261,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
for (let bootstrapAttempt = 0; bootstrapAttempt < 6; bootstrapAttempt += 1) {
try {
if (isInitialBootstrap) {
await ensureApiGatewayReady();
await waitForStableGateway(3);
}
if (currentToken) {
@@ -234,6 +269,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
try {
const session = await fetchAuthSession(currentToken);
applySessionState(session, { setUser, setToken, activatePinLock, clearPinLock });
setIsSessionReady(true);
return;
} catch (error) {
if (isPinRequiredError(error)) {
@@ -266,6 +302,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
if (refreshed.accessToken) {
const session = await fetchAuthSession(refreshed.accessToken);
applySessionState(session, { setUser, setToken, activatePinLock, clearPinLock });
setIsSessionReady(true);
return;
}
}
@@ -299,6 +336,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
if (isInitialBootstrap) {
initialBootstrapDoneRef.current = true;
setIsApiReady(isApiGatewayReady());
setIsSessionReady(isApiGatewayReady());
setIsLoading(false);
}
}
@@ -359,14 +397,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
})
});
if (auth.pinVerified) {
saveSession(auth);
await establishSession(auth);
} else {
persistPartialAuth(auth);
setHasStoredSession(true);
}
return auth;
},
[saveSession]
[establishSession]
);
const identifyLogin = React.useCallback(async (loginValue: string) => {
@@ -394,14 +432,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
})
});
if (response.auth?.pinVerified) {
saveSession(response.auth);
await establishSession(response.auth);
} else if (response.auth) {
persistPartialAuth(response.auth);
setHasStoredSession(true);
}
return response;
},
[saveSession]
[establishSession]
);
const loginWithPassword = React.useCallback(
@@ -415,14 +453,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
})
});
if (auth.pinVerified) {
saveSession(auth);
await establishSession(auth);
} else {
persistPartialAuth(auth);
setHasStoredSession(true);
}
return auth;
},
[saveSession]
[establishSession]
);
const loginWithLdap = React.useCallback(
@@ -436,14 +474,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
})
});
if (auth.pinVerified) {
saveSession(auth);
await establishSession(auth);
} else {
persistPartialAuth(auth);
setHasStoredSession(true);
}
return auth;
},
[saveSession]
[establishSession]
);
const beginTotpLogin = React.useCallback(async (recipient: string) => {
@@ -464,20 +502,20 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
body: JSON.stringify({ totpChallengeToken, code })
});
if (auth.pinVerified) {
saveSession(auth);
await establishSession(auth);
} else {
persistPartialAuth(auth);
setHasStoredSession(true);
}
return auth;
},
[saveSession]
[establishSession]
);
const applyLoginAuth = React.useCallback(
(auth: AuthTokens) => {
async (auth: AuthTokens) => {
if (auth.pinVerified) {
saveSession(auth);
await establishSession(auth);
} else {
persistPartialAuth(auth);
setHasStoredSession(true);
@@ -485,7 +523,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}
return auth;
},
[activatePinLock, saveSession]
[activatePinLock, establishSession]
);
const completePin = React.useCallback(
@@ -498,14 +536,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
window.localStorage.setItem(AUTH_SESSION_KEY, sessionId);
setToken(response.accessToken);
const session = await fetchAuthSession(response.accessToken);
applySessionState(session, { setUser, setToken, activatePinLock, clearPinLock });
await warmUpAndApplySession(response.accessToken);
if (pathname.startsWith('/auth/')) {
router.replace('/');
}
},
[activatePinLock, clearPinLock, pathname, router]
[pathname, router, warmUpAndApplySession]
);
const handlePinUnlock = React.useCallback(
@@ -548,22 +585,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const isBootstrapping =
!isPinLocked &&
Boolean(hasStoredSession || token || user) &&
(isLoading || !isApiReady);
React.useEffect(() => {
if (isBootstrapping) {
setIsContentReady(false);
return;
}
let cancelled = false;
const timer = window.setTimeout(() => {
if (!cancelled) setIsContentReady(true);
}, 650);
return () => {
cancelled = true;
window.clearTimeout(timer);
};
}, [isBootstrapping]);
(isLoading || !isApiReady || !isSessionReady);
return (
<AuthContext.Provider
@@ -572,7 +594,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
token,
isLoading,
isApiReady,
isContentReady,
isSessionReady,
isContentReady: isSessionReady,
isPinLocked,
hasStoredSession,
login,
@@ -590,7 +613,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
logout
}}
>
{isBootstrapping || !isContentReady ? <AppBootstrapScreen /> : children}
{isBootstrapping ? <AppBootstrapScreen /> : children}
<PinLockModal open={isPinLocked} isSubmitting={pinSubmitting} error={pinError} onSubmit={handlePinUnlock} />
</AuthContext.Provider>
);

View File

@@ -1,7 +1,7 @@
'use client';
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { apiFetch } from '@/lib/api';
import { apiFetch, ensureApiGatewayReady } from '@/lib/api';
interface PublicSettingsContextValue {
projectName: string;
@@ -27,6 +27,7 @@ export function PublicSettingsProvider({ children }: { children: React.ReactNode
const refreshPublicSettings = useCallback(async () => {
try {
await ensureApiGatewayReady();
const response = await apiFetch<{ settings: Array<{ key: string; value: string }> }>('/settings/public');
const map = Object.fromEntries((response.settings ?? []).map((item) => [item.key, item.value]));
setSettings(map);