fix and update
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user