fix: remove 401 hard redirect, let React Query retry
The /api/me call races with TokenSync — fires before the token provider is set. Removed the hard window.location redirect on 401 from the API client. React Query retries with backoff instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,11 +6,6 @@ export function setTokenProvider(provider: (() => Promise<string | undefined>) |
|
|||||||
tokenProvider = provider;
|
tokenProvider = provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
let logoutHandler: (() => void) | null = null;
|
|
||||||
|
|
||||||
export function setLogoutHandler(handler: (() => void) | null) {
|
|
||||||
logoutHandler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||||
const token = tokenProvider ? await tokenProvider() : null;
|
const token = tokenProvider ? await tokenProvider() : null;
|
||||||
@@ -27,8 +22,8 @@ async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T>
|
|||||||
const response = await fetch(`${API_BASE}${path}`, { ...options, headers });
|
const response = await fetch(`${API_BASE}${path}`, { ...options, headers });
|
||||||
|
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
if (logoutHandler) logoutHandler();
|
// Don't hard-redirect — let React Query retry (token may not be ready yet).
|
||||||
window.location.href = '/login';
|
// The ProtectedRoute handles unauthenticated state.
|
||||||
throw new Error('Unauthorized');
|
throw new Error('Unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,8 @@ export function useMe() {
|
|||||||
queryKey: ['me'],
|
queryKey: ['me'],
|
||||||
queryFn: () => api.get<MeResponse>('/me'),
|
queryFn: () => api.get<MeResponse>('/me'),
|
||||||
staleTime: 60_000,
|
staleTime: 60_000,
|
||||||
|
retry: 3,
|
||||||
|
retryDelay: 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { LogtoProvider, UserScope, useLogto } from '@logto/react';
|
import { LogtoProvider, UserScope, useLogto } from '@logto/react';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
@@ -7,7 +7,7 @@ import { ThemeProvider, ToastProvider, BreadcrumbProvider, GlobalFilterProvider,
|
|||||||
import '@cameleer/design-system/style.css';
|
import '@cameleer/design-system/style.css';
|
||||||
import { AppRouter } from './router';
|
import { AppRouter } from './router';
|
||||||
import { fetchConfig } from './config';
|
import { fetchConfig } from './config';
|
||||||
import { setTokenProvider, setLogoutHandler } from './api/client';
|
import { setTokenProvider } from './api/client';
|
||||||
import { useOrgStore } from './auth/useOrganization';
|
import { useOrgStore } from './auth/useOrganization';
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
@@ -20,7 +20,7 @@ const queryClient = new QueryClient({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function TokenSync({ resource }: { resource: string }) {
|
function TokenSync({ resource }: { resource: string }) {
|
||||||
const { getAccessToken, isAuthenticated, signOut, fetchUserInfo } = useLogto();
|
const { getAccessToken, isAuthenticated, fetchUserInfo } = useLogto();
|
||||||
const { currentOrgId, setCurrentOrg, setOrganizations } = useOrgStore();
|
const { currentOrgId, setCurrentOrg, setOrganizations } = useOrgStore();
|
||||||
|
|
||||||
// After auth, resolve user's organizations from Logto
|
// After auth, resolve user's organizations from Logto
|
||||||
@@ -59,15 +59,6 @@ function TokenSync({ resource }: { resource: string }) {
|
|||||||
}
|
}
|
||||||
}, [isAuthenticated, getAccessToken, resource, currentOrgId]);
|
}, [isAuthenticated, getAccessToken, resource, currentOrgId]);
|
||||||
|
|
||||||
const handleLogout = useCallback(() => {
|
|
||||||
signOut(window.location.origin + '/login');
|
|
||||||
}, [signOut]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLogoutHandler(handleLogout);
|
|
||||||
return () => setLogoutHandler(null);
|
|
||||||
}, [handleLogout]);
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user