From ffb02978547b77d94eef8c96943fd03fe0fc9612 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 12 Oct 2025 19:02:59 +0000 Subject: [PATCH] feat: Implement auth logging and session verification optimizations --- src/hooks/useAuth.tsx | 66 ++++++++++++++++++++++++------------------ src/lib/authLogger.ts | 23 +++++++++++++++ src/lib/authStorage.ts | 44 ++++++++++++++-------------- 3 files changed, 84 insertions(+), 49 deletions(-) create mode 100644 src/lib/authLogger.ts diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index 0df910c0..c6e48897 100644 --- a/src/hooks/useAuth.tsx +++ b/src/hooks/useAuth.tsx @@ -3,6 +3,7 @@ import type { User, Session } from '@supabase/supabase-js'; import { supabase } from '@/integrations/supabase/client'; import type { Profile } from '@/types/database'; import { toast } from '@/hooks/use-toast'; +import { authLog, authWarn, authError } from '@/lib/authLogger'; interface AuthContextType { user: User | null; @@ -36,6 +37,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { const previousEmailRef = useRef(null); const loadingTimeoutRef = useRef(null); const loadingStateRef = useRef(loading); + const lastVisibilityVerificationRef = useRef(Date.now()); const fetchProfile = async (userId: string, retryCount = 0, onComplete?: () => void) => { try { @@ -46,12 +48,12 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { .maybeSingle(); if (error && error.code !== 'PGRST116') { - console.error('[Auth] Error fetching profile:', error); + authError('[Auth] Error fetching profile:', error); // Retry up to 3 times with exponential backoff if (retryCount < 3 && isMountedRef.current) { const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s - console.log(`[Auth] Retrying profile fetch in ${delay}ms (attempt ${retryCount + 1}/3)`); + authLog(`[Auth] Retrying profile fetch in ${delay}ms (attempt ${retryCount + 1}/3)`); setTimeout(() => { if (isMountedRef.current) { fetchProfile(userId, retryCount + 1, onComplete); @@ -62,7 +64,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { // All retries exhausted - complete anyway if (isMountedRef.current) { - console.warn('[Auth] Profile fetch failed after 3 retries'); + authWarn('[Auth] Profile fetch failed after 3 retries'); setProfile(null); onComplete?.(); } @@ -76,7 +78,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { onComplete?.(); } } catch (error) { - console.error('[Auth] Error fetching profile:', error); + authError('[Auth] Error fetching profile:', error); // Retry logic for network errors if (retryCount < 3 && isMountedRef.current) { @@ -112,7 +114,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { const { data: { session }, error } = await supabase.auth.getSession(); if (error) { - console.error('[Auth] Session verification failed:', error); + authError('[Auth] Session verification failed:', error); setSessionError(error.message); if (updateLoadingState && isMountedRef.current) { setLoading(false); @@ -121,14 +123,14 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { } if (!session) { - console.log('[Auth] No active session found'); + authLog('[Auth] No active session found'); if (updateLoadingState && isMountedRef.current) { setLoading(false); } return false; } - console.log('[Auth] Session verified:', session.user.email); + authLog('[Auth] Session verified:', session.user.email); sessionVerifiedRef.current = true; // Update state if session was found but not set @@ -142,7 +144,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { return true; } catch (error) { - console.error('[Auth] Session verification error:', error); + authError('[Auth] Session verification error:', error); if (updateLoadingState && isMountedRef.current) { setLoading(false); } @@ -153,17 +155,17 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { // Keep loading state ref in sync useEffect(() => { loadingStateRef.current = loading; - console.log('[Auth] Loading state changed:', loading); + authLog('[Auth] Loading state changed:', loading); }, [loading]); useEffect(() => { - console.log('[Auth] Initializing auth provider'); + authLog('[Auth] Initializing auth provider'); // CRITICAL: Set up listener FIRST to catch all events const { data: { subscription }, } = supabase.auth.onAuthStateChange((event, session) => { - console.log('[Auth] State change:', event, 'User:', session?.user?.email || 'none', 'Has session:', !!session); + authLog('[Auth] State change:', event, 'User:', session?.user?.email || 'none', 'Has session:', !!session); // Extract email info early for cleanup const currentEmail = session?.user?.email; @@ -177,18 +179,18 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { // Update session and user state based on event if (event === 'SIGNED_IN' && session) { - console.log('[Auth] SIGNED_IN detected, setting session and user'); + authLog('[Auth] SIGNED_IN detected, setting session and user'); setSession(session); setUser(session.user); sessionVerifiedRef.current = true; } else if (event === 'INITIAL_SESSION') { if (session?.user) { - console.log('[Auth] INITIAL_SESSION with user, setting session'); + authLog('[Auth] INITIAL_SESSION with user, setting session'); setSession(session); setUser(session.user); sessionVerifiedRef.current = true; } else { - console.log('[Auth] INITIAL_SESSION with no user - setting loading to false'); + authLog('[Auth] INITIAL_SESSION with no user - setting loading to false'); setSession(null); setUser(null); setProfile(null); @@ -198,7 +200,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { return; // Exit early, no need to fetch profile } } else if (event === 'SIGNED_OUT') { - console.log('[Auth] SIGNED_OUT detected, clearing all state'); + authLog('[Auth] SIGNED_OUT detected, clearing all state'); setSession(null); setUser(null); setProfile(null); @@ -261,7 +263,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { }); } } catch (error) { - console.error('Error updating Novu after email confirmation:', error); + authError('Error updating Novu after email confirmation:', error); } finally { novuUpdateTimeoutRef.current = null; } @@ -282,12 +284,12 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { // Only wait for profile on initial auth events const shouldWaitForProfile = (event === 'SIGNED_IN' || event === 'INITIAL_SESSION'); - console.log('[Auth] Fetching profile, shouldWaitForProfile:', shouldWaitForProfile); + authLog('[Auth] Fetching profile, shouldWaitForProfile:', shouldWaitForProfile); profileFetchTimeoutRef.current = setTimeout(() => { fetchProfile(session.user.id, 0, () => { if (shouldWaitForProfile) { - console.log('[Auth] Profile fetch complete, setting loading to false'); + authLog('[Auth] Profile fetch complete, setting loading to false'); setLoading(false); } }); @@ -296,7 +298,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { } else { // No session/user - clear profile and resolve loading setProfile(null); - console.log('[Auth] No user, setting loading to false'); + authLog('[Auth] No user, setting loading to false'); setLoading(false); } }); @@ -304,7 +306,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { // THEN get initial session (this may trigger INITIAL_SESSION event) supabase.auth.getSession().then(({ data: { session }, error }) => { if (error) { - console.error('[Auth] Initial session fetch error:', error); + authError('[Auth] Initial session fetch error:', error); setSessionError(error.message); setLoading(false); return; @@ -312,13 +314,13 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { // Note: onAuthStateChange will handle the INITIAL_SESSION event // This is just a backup in case the event doesn't fire - console.log('[Auth] getSession completed, session exists:', !!session); + authLog('[Auth] getSession completed, session exists:', !!session); }); // Add a STRONG safety timeout to force loading to resolve loadingTimeoutRef.current = setTimeout(() => { if (loadingStateRef.current) { - console.warn('[Auth] ⚠️ SAFETY TIMEOUT: Forcing loading to false after 3 seconds'); + authWarn('[Auth] ⚠️ SAFETY TIMEOUT: Forcing loading to false after 3 seconds'); setLoading(false); } }, 3000); @@ -326,23 +328,31 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { // Session verification fallback const verificationTimeout = setTimeout(() => { if (!sessionVerifiedRef.current) { - console.log('[Auth] Session not verified after 2s, attempting manual verification'); + authLog('[Auth] Session not verified after 2s, attempting manual verification'); verifySession(); } }, 2000); - // Handle page visibility changes + // Handle page visibility changes - only verify if inactive for >5 minutes const handleVisibilityChange = () => { if (document.visibilityState === 'visible') { - console.log('[Auth] Tab became visible, verifying session'); - verifySession(); + const timeSinceLastCheck = Date.now() - lastVisibilityVerificationRef.current; + const FIVE_MINUTES = 5 * 60 * 1000; + + if (timeSinceLastCheck > FIVE_MINUTES) { + authLog('[Auth] Tab visible after 5+ minutes, verifying session'); + lastVisibilityVerificationRef.current = Date.now(); + verifySession(); + } else { + authLog('[Auth] Tab visible, session recently verified, skipping'); + } } }; document.addEventListener('visibilitychange', handleVisibilityChange); return () => { - console.log('[Auth] Cleaning up auth provider'); + authLog('[Auth] Cleaning up auth provider'); isMountedRef.current = false; subscription.unsubscribe(); clearTimeout(verificationTimeout); @@ -366,7 +376,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { const signOut = async () => { const { error } = await supabase.auth.signOut(); if (error) { - console.error('Error signing out:', error); + authError('Error signing out:', error); throw error; } }; diff --git a/src/lib/authLogger.ts b/src/lib/authLogger.ts new file mode 100644 index 00000000..7d305b7d --- /dev/null +++ b/src/lib/authLogger.ts @@ -0,0 +1,23 @@ +/** + * Conditional authentication logging utility + * Logs are only shown in development mode + */ + +const isDevelopment = import.meta.env.DEV; + +export const authLog = (...args: any[]) => { + if (isDevelopment) { + console.log(...args); + } +}; + +export const authWarn = (...args: any[]) => { + if (isDevelopment) { + console.warn(...args); + } +}; + +export const authError = (...args: any[]) => { + // Always log errors + console.error(...args); +}; diff --git a/src/lib/authStorage.ts b/src/lib/authStorage.ts index 40e5799a..cb04e7c7 100644 --- a/src/lib/authStorage.ts +++ b/src/lib/authStorage.ts @@ -1,3 +1,5 @@ +import { authLog, authWarn, authError } from './authLogger'; + /** * Custom storage adapter for Supabase authentication that handles iframe localStorage restrictions. * Falls back to sessionStorage or in-memory storage if localStorage is blocked. @@ -15,7 +17,7 @@ class AuthStorage { localStorage.removeItem('__supabase_test__'); this.storage = localStorage; this.storageType = 'localStorage'; - console.log('[AuthStorage] Using localStorage ✓'); + authLog('[AuthStorage] Using localStorage ✓'); } catch { // Try sessionStorage as fallback try { @@ -23,12 +25,12 @@ class AuthStorage { sessionStorage.removeItem('__supabase_test__'); this.storage = sessionStorage; this.storageType = 'sessionStorage'; - console.warn('[AuthStorage] localStorage blocked, using sessionStorage ⚠️'); + authWarn('[AuthStorage] localStorage blocked, using sessionStorage ⚠️'); } catch { // Use in-memory storage as last resort this.storageType = 'memory'; - console.error('[AuthStorage] Both localStorage and sessionStorage blocked, using in-memory storage ⛔'); - console.error('[AuthStorage] Sessions will NOT persist across page reloads!'); + authError('[AuthStorage] Both localStorage and sessionStorage blocked, using in-memory storage ⛔'); + authError('[AuthStorage] Sessions will NOT persist across page reloads!'); // Attempt to recover session from URL this.attemptSessionRecoveryFromURL(); @@ -51,7 +53,7 @@ class AuthStorage { const refreshToken = urlParams.get('refresh_token'); if (accessToken && refreshToken) { - console.log('[AuthStorage] Recovering session from URL parameters'); + authLog('[AuthStorage] Recovering session from URL parameters'); // Store in memory this.memoryStorage.set('sb-auth-token', JSON.stringify({ access_token: accessToken, @@ -63,23 +65,23 @@ class AuthStorage { window.history.replaceState({}, document.title, window.location.pathname); } } catch (error) { - console.error('[AuthStorage] Failed to recover session from URL:', error); + authError('[AuthStorage] Failed to recover session from URL:', error); } } private handleStorageChange(event: StorageEvent) { // Sync auth state across tabs if (event.key?.startsWith('sb-') && event.newValue) { - console.log('[AuthStorage] Syncing auth state across tabs'); + authLog('[AuthStorage] Syncing auth state across tabs'); } } getItem(key: string): string | null { - console.log('[AuthStorage] Getting key:', key); + authLog('[AuthStorage] Getting key:', key); try { if (this.storage) { const value = this.storage.getItem(key); - console.log('[AuthStorage] Retrieved from storage:', !!value); + authLog('[AuthStorage] Retrieved from storage:', !!value); if (value) { // Verify it's not expired @@ -94,7 +96,7 @@ class AuthStorage { : parsed.expires_at * 1000; // Convert from seconds to milliseconds if (parsed.expires_at && expiryTime < Date.now()) { - console.warn('[AuthStorage] Token expired, removing', { + authWarn('[AuthStorage] Token expired, removing', { expires_at: parsed.expires_at, expiryTime: new Date(expiryTime), now: new Date() @@ -103,24 +105,24 @@ class AuthStorage { return null; } - console.log('[AuthStorage] Token valid, expires:', new Date(expiryTime)); + authLog('[AuthStorage] Token valid, expires:', new Date(expiryTime)); } catch (e) { - console.warn('[AuthStorage] Could not parse token for expiry check:', e); + authWarn('[AuthStorage] Could not parse token for expiry check:', e); } } } return value; } - console.log('[AuthStorage] Using memory storage'); + authLog('[AuthStorage] Using memory storage'); return this.memoryStorage.get(key) || null; } catch (error) { - console.error('[AuthStorage] Error reading from storage:', error); + authError('[AuthStorage] Error reading from storage:', error); return this.memoryStorage.get(key) || null; } } setItem(key: string, value: string): void { - console.log('[AuthStorage] Setting key:', key); + authLog('[AuthStorage] Setting key:', key); try { if (this.storage) { this.storage.setItem(key, value); @@ -128,7 +130,7 @@ class AuthStorage { // Always keep in memory as backup this.memoryStorage.set(key, value); } catch (error) { - console.error('[AuthStorage] Error writing to storage:', error); + authError('[AuthStorage] Error writing to storage:', error); // Fallback to memory only this.memoryStorage.set(key, value); } @@ -141,7 +143,7 @@ class AuthStorage { } this.memoryStorage.delete(key); } catch (error) { - console.error('[AuthStorage] Error removing from storage:', error); + authError('[AuthStorage] Error removing from storage:', error); this.memoryStorage.delete(key); } } @@ -159,7 +161,7 @@ class AuthStorage { // Clear all auth-related storage (for force logout) clearAll(): void { - console.log('[AuthStorage] Clearing all auth storage'); + authLog('[AuthStorage] Clearing all auth storage'); try { if (this.storage) { // Get all keys from storage @@ -173,16 +175,16 @@ class AuthStorage { // Remove all Supabase auth keys keys.forEach(key => { - console.log('[AuthStorage] Removing key:', key); + authLog('[AuthStorage] Removing key:', key); this.storage!.removeItem(key); }); } // Clear memory storage this.memoryStorage.clear(); - console.log('[AuthStorage] ✓ All auth storage cleared'); + authLog('[AuthStorage] ✓ All auth storage cleared'); } catch (error) { - console.error('[AuthStorage] Error clearing storage:', error); + authError('[AuthStorage] Error clearing storage:', error); // Still clear memory storage as fallback this.memoryStorage.clear(); }