From 1d39f3f00a03c87ee46c9c0362218ae9b3063cba Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Sun, 16 Nov 2025 19:22:45 +0000 Subject: [PATCH] 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 --- frontend/src/layout/AppLayout.tsx | 94 +++++++++++++++++++++---------- frontend/src/services/api.ts | 29 ++++++++-- 2 files changed, 88 insertions(+), 35 deletions(-) diff --git a/frontend/src/layout/AppLayout.tsx b/frontend/src/layout/AppLayout.tsx index 1483e847..6d965581 100644 --- a/frontend/src/layout/AppLayout.tsx +++ b/frontend/src/layout/AppLayout.tsx @@ -26,39 +26,60 @@ const LayoutContent: React.FC = () => { // Initialize site store on mount - only once, but only if authenticated useEffect(() => { - // Only load sites if user is authenticated + // Only load sites if user is authenticated AND has a token if (!isAuthenticated) return; - 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'); + // Check if token exists - if not, wait a bit for Zustand persist to write it + const checkTokenAndLoad = () => { + const authState = useAuthStore.getState(); + if (!authState?.token) { + // Token not available yet - wait a bit and retry (Zustand persist might still be writing) + setTimeout(() => { + const retryAuthState = useAuthStore.getState(); + if (retryAuthState?.token && !hasLoadedSite.current && !isLoadingSite.current) { + loadSites(); } - }) - .finally(() => { - clearTimeout(timeoutId); - trackLoading('site-loading', false); - isLoadingSite.current = false; - }); - } + }, 100); // Wait 100ms for persist to write + return; + } + + 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 // 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) 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 { lastUserRefresh.current = now; await refreshUser(); diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 566bf003..4ec8d50e 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -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 => { 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'); if (authStorage) { const parsed = JSON.parse(authStorage); @@ -92,9 +99,16 @@ const getAuthToken = (): string | 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 => { 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'); if (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') || errorData?.message?.includes('Authentication credentials') || errorData?.error?.includes('Authentication credentials')) { - // Token is invalid - clear auth state and force re-login - const { logout } = useAuthStore.getState(); - logout(); + // Only logout if we actually have a token stored (means it's invalid) + // If no token, it might be a race condition after login - don't 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 } } catch (e) {