feat: Implement auth logging and session verification optimizations

This commit is contained in:
gpt-engineer-app[bot]
2025-10-12 19:02:59 +00:00
parent 3a58dcf62d
commit ffb0297854
3 changed files with 84 additions and 49 deletions

View File

@@ -3,6 +3,7 @@ import type { User, Session } from '@supabase/supabase-js';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import type { Profile } from '@/types/database'; import type { Profile } from '@/types/database';
import { toast } from '@/hooks/use-toast'; import { toast } from '@/hooks/use-toast';
import { authLog, authWarn, authError } from '@/lib/authLogger';
interface AuthContextType { interface AuthContextType {
user: User | null; user: User | null;
@@ -36,6 +37,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
const previousEmailRef = useRef<string | null>(null); const previousEmailRef = useRef<string | null>(null);
const loadingTimeoutRef = useRef<NodeJS.Timeout | null>(null); const loadingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const loadingStateRef = useRef(loading); const loadingStateRef = useRef(loading);
const lastVisibilityVerificationRef = useRef<number>(Date.now());
const fetchProfile = async (userId: string, retryCount = 0, onComplete?: () => void) => { const fetchProfile = async (userId: string, retryCount = 0, onComplete?: () => void) => {
try { try {
@@ -46,12 +48,12 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
.maybeSingle(); .maybeSingle();
if (error && error.code !== 'PGRST116') { 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 // Retry up to 3 times with exponential backoff
if (retryCount < 3 && isMountedRef.current) { if (retryCount < 3 && isMountedRef.current) {
const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s 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(() => { setTimeout(() => {
if (isMountedRef.current) { if (isMountedRef.current) {
fetchProfile(userId, retryCount + 1, onComplete); fetchProfile(userId, retryCount + 1, onComplete);
@@ -62,7 +64,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
// All retries exhausted - complete anyway // All retries exhausted - complete anyway
if (isMountedRef.current) { if (isMountedRef.current) {
console.warn('[Auth] Profile fetch failed after 3 retries'); authWarn('[Auth] Profile fetch failed after 3 retries');
setProfile(null); setProfile(null);
onComplete?.(); onComplete?.();
} }
@@ -76,7 +78,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
onComplete?.(); onComplete?.();
} }
} catch (error) { } catch (error) {
console.error('[Auth] Error fetching profile:', error); authError('[Auth] Error fetching profile:', error);
// Retry logic for network errors // Retry logic for network errors
if (retryCount < 3 && isMountedRef.current) { if (retryCount < 3 && isMountedRef.current) {
@@ -112,7 +114,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
const { data: { session }, error } = await supabase.auth.getSession(); const { data: { session }, error } = await supabase.auth.getSession();
if (error) { if (error) {
console.error('[Auth] Session verification failed:', error); authError('[Auth] Session verification failed:', error);
setSessionError(error.message); setSessionError(error.message);
if (updateLoadingState && isMountedRef.current) { if (updateLoadingState && isMountedRef.current) {
setLoading(false); setLoading(false);
@@ -121,14 +123,14 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
} }
if (!session) { if (!session) {
console.log('[Auth] No active session found'); authLog('[Auth] No active session found');
if (updateLoadingState && isMountedRef.current) { if (updateLoadingState && isMountedRef.current) {
setLoading(false); setLoading(false);
} }
return false; return false;
} }
console.log('[Auth] Session verified:', session.user.email); authLog('[Auth] Session verified:', session.user.email);
sessionVerifiedRef.current = true; sessionVerifiedRef.current = true;
// Update state if session was found but not set // Update state if session was found but not set
@@ -142,7 +144,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
return true; return true;
} catch (error) { } catch (error) {
console.error('[Auth] Session verification error:', error); authError('[Auth] Session verification error:', error);
if (updateLoadingState && isMountedRef.current) { if (updateLoadingState && isMountedRef.current) {
setLoading(false); setLoading(false);
} }
@@ -153,17 +155,17 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
// Keep loading state ref in sync // Keep loading state ref in sync
useEffect(() => { useEffect(() => {
loadingStateRef.current = loading; loadingStateRef.current = loading;
console.log('[Auth] Loading state changed:', loading); authLog('[Auth] Loading state changed:', loading);
}, [loading]); }, [loading]);
useEffect(() => { useEffect(() => {
console.log('[Auth] Initializing auth provider'); authLog('[Auth] Initializing auth provider');
// CRITICAL: Set up listener FIRST to catch all events // CRITICAL: Set up listener FIRST to catch all events
const { const {
data: { subscription }, data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => { } = 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 // Extract email info early for cleanup
const currentEmail = session?.user?.email; const currentEmail = session?.user?.email;
@@ -177,18 +179,18 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
// Update session and user state based on event // Update session and user state based on event
if (event === 'SIGNED_IN' && session) { 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); setSession(session);
setUser(session.user); setUser(session.user);
sessionVerifiedRef.current = true; sessionVerifiedRef.current = true;
} else if (event === 'INITIAL_SESSION') { } else if (event === 'INITIAL_SESSION') {
if (session?.user) { if (session?.user) {
console.log('[Auth] INITIAL_SESSION with user, setting session'); authLog('[Auth] INITIAL_SESSION with user, setting session');
setSession(session); setSession(session);
setUser(session.user); setUser(session.user);
sessionVerifiedRef.current = true; sessionVerifiedRef.current = true;
} else { } 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); setSession(null);
setUser(null); setUser(null);
setProfile(null); setProfile(null);
@@ -198,7 +200,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
return; // Exit early, no need to fetch profile return; // Exit early, no need to fetch profile
} }
} else if (event === 'SIGNED_OUT') { } else if (event === 'SIGNED_OUT') {
console.log('[Auth] SIGNED_OUT detected, clearing all state'); authLog('[Auth] SIGNED_OUT detected, clearing all state');
setSession(null); setSession(null);
setUser(null); setUser(null);
setProfile(null); setProfile(null);
@@ -261,7 +263,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
}); });
} }
} catch (error) { } catch (error) {
console.error('Error updating Novu after email confirmation:', error); authError('Error updating Novu after email confirmation:', error);
} finally { } finally {
novuUpdateTimeoutRef.current = null; novuUpdateTimeoutRef.current = null;
} }
@@ -282,12 +284,12 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
// Only wait for profile on initial auth events // Only wait for profile on initial auth events
const shouldWaitForProfile = (event === 'SIGNED_IN' || event === 'INITIAL_SESSION'); const shouldWaitForProfile = (event === 'SIGNED_IN' || event === 'INITIAL_SESSION');
console.log('[Auth] Fetching profile, shouldWaitForProfile:', shouldWaitForProfile); authLog('[Auth] Fetching profile, shouldWaitForProfile:', shouldWaitForProfile);
profileFetchTimeoutRef.current = setTimeout(() => { profileFetchTimeoutRef.current = setTimeout(() => {
fetchProfile(session.user.id, 0, () => { fetchProfile(session.user.id, 0, () => {
if (shouldWaitForProfile) { if (shouldWaitForProfile) {
console.log('[Auth] Profile fetch complete, setting loading to false'); authLog('[Auth] Profile fetch complete, setting loading to false');
setLoading(false); setLoading(false);
} }
}); });
@@ -296,7 +298,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
} else { } else {
// No session/user - clear profile and resolve loading // No session/user - clear profile and resolve loading
setProfile(null); setProfile(null);
console.log('[Auth] No user, setting loading to false'); authLog('[Auth] No user, setting loading to false');
setLoading(false); setLoading(false);
} }
}); });
@@ -304,7 +306,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
// THEN get initial session (this may trigger INITIAL_SESSION event) // THEN get initial session (this may trigger INITIAL_SESSION event)
supabase.auth.getSession().then(({ data: { session }, error }) => { supabase.auth.getSession().then(({ data: { session }, error }) => {
if (error) { if (error) {
console.error('[Auth] Initial session fetch error:', error); authError('[Auth] Initial session fetch error:', error);
setSessionError(error.message); setSessionError(error.message);
setLoading(false); setLoading(false);
return; return;
@@ -312,13 +314,13 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
// Note: onAuthStateChange will handle the INITIAL_SESSION event // Note: onAuthStateChange will handle the INITIAL_SESSION event
// This is just a backup in case the event doesn't fire // 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 // Add a STRONG safety timeout to force loading to resolve
loadingTimeoutRef.current = setTimeout(() => { loadingTimeoutRef.current = setTimeout(() => {
if (loadingStateRef.current) { 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); setLoading(false);
} }
}, 3000); }, 3000);
@@ -326,23 +328,31 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
// Session verification fallback // Session verification fallback
const verificationTimeout = setTimeout(() => { const verificationTimeout = setTimeout(() => {
if (!sessionVerifiedRef.current) { 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(); verifySession();
} }
}, 2000); }, 2000);
// Handle page visibility changes // Handle page visibility changes - only verify if inactive for >5 minutes
const handleVisibilityChange = () => { const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') { if (document.visibilityState === 'visible') {
console.log('[Auth] Tab became visible, verifying session'); 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(); verifySession();
} else {
authLog('[Auth] Tab visible, session recently verified, skipping');
}
} }
}; };
document.addEventListener('visibilitychange', handleVisibilityChange); document.addEventListener('visibilitychange', handleVisibilityChange);
return () => { return () => {
console.log('[Auth] Cleaning up auth provider'); authLog('[Auth] Cleaning up auth provider');
isMountedRef.current = false; isMountedRef.current = false;
subscription.unsubscribe(); subscription.unsubscribe();
clearTimeout(verificationTimeout); clearTimeout(verificationTimeout);
@@ -366,7 +376,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
const signOut = async () => { const signOut = async () => {
const { error } = await supabase.auth.signOut(); const { error } = await supabase.auth.signOut();
if (error) { if (error) {
console.error('Error signing out:', error); authError('Error signing out:', error);
throw error; throw error;
} }
}; };

23
src/lib/authLogger.ts Normal file
View File

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

View File

@@ -1,3 +1,5 @@
import { authLog, authWarn, authError } from './authLogger';
/** /**
* Custom storage adapter for Supabase authentication that handles iframe localStorage restrictions. * Custom storage adapter for Supabase authentication that handles iframe localStorage restrictions.
* Falls back to sessionStorage or in-memory storage if localStorage is blocked. * Falls back to sessionStorage or in-memory storage if localStorage is blocked.
@@ -15,7 +17,7 @@ class AuthStorage {
localStorage.removeItem('__supabase_test__'); localStorage.removeItem('__supabase_test__');
this.storage = localStorage; this.storage = localStorage;
this.storageType = 'localStorage'; this.storageType = 'localStorage';
console.log('[AuthStorage] Using localStorage ✓'); authLog('[AuthStorage] Using localStorage ✓');
} catch { } catch {
// Try sessionStorage as fallback // Try sessionStorage as fallback
try { try {
@@ -23,12 +25,12 @@ class AuthStorage {
sessionStorage.removeItem('__supabase_test__'); sessionStorage.removeItem('__supabase_test__');
this.storage = sessionStorage; this.storage = sessionStorage;
this.storageType = 'sessionStorage'; this.storageType = 'sessionStorage';
console.warn('[AuthStorage] localStorage blocked, using sessionStorage ⚠️'); authWarn('[AuthStorage] localStorage blocked, using sessionStorage ⚠️');
} catch { } catch {
// Use in-memory storage as last resort // Use in-memory storage as last resort
this.storageType = 'memory'; this.storageType = 'memory';
console.error('[AuthStorage] Both localStorage and sessionStorage blocked, using in-memory storage ⛔'); authError('[AuthStorage] Both localStorage and sessionStorage blocked, using in-memory storage ⛔');
console.error('[AuthStorage] Sessions will NOT persist across page reloads!'); authError('[AuthStorage] Sessions will NOT persist across page reloads!');
// Attempt to recover session from URL // Attempt to recover session from URL
this.attemptSessionRecoveryFromURL(); this.attemptSessionRecoveryFromURL();
@@ -51,7 +53,7 @@ class AuthStorage {
const refreshToken = urlParams.get('refresh_token'); const refreshToken = urlParams.get('refresh_token');
if (accessToken && refreshToken) { if (accessToken && refreshToken) {
console.log('[AuthStorage] Recovering session from URL parameters'); authLog('[AuthStorage] Recovering session from URL parameters');
// Store in memory // Store in memory
this.memoryStorage.set('sb-auth-token', JSON.stringify({ this.memoryStorage.set('sb-auth-token', JSON.stringify({
access_token: accessToken, access_token: accessToken,
@@ -63,23 +65,23 @@ class AuthStorage {
window.history.replaceState({}, document.title, window.location.pathname); window.history.replaceState({}, document.title, window.location.pathname);
} }
} catch (error) { } 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) { private handleStorageChange(event: StorageEvent) {
// Sync auth state across tabs // Sync auth state across tabs
if (event.key?.startsWith('sb-') && event.newValue) { 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 { getItem(key: string): string | null {
console.log('[AuthStorage] Getting key:', key); authLog('[AuthStorage] Getting key:', key);
try { try {
if (this.storage) { if (this.storage) {
const value = this.storage.getItem(key); const value = this.storage.getItem(key);
console.log('[AuthStorage] Retrieved from storage:', !!value); authLog('[AuthStorage] Retrieved from storage:', !!value);
if (value) { if (value) {
// Verify it's not expired // Verify it's not expired
@@ -94,7 +96,7 @@ class AuthStorage {
: parsed.expires_at * 1000; // Convert from seconds to milliseconds : parsed.expires_at * 1000; // Convert from seconds to milliseconds
if (parsed.expires_at && expiryTime < Date.now()) { if (parsed.expires_at && expiryTime < Date.now()) {
console.warn('[AuthStorage] Token expired, removing', { authWarn('[AuthStorage] Token expired, removing', {
expires_at: parsed.expires_at, expires_at: parsed.expires_at,
expiryTime: new Date(expiryTime), expiryTime: new Date(expiryTime),
now: new Date() now: new Date()
@@ -103,24 +105,24 @@ class AuthStorage {
return null; return null;
} }
console.log('[AuthStorage] Token valid, expires:', new Date(expiryTime)); authLog('[AuthStorage] Token valid, expires:', new Date(expiryTime));
} catch (e) { } 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; return value;
} }
console.log('[AuthStorage] Using memory storage'); authLog('[AuthStorage] Using memory storage');
return this.memoryStorage.get(key) || null; return this.memoryStorage.get(key) || null;
} catch (error) { } catch (error) {
console.error('[AuthStorage] Error reading from storage:', error); authError('[AuthStorage] Error reading from storage:', error);
return this.memoryStorage.get(key) || null; return this.memoryStorage.get(key) || null;
} }
} }
setItem(key: string, value: string): void { setItem(key: string, value: string): void {
console.log('[AuthStorage] Setting key:', key); authLog('[AuthStorage] Setting key:', key);
try { try {
if (this.storage) { if (this.storage) {
this.storage.setItem(key, value); this.storage.setItem(key, value);
@@ -128,7 +130,7 @@ class AuthStorage {
// Always keep in memory as backup // Always keep in memory as backup
this.memoryStorage.set(key, value); this.memoryStorage.set(key, value);
} catch (error) { } catch (error) {
console.error('[AuthStorage] Error writing to storage:', error); authError('[AuthStorage] Error writing to storage:', error);
// Fallback to memory only // Fallback to memory only
this.memoryStorage.set(key, value); this.memoryStorage.set(key, value);
} }
@@ -141,7 +143,7 @@ class AuthStorage {
} }
this.memoryStorage.delete(key); this.memoryStorage.delete(key);
} catch (error) { } catch (error) {
console.error('[AuthStorage] Error removing from storage:', error); authError('[AuthStorage] Error removing from storage:', error);
this.memoryStorage.delete(key); this.memoryStorage.delete(key);
} }
} }
@@ -159,7 +161,7 @@ class AuthStorage {
// Clear all auth-related storage (for force logout) // Clear all auth-related storage (for force logout)
clearAll(): void { clearAll(): void {
console.log('[AuthStorage] Clearing all auth storage'); authLog('[AuthStorage] Clearing all auth storage');
try { try {
if (this.storage) { if (this.storage) {
// Get all keys from storage // Get all keys from storage
@@ -173,16 +175,16 @@ class AuthStorage {
// Remove all Supabase auth keys // Remove all Supabase auth keys
keys.forEach(key => { keys.forEach(key => {
console.log('[AuthStorage] Removing key:', key); authLog('[AuthStorage] Removing key:', key);
this.storage!.removeItem(key); this.storage!.removeItem(key);
}); });
} }
// Clear memory storage // Clear memory storage
this.memoryStorage.clear(); this.memoryStorage.clear();
console.log('[AuthStorage] ✓ All auth storage cleared'); authLog('[AuthStorage] ✓ All auth storage cleared');
} catch (error) { } catch (error) {
console.error('[AuthStorage] Error clearing storage:', error); authError('[AuthStorage] Error clearing storage:', error);
// Still clear memory storage as fallback // Still clear memory storage as fallback
this.memoryStorage.clear(); this.memoryStorage.clear();
} }