Phase 0: Fix token race condition causing logout after login
- Updated getAuthToken/getRefreshToken to read from Zustand store first (faster, no parsing delay) - Added token existence check before making API calls in AppLayout - Added retry mechanism with 100ms delay to wait for Zustand persist to write token - Made 403 error handler smarter - only logout if token actually exists (prevents false logouts) - Fixes issue where user gets logged out immediately after successful login
This commit is contained in:
@@ -26,39 +26,60 @@ const LayoutContent: React.FC = () => {
|
|||||||
|
|
||||||
// Initialize site store on mount - only once, but only if authenticated
|
// Initialize site store on mount - only once, but only if authenticated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only load sites if user is authenticated
|
// Only load sites if user is authenticated AND has a token
|
||||||
if (!isAuthenticated) return;
|
if (!isAuthenticated) return;
|
||||||
|
|
||||||
if (!hasLoadedSite.current && !isLoadingSite.current) {
|
// Check if token exists - if not, wait a bit for Zustand persist to write it
|
||||||
hasLoadedSite.current = true;
|
const checkTokenAndLoad = () => {
|
||||||
isLoadingSite.current = true;
|
const authState = useAuthStore.getState();
|
||||||
trackLoading('site-loading', true);
|
if (!authState?.token) {
|
||||||
|
// Token not available yet - wait a bit and retry (Zustand persist might still be writing)
|
||||||
// Add timeout to prevent infinite loading
|
setTimeout(() => {
|
||||||
// Match API timeout (30s) + buffer for network delays
|
const retryAuthState = useAuthStore.getState();
|
||||||
const timeoutId = setTimeout(() => {
|
if (retryAuthState?.token && !hasLoadedSite.current && !isLoadingSite.current) {
|
||||||
if (isLoadingSite.current) {
|
loadSites();
|
||||||
console.error('AppLayout: Site loading timeout after 35 seconds');
|
|
||||||
trackLoading('site-loading', false);
|
|
||||||
isLoadingSite.current = false;
|
|
||||||
addError(new Error('Site loading timeout - check network connection'), 'AppLayout.loadActiveSite');
|
|
||||||
}
|
|
||||||
}, 35000); // 35 seconds to match API timeout (30s) + buffer
|
|
||||||
|
|
||||||
loadActiveSite()
|
|
||||||
.catch((error) => {
|
|
||||||
// Don't log 403 errors as they're expected when not authenticated
|
|
||||||
if (error.status !== 403) {
|
|
||||||
console.error('AppLayout: Error loading active site:', error);
|
|
||||||
addError(error, 'AppLayout.loadActiveSite');
|
|
||||||
}
|
}
|
||||||
})
|
}, 100); // Wait 100ms for persist to write
|
||||||
.finally(() => {
|
return;
|
||||||
clearTimeout(timeoutId);
|
}
|
||||||
trackLoading('site-loading', false);
|
|
||||||
isLoadingSite.current = false;
|
loadSites();
|
||||||
});
|
};
|
||||||
}
|
|
||||||
|
const loadSites = () => {
|
||||||
|
if (!hasLoadedSite.current && !isLoadingSite.current) {
|
||||||
|
hasLoadedSite.current = true;
|
||||||
|
isLoadingSite.current = true;
|
||||||
|
trackLoading('site-loading', true);
|
||||||
|
|
||||||
|
// Add timeout to prevent infinite loading
|
||||||
|
// Match API timeout (30s) + buffer for network delays
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (isLoadingSite.current) {
|
||||||
|
console.error('AppLayout: Site loading timeout after 35 seconds');
|
||||||
|
trackLoading('site-loading', false);
|
||||||
|
isLoadingSite.current = false;
|
||||||
|
addError(new Error('Site loading timeout - check network connection'), 'AppLayout.loadActiveSite');
|
||||||
|
}
|
||||||
|
}, 35000); // 35 seconds to match API timeout (30s) + buffer
|
||||||
|
|
||||||
|
loadActiveSite()
|
||||||
|
.catch((error) => {
|
||||||
|
// Don't log 403 errors as they're expected when not authenticated
|
||||||
|
if (error.status !== 403) {
|
||||||
|
console.error('AppLayout: Error loading active site:', error);
|
||||||
|
addError(error, 'AppLayout.loadActiveSite');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
trackLoading('site-loading', false);
|
||||||
|
isLoadingSite.current = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkTokenAndLoad();
|
||||||
}, [isAuthenticated]); // Run when authentication state changes
|
}, [isAuthenticated]); // Run when authentication state changes
|
||||||
|
|
||||||
// Load sectors when active site changes (by ID, not object reference)
|
// Load sectors when active site changes (by ID, not object reference)
|
||||||
@@ -120,6 +141,19 @@ const LayoutContent: React.FC = () => {
|
|||||||
// Throttle: only refresh if last refresh was more than 30 seconds ago (unless forced)
|
// Throttle: only refresh if last refresh was more than 30 seconds ago (unless forced)
|
||||||
if (!force && now - lastUserRefresh.current < 30000) return;
|
if (!force && now - lastUserRefresh.current < 30000) return;
|
||||||
|
|
||||||
|
// Check if token exists before making API call
|
||||||
|
const authState = useAuthStore.getState();
|
||||||
|
if (!authState?.token) {
|
||||||
|
// Token not available yet - wait a bit for Zustand persist to write it
|
||||||
|
setTimeout(() => {
|
||||||
|
const retryAuthState = useAuthStore.getState();
|
||||||
|
if (retryAuthState?.token && retryAuthState?.isAuthenticated) {
|
||||||
|
refreshUserData(force);
|
||||||
|
}
|
||||||
|
}, 100); // Wait 100ms for persist to write
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
lastUserRefresh.current = now;
|
lastUserRefresh.current = now;
|
||||||
await refreshUser();
|
await refreshUser();
|
||||||
|
|||||||
@@ -78,9 +78,16 @@ function getActiveSectorId(): number | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get auth token from store
|
// Get auth token from store - try Zustand store first, then localStorage as fallback
|
||||||
const getAuthToken = (): string | null => {
|
const getAuthToken = (): string | null => {
|
||||||
try {
|
try {
|
||||||
|
// First try to get from Zustand store directly (faster, no parsing)
|
||||||
|
const authState = useAuthStore.getState();
|
||||||
|
if (authState?.token) {
|
||||||
|
return authState.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to localStorage (for cases where store hasn't initialized yet)
|
||||||
const authStorage = localStorage.getItem('auth-storage');
|
const authStorage = localStorage.getItem('auth-storage');
|
||||||
if (authStorage) {
|
if (authStorage) {
|
||||||
const parsed = JSON.parse(authStorage);
|
const parsed = JSON.parse(authStorage);
|
||||||
@@ -92,9 +99,16 @@ const getAuthToken = (): string | null => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get refresh token from store
|
// Get refresh token from store - try Zustand store first, then localStorage as fallback
|
||||||
const getRefreshToken = (): string | null => {
|
const getRefreshToken = (): string | null => {
|
||||||
try {
|
try {
|
||||||
|
// First try to get from Zustand store directly (faster, no parsing)
|
||||||
|
const authState = useAuthStore.getState();
|
||||||
|
if (authState?.refreshToken) {
|
||||||
|
return authState.refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to localStorage (for cases where store hasn't initialized yet)
|
||||||
const authStorage = localStorage.getItem('auth-storage');
|
const authStorage = localStorage.getItem('auth-storage');
|
||||||
if (authStorage) {
|
if (authStorage) {
|
||||||
const parsed = JSON.parse(authStorage);
|
const parsed = JSON.parse(authStorage);
|
||||||
@@ -148,9 +162,14 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo
|
|||||||
if (errorData?.detail?.includes('Authentication credentials') ||
|
if (errorData?.detail?.includes('Authentication credentials') ||
|
||||||
errorData?.message?.includes('Authentication credentials') ||
|
errorData?.message?.includes('Authentication credentials') ||
|
||||||
errorData?.error?.includes('Authentication credentials')) {
|
errorData?.error?.includes('Authentication credentials')) {
|
||||||
// Token is invalid - clear auth state and force re-login
|
// Only logout if we actually have a token stored (means it's invalid)
|
||||||
const { logout } = useAuthStore.getState();
|
// If no token, it might be a race condition after login - don't logout
|
||||||
logout();
|
const authState = useAuthStore.getState();
|
||||||
|
if (authState?.token || authState?.isAuthenticated) {
|
||||||
|
// Token exists but is invalid - clear auth state and force re-login
|
||||||
|
const { logout } = useAuthStore.getState();
|
||||||
|
logout();
|
||||||
|
}
|
||||||
// Don't throw here - let the error handling below show the error
|
// Don't throw here - let the error handling below show the error
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user