fix and update

This commit is contained in:
lendry
2026-06-30 09:39:35 +03:00
parent 250976ca08
commit 4e98f6bfab
8 changed files with 192 additions and 110 deletions

View File

@@ -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,

View File

@@ -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();

View File

@@ -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 }}>