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:
IGNY8 VPS (Salman)
2025-11-16 19:22:45 +00:00
parent b20fab8ec1
commit 1d39f3f00a
2 changed files with 88 additions and 35 deletions

View File

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

View File

@@ -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) {