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. */ class AuthStorage { private storage: Storage | null = null; private memoryStorage: Map = new Map(); private storageType: 'localStorage' | 'sessionStorage' | 'memory' = 'memory'; private sessionRecoveryAttempted = false; constructor() { // Try localStorage first try { localStorage.setItem('__supabase_test__', 'test'); localStorage.removeItem('__supabase_test__'); this.storage = localStorage; this.storageType = 'localStorage'; authLog('[AuthStorage] Using localStorage ✓'); } catch { // Try sessionStorage as fallback try { sessionStorage.setItem('__supabase_test__', 'test'); sessionStorage.removeItem('__supabase_test__'); this.storage = sessionStorage; this.storageType = 'sessionStorage'; authWarn('[AuthStorage] localStorage blocked, using sessionStorage ⚠️'); } catch { // Use in-memory storage as last resort this.storageType = 'memory'; 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(); } } // Listen for storage events to sync across tabs (when possible) if (this.storage) { window.addEventListener('storage', this.handleStorageChange.bind(this)); } } private attemptSessionRecoveryFromURL() { if (this.sessionRecoveryAttempted) return; this.sessionRecoveryAttempted = true; try { const urlParams = new URLSearchParams(window.location.hash.substring(1)); const accessToken = urlParams.get('access_token'); const refreshToken = urlParams.get('refresh_token'); if (accessToken && refreshToken) { authLog('[AuthStorage] Recovering session from URL parameters'); // Store in memory this.memoryStorage.set('sb-auth-token', JSON.stringify({ access_token: accessToken, refresh_token: refreshToken, expires_at: Date.now() + 3600000, // 1 hour })); // Clean URL window.history.replaceState({}, document.title, window.location.pathname); } } catch (error: unknown) { 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) { authLog('[AuthStorage] Syncing auth state across tabs'); } } getItem(key: string): string | null { authLog('[AuthStorage] Getting key:', key); try { if (this.storage) { const value = this.storage.getItem(key); authLog('[AuthStorage] Retrieved from storage:', !!value); if (value) { // Verify it's not expired if (key.includes('auth-token')) { try { const parsed = JSON.parse(value); // Supabase stores expires_at in seconds, Date.now() is in milliseconds // Check if expires_at is in seconds (< year 3000 in milliseconds) const expiryTime = parsed.expires_at > 10000000000 ? parsed.expires_at // Already in milliseconds : parsed.expires_at * 1000; // Convert from seconds to milliseconds if (parsed.expires_at && expiryTime < Date.now()) { authWarn('[AuthStorage] Token expired, removing', { expires_at: parsed.expires_at, expiryTime: new Date(expiryTime), now: new Date() }); this.removeItem(key); return null; } authLog('[AuthStorage] Token valid, expires:', new Date(expiryTime)); } catch (e) { authWarn('[AuthStorage] Could not parse token for expiry check:', e); } } } return value; } authLog('[AuthStorage] Using memory storage'); return this.memoryStorage.get(key) || null; } catch (error: unknown) { authError('[AuthStorage] Error reading from storage:', error); return this.memoryStorage.get(key) || null; } } setItem(key: string, value: string): void { authLog('[AuthStorage] Setting key:', key); try { if (this.storage) { this.storage.setItem(key, value); } // Always keep in memory as backup this.memoryStorage.set(key, value); } catch (error: unknown) { authError('[AuthStorage] Error writing to storage:', error); // Fallback to memory only this.memoryStorage.set(key, value); } } removeItem(key: string): void { try { if (this.storage) { this.storage.removeItem(key); } this.memoryStorage.delete(key); } catch (error: unknown) { authError('[AuthStorage] Error removing from storage:', error); this.memoryStorage.delete(key); } } // Get storage status for diagnostics getStorageStatus(): { type: string; persistent: boolean; warning: string | null } { return { type: this.storageType, persistent: this.storageType !== 'memory', warning: this.storageType === 'memory' ? 'Sessions will not persist across page reloads. Please enable cookies/storage for this site.' : null }; } // Clear all auth-related storage (for force logout) clearAll(): void { authLog('[AuthStorage] Clearing all auth storage'); try { if (this.storage) { // Get all keys from storage const keys: string[] = []; for (let i = 0; i < this.storage.length; i++) { const key = this.storage.key(i); if (key?.startsWith('sb-')) { keys.push(key); } } // Remove all Supabase auth keys keys.forEach(key => { authLog('[AuthStorage] Removing key:', key); this.storage!.removeItem(key); }); } // Clear memory storage this.memoryStorage.clear(); authLog('[AuthStorage] ✓ All auth storage cleared'); } catch (error: unknown) { authError('[AuthStorage] Error clearing storage:', error); // Still clear memory storage as fallback this.memoryStorage.clear(); } } } export const authStorage = new AuthStorage();