fix and update
This commit is contained in:
@@ -16,6 +16,9 @@ import {
|
||||
buildAuthDevicePayload,
|
||||
IdentifyResponse,
|
||||
getApiErrorMessage,
|
||||
invalidateApiGatewayReady,
|
||||
isApiGatewayReady,
|
||||
isGatewayUnavailableError,
|
||||
isPinRequiredError,
|
||||
OtpSendResponse,
|
||||
PasswordlessAuthResponse,
|
||||
@@ -25,6 +28,7 @@ import {
|
||||
resetPinRequiredNotification,
|
||||
scheduleFedcmSessionSync,
|
||||
setPinRequiredHandler,
|
||||
subscribeApiReady,
|
||||
} from '@/lib/api';
|
||||
import { useToast } from './toast-provider';
|
||||
import { PinLockModal } from './pin-lock-modal';
|
||||
@@ -35,6 +39,7 @@ interface AuthContextValue {
|
||||
user: PublicUser | null;
|
||||
token: string | null;
|
||||
isLoading: boolean;
|
||||
isApiReady: boolean;
|
||||
isPinLocked: boolean;
|
||||
hasStoredSession: boolean;
|
||||
login: (login: string, password: string) => Promise<AuthTokens>;
|
||||
@@ -103,6 +108,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [token, setToken] = React.useState<string | null>(null);
|
||||
const [user, setUser] = React.useState<PublicUser | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [isApiReady, setIsApiReady] = React.useState(false);
|
||||
const [isPinLocked, setIsPinLocked] = React.useState(false);
|
||||
const [hasStoredSession, setHasStoredSession] = React.useState(false);
|
||||
const [lockedSessionId, setLockedSessionId] = React.useState<string | null>(null);
|
||||
@@ -112,6 +118,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const refreshInFlightRef = React.useRef<Promise<void> | null>(null);
|
||||
const initialBootstrapDoneRef = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isApiGatewayReady()) {
|
||||
setIsApiReady(true);
|
||||
}
|
||||
return subscribeApiReady(() => {
|
||||
setIsApiReady(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
isPinLockedRef.current = isPinLocked;
|
||||
}, [isPinLocked]);
|
||||
@@ -160,6 +175,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
setHasStoredSession(true);
|
||||
clearPinLock();
|
||||
initialBootstrapDoneRef.current = true;
|
||||
setIsApiReady(true);
|
||||
setIsLoading(false);
|
||||
if (auth.pinVerified !== false) {
|
||||
scheduleFedcmSessionSync(auth.accessToken, 2000);
|
||||
@@ -186,7 +202,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
return refreshInFlightRef.current;
|
||||
}
|
||||
|
||||
const task = (async () => {
|
||||
const task: Promise<void> = (async () => {
|
||||
const currentToken = window.localStorage.getItem(AUTH_TOKEN_KEY);
|
||||
const refreshToken = window.localStorage.getItem(AUTH_REFRESH_KEY);
|
||||
setHasStoredSession(Boolean(refreshToken));
|
||||
@@ -200,78 +216,94 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const isInitialBootstrap = !initialBootstrapDoneRef.current;
|
||||
if (isInitialBootstrap) {
|
||||
setIsLoading(true);
|
||||
await ensureApiGatewayReady();
|
||||
}
|
||||
|
||||
try {
|
||||
if (currentToken) {
|
||||
setToken(currentToken);
|
||||
for (let bootstrapAttempt = 0; bootstrapAttempt < 6; bootstrapAttempt += 1) {
|
||||
try {
|
||||
const session = await fetchAuthSession(currentToken);
|
||||
applySessionState(session, { setUser, setToken, activatePinLock, clearPinLock });
|
||||
return;
|
||||
if (isInitialBootstrap) {
|
||||
await ensureApiGatewayReady();
|
||||
}
|
||||
|
||||
if (currentToken) {
|
||||
setToken(currentToken);
|
||||
try {
|
||||
const session = await fetchAuthSession(currentToken);
|
||||
applySessionState(session, { setUser, setToken, activatePinLock, clearPinLock });
|
||||
return;
|
||||
} catch (error) {
|
||||
if (isPinRequiredError(error)) {
|
||||
activatePinLock(error.sessionId ?? window.localStorage.getItem(AUTH_SESSION_KEY) ?? '');
|
||||
return;
|
||||
}
|
||||
if (!(error instanceof ApiError) || error.code !== 'TOKEN_EXPIRED') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (refreshToken) {
|
||||
const refreshed = await refreshAuthSession();
|
||||
if (refreshed.accessToken) {
|
||||
window.localStorage.setItem(AUTH_TOKEN_KEY, refreshed.accessToken);
|
||||
setToken(refreshed.accessToken);
|
||||
}
|
||||
if (refreshed.sessionId) {
|
||||
window.localStorage.setItem(AUTH_SESSION_KEY, refreshed.sessionId);
|
||||
}
|
||||
if (refreshed.requiresPin) {
|
||||
if (refreshed.user) {
|
||||
setUser(refreshed.user);
|
||||
cacheUserProfile(refreshed.user);
|
||||
}
|
||||
activatePinLock(refreshed.sessionId ?? '');
|
||||
return;
|
||||
}
|
||||
if (refreshed.accessToken) {
|
||||
const session = await fetchAuthSession(refreshed.accessToken);
|
||||
applySessionState(session, { setUser, setToken, activatePinLock, clearPinLock });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ApiError('Сессия недействительна', 401);
|
||||
} catch (error) {
|
||||
if (isGatewayUnavailableError(error) && isInitialBootstrap && bootstrapAttempt + 1 < 6) {
|
||||
invalidateApiGatewayReady();
|
||||
await new Promise((resolve) => setTimeout(resolve, 1200));
|
||||
continue;
|
||||
}
|
||||
if (isPinRequiredError(error)) {
|
||||
activatePinLock(error.sessionId ?? window.localStorage.getItem(AUTH_SESSION_KEY) ?? '');
|
||||
return;
|
||||
}
|
||||
if (!(error instanceof ApiError) || error.code !== 'TOKEN_EXPIRED') {
|
||||
throw error;
|
||||
if (refreshToken && error instanceof ApiError && error.status === 403) {
|
||||
activatePinLock(window.localStorage.getItem(AUTH_SESSION_KEY) ?? '');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (refreshToken) {
|
||||
const refreshed = await refreshAuthSession();
|
||||
if (refreshed.accessToken) {
|
||||
window.localStorage.setItem(AUTH_TOKEN_KEY, refreshed.accessToken);
|
||||
setToken(refreshed.accessToken);
|
||||
}
|
||||
if (refreshed.sessionId) {
|
||||
window.localStorage.setItem(AUTH_SESSION_KEY, refreshed.sessionId);
|
||||
}
|
||||
if (refreshed.requiresPin) {
|
||||
if (refreshed.user) {
|
||||
setUser(refreshed.user);
|
||||
cacheUserProfile(refreshed.user);
|
||||
if (error instanceof ApiError && (error.code === 'NO_REFRESH' || error.status === 401)) {
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
activatePinLock(refreshed.sessionId ?? '');
|
||||
return;
|
||||
}
|
||||
if (refreshed.accessToken) {
|
||||
const session = await fetchAuthSession(refreshed.accessToken);
|
||||
applySessionState(session, { setUser, setToken, activatePinLock, clearPinLock });
|
||||
const message = getApiErrorMessage(error, 'Не удалось загрузить профиль');
|
||||
if (message) showToast(message);
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ApiError('Сессия недействительна', 401);
|
||||
} catch (error) {
|
||||
if (isPinRequiredError(error)) {
|
||||
activatePinLock(error.sessionId ?? window.localStorage.getItem(AUTH_SESSION_KEY) ?? '');
|
||||
return;
|
||||
}
|
||||
if (refreshToken && error instanceof ApiError && error.status === 403) {
|
||||
activatePinLock(window.localStorage.getItem(AUTH_SESSION_KEY) ?? '');
|
||||
return;
|
||||
}
|
||||
if (error instanceof ApiError && (error.code === 'NO_REFRESH' || error.status === 401)) {
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
const message = getApiErrorMessage(error, 'Не удалось загрузить профиль');
|
||||
if (message) showToast(message);
|
||||
logout();
|
||||
} finally {
|
||||
if (isInitialBootstrap) {
|
||||
initialBootstrapDoneRef.current = true;
|
||||
setIsApiReady(isApiGatewayReady());
|
||||
setIsLoading(false);
|
||||
}
|
||||
refreshInFlightRef.current = null;
|
||||
}
|
||||
})();
|
||||
|
||||
refreshInFlightRef.current = task;
|
||||
void task.finally(() => {
|
||||
refreshInFlightRef.current = null;
|
||||
});
|
||||
return task;
|
||||
}, [activatePinLock, clearPinLock, logout, showToast]);
|
||||
|
||||
@@ -510,7 +542,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
);
|
||||
|
||||
const isBootstrapping =
|
||||
isLoading && !isPinLocked && Boolean(hasStoredSession || token || user);
|
||||
!isPinLocked &&
|
||||
Boolean(hasStoredSession || token || user) &&
|
||||
(isLoading || !isApiReady);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
@@ -518,6 +552,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
user,
|
||||
token,
|
||||
isLoading,
|
||||
isApiReady,
|
||||
isPinLocked,
|
||||
hasStoredSession,
|
||||
login,
|
||||
|
||||
@@ -27,7 +27,7 @@ function formatTime(value: string) {
|
||||
|
||||
export function NotificationBell() {
|
||||
const router = useRouter();
|
||||
const { token, isPinLocked, isLoading } = useAuth();
|
||||
const { token, isPinLocked, isLoading, isApiReady } = useAuth();
|
||||
const { showToast } = useToast();
|
||||
const { unreadCount, setUnreadCount, subscribe } = useRealtime();
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -36,7 +36,7 @@ export function NotificationBell() {
|
||||
const [respondingId, setRespondingId] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!token || isPinLocked || isLoading) return;
|
||||
if (!token || isPinLocked || isLoading || !isApiReady) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const [list, count] = await Promise.all([fetchNotifications(token), fetchUnreadNotificationCount(token)]);
|
||||
@@ -48,7 +48,7 @@ export function NotificationBell() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [isLoading, isPinLocked, setUnreadCount, showToast, token]);
|
||||
}, [isApiReady, isLoading, isPinLocked, setUnreadCount, showToast, token]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
|
||||
@@ -31,7 +31,7 @@ interface RealtimeContextValue {
|
||||
const RealtimeContext = React.createContext<RealtimeContextValue | null>(null);
|
||||
|
||||
export function RealtimeProvider({ children }: { children: React.ReactNode }) {
|
||||
const { user, token, isPinLocked, isLoading } = useAuth();
|
||||
const { user, token, isPinLocked, isLoading, isApiReady } = useAuth();
|
||||
const [unreadCount, setUnreadCount] = React.useState(0);
|
||||
const [connected, setConnected] = React.useState(false);
|
||||
const listenersRef = React.useRef(new Set<Listener>());
|
||||
@@ -48,7 +48,7 @@ export function RealtimeProvider({ children }: { children: React.ReactNode }) {
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!user || !token || isPinLocked || isLoading) {
|
||||
if (!user || !token || isPinLocked || isLoading || !isApiReady) {
|
||||
socketRef.current?.close();
|
||||
socketRef.current = null;
|
||||
setConnected(false);
|
||||
@@ -121,7 +121,7 @@ export function RealtimeProvider({ children }: { children: React.ReactNode }) {
|
||||
socketRef.current = null;
|
||||
setConnected(false);
|
||||
};
|
||||
}, [emit, isLoading, isPinLocked, token, user]);
|
||||
}, [emit, isApiReady, isLoading, isPinLocked, token, user]);
|
||||
|
||||
return (
|
||||
<RealtimeContext.Provider value={{ unreadCount, setUnreadCount, subscribe, connected }}>
|
||||
|
||||
Reference in New Issue
Block a user