From a195fa9231093fa1ae94e443cddd419693912edf Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Sat, 14 Feb 2026 21:55:24 -0600 Subject: [PATCH 1/9] fix: allow callback route to complete Auth0 code exchange (refs #189) Co-Authored-By: Claude Opus 4.6 --- frontend/src/App.tsx | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0aa66e0..3a161dd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -557,18 +557,37 @@ function App() { ); } - // Callback route requires authentication - handled by CallbackPage component - if (isCallbackRoute && isAuthenticated) { + if (isCallbackRoute) { + if (isAuthenticated) { + return ( + + +
Processing login...
+ + }> + {mobileMode ? : } +
+ +
+ ); + } + if (mobileMode) { + return ( + + +
+
Processing login...
+
+
+
+ ); + } return ( - -
Processing login...
- - }> - {mobileMode ? : } -
- +
+
Processing login...
+
); } -- 2.49.1 From 6e493e9bc77e7c72e8c37ac13cfdd3b4f937d593 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Sat, 14 Feb 2026 21:55:54 -0600 Subject: [PATCH 2/9] fix: detect and clear stale IndexedDB auth tokens (refs #190) Co-Authored-By: Claude Opus 4.6 --- frontend/src/core/auth/Auth0Provider.tsx | 28 +++++++++++++++++++- frontend/src/core/utils/indexeddb-storage.ts | 17 ++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/frontend/src/core/auth/Auth0Provider.tsx b/frontend/src/core/auth/Auth0Provider.tsx index 7678046..a1025dc 100644 --- a/frontend/src/core/auth/Auth0Provider.tsx +++ b/frontend/src/core/auth/Auth0Provider.tsx @@ -57,8 +57,9 @@ export const Auth0Provider: React.FC = ({ children }) => { // Component to inject token into API client with mobile support const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const { getAccessTokenSilently, isAuthenticated, isLoading, user } = useAuth0(); + const { getAccessTokenSilently, isAuthenticated, isLoading, user, logout } = useAuth0(); const [retryCount, setRetryCount] = React.useState(0); + const validatingRef = React.useRef(false); // Basic component loading debug console.log('[TokenInjector] Component loaded'); @@ -116,6 +117,31 @@ const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) => return null; }; + // Prevent stale session state when cached token is no longer valid + React.useEffect(() => { + if (!isAuthenticated || isLoading || validatingRef.current) return; + + const validateToken = async () => { + validatingRef.current = true; + try { + await getAccessTokenSilently({ cacheMode: 'off', timeoutInSeconds: 10 }); + } catch (error: any) { + const errorType = error?.error || error?.message || ''; + if (errorType.includes('login_required') || errorType.includes('consent_required') || + errorType.includes('invalid_grant')) { + console.warn('[Auth] Stale token detected, clearing auth state'); + const { indexedDBStorage } = await import('../utils/indexeddb-storage'); + await indexedDBStorage.clearAll(); + logout({ openUrl: false }); + return; + } + } + validatingRef.current = false; + }; + + validateToken(); + }, [isAuthenticated, isLoading]); + // Force authentication check for devices when user seems logged in but isAuthenticated is false React.useEffect(() => { const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); diff --git a/frontend/src/core/utils/indexeddb-storage.ts b/frontend/src/core/utils/indexeddb-storage.ts index 6ce8dd6..3014e50 100644 --- a/frontend/src/core/utils/indexeddb-storage.ts +++ b/frontend/src/core/utils/indexeddb-storage.ts @@ -157,6 +157,23 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache { } } + async clearAll(): Promise { + await this.initPromise; + if (!this.db) { + this.memoryCache.clear(); + return; + } + const tx = this.db.transaction(this.storeName, 'readwrite'); + tx.objectStore(this.storeName).clear(); + await new Promise((resolve, reject) => { + tx.oncomplete = () => { + this.memoryCache.clear(); + resolve(); + }; + tx.onerror = () => reject(tx.error); + }); + } + key(index: number): string | null { const keys = Array.from(this.memoryCache.keys()); return keys[index] || null; -- 2.49.1 From 723e25e1a745a6f949064d62220b3390fbaa5c65 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Sat, 14 Feb 2026 21:56:24 -0600 Subject: [PATCH 3/9] fix: add pre-auth session clear mechanism on HomePage (refs #192) Co-Authored-By: Claude Opus 4.6 --- frontend/src/pages/HomePage.tsx | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 5091dac..ff0492a 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -6,9 +6,10 @@ import { FeaturesGrid } from './HomePage/FeaturesGrid'; import { motion } from 'framer-motion'; export const HomePage = () => { - const { loginWithRedirect, isAuthenticated } = useAuth0(); + const { loginWithRedirect, isAuthenticated, logout } = useAuth0(); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [isScrolled, setIsScrolled] = useState(false); + const [sessionCleared, setSessionCleared] = useState(false); const navigate = useNavigate(); useEffect(() => { @@ -41,6 +42,22 @@ export const HomePage = () => { navigate('/signup'); }; + const handleClearSession = async () => { + try { + const { indexedDBStorage } = await import('../core/utils/indexeddb-storage'); + await indexedDBStorage.clearAll(); + Object.keys(localStorage).forEach(key => { + if (key.startsWith('@@auth0')) localStorage.removeItem(key); + }); + logout({ openUrl: false }); + setSessionCleared(true); + setTimeout(() => setSessionCleared(false), 3000); + } catch (error) { + console.error('[HomePage] Failed to clear session:', error); + window.location.reload(); + } + }; + return (
{/* Navigation Bar */} @@ -84,6 +101,12 @@ export const HomePage = () => { > Login +
{/* Mobile Menu Button */} @@ -149,6 +172,12 @@ export const HomePage = () => { > Login + )} -- 2.49.1 From 15128bfd50ed0bbf680eaa66698d938528abdb7b Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Sat, 14 Feb 2026 21:57:28 -0600 Subject: [PATCH 4/9] fix: add missing hook dependencies for stale token effect (refs #190) Co-Authored-By: Claude Opus 4.6 --- frontend/src/core/auth/Auth0Provider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/core/auth/Auth0Provider.tsx b/frontend/src/core/auth/Auth0Provider.tsx index a1025dc..c7c743e 100644 --- a/frontend/src/core/auth/Auth0Provider.tsx +++ b/frontend/src/core/auth/Auth0Provider.tsx @@ -140,7 +140,7 @@ const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) => }; validateToken(); - }, [isAuthenticated, isLoading]); + }, [isAuthenticated, isLoading, getAccessTokenSilently, logout]); // Force authentication check for devices when user seems logged in but isAuthenticated is false React.useEffect(() => { -- 2.49.1 From db127eb24c9478d7671b64bf802412207dde6980 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Sat, 14 Feb 2026 21:59:31 -0600 Subject: [PATCH 5/9] fix: address QR review findings for token validation and clearAll reliability (refs #190) Co-Authored-By: Claude Opus 4.6 --- frontend/src/core/auth/Auth0Provider.tsx | 4 ++-- frontend/src/core/utils/indexeddb-storage.ts | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/src/core/auth/Auth0Provider.tsx b/frontend/src/core/auth/Auth0Provider.tsx index c7c743e..3c2926b 100644 --- a/frontend/src/core/auth/Auth0Provider.tsx +++ b/frontend/src/core/auth/Auth0Provider.tsx @@ -133,10 +133,10 @@ const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) => const { indexedDBStorage } = await import('../utils/indexeddb-storage'); await indexedDBStorage.clearAll(); logout({ openUrl: false }); - return; } + } finally { + validatingRef.current = false; } - validatingRef.current = false; }; validateToken(); diff --git a/frontend/src/core/utils/indexeddb-storage.ts b/frontend/src/core/utils/indexeddb-storage.ts index 3014e50..24f0d60 100644 --- a/frontend/src/core/utils/indexeddb-storage.ts +++ b/frontend/src/core/utils/indexeddb-storage.ts @@ -170,7 +170,14 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache { this.memoryCache.clear(); resolve(); }; - tx.onerror = () => reject(tx.error); + tx.onerror = () => { + this.memoryCache.clear(); + reject(tx.error); + }; + tx.onabort = () => { + this.memoryCache.clear(); + reject(new Error('Transaction aborted')); + }; }); } -- 2.49.1 From 38debaad5dd13cb9a5659893a22571e5126535e0 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Sat, 14 Feb 2026 22:09:09 -0600 Subject: [PATCH 6/9] fix: skip stale token validation during callback code exchange (refs #190) Co-Authored-By: Claude Opus 4.6 --- frontend/src/core/auth/Auth0Provider.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/core/auth/Auth0Provider.tsx b/frontend/src/core/auth/Auth0Provider.tsx index 3c2926b..9ee6477 100644 --- a/frontend/src/core/auth/Auth0Provider.tsx +++ b/frontend/src/core/auth/Auth0Provider.tsx @@ -120,6 +120,7 @@ const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) => // Prevent stale session state when cached token is no longer valid React.useEffect(() => { if (!isAuthenticated || isLoading || validatingRef.current) return; + if (window.location.pathname === '/callback') return; const validateToken = async () => { validatingRef.current = true; -- 2.49.1 From da59168d7b0bb81fe1377915aa6bcdc163e33cf1 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Sat, 14 Feb 2026 22:20:34 -0600 Subject: [PATCH 7/9] fix: IndexedDB cache broken on page reload - root cause of mobile login failure (refs #190) loadCacheFromDB used store.getAll() which returns raw values, not key-value pairs. The item.key check always failed, so memoryCache was empty after every page reload. Auth0 SDK state stored before redirect was lost on mobile Safari (no bfcache). Also fixed set()/remove() to await IDB persistence so Auth0 state is fully written before loginWithRedirect() navigates away. Added 10s timeout on callback loading state as safety net. Co-Authored-By: Claude Opus 4.6 --- frontend/src/App.tsx | 13 ++++++++ frontend/src/core/utils/indexeddb-storage.ts | 31 ++++++++++++-------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3a161dd..64be162 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -496,6 +496,16 @@ function App() { const isAuthRoute = isSignupRoute || isVerifyEmailRoute || isOnboardingRoute; const shouldShowHomePage = !isGarageRoute && !isCallbackRoute && !isAuthRoute; + const [callbackTimedOut, setCallbackTimedOut] = useState(false); + useEffect(() => { + if (isCallbackRoute && !isAuthenticated && !isLoading) { + const timer = setTimeout(() => setCallbackTimedOut(true), 10000); + return () => clearTimeout(timer); + } + setCallbackTimedOut(false); + return undefined; + }, [isCallbackRoute, isAuthenticated, isLoading]); + // Enhanced navigation handlers for mobile const handleVehicleSelect = useCallback((vehicle: Vehicle) => { setSelectedVehicle(vehicle); @@ -572,6 +582,9 @@ function App() { ); } + if (callbackTimedOut) { + return ; + } if (mobileMode) { return ( diff --git a/frontend/src/core/utils/indexeddb-storage.ts b/frontend/src/core/utils/indexeddb-storage.ts index 24f0d60..2dc10d0 100644 --- a/frontend/src/core/utils/indexeddb-storage.ts +++ b/frontend/src/core/utils/indexeddb-storage.ts @@ -71,25 +71,27 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache { return new Promise((resolve) => { const transaction = this.db!.transaction([this.storeName], 'readonly'); const store = transaction.objectStore(this.storeName); - const request = store.getAll(); + const request = store.openCursor(); + this.memoryCache.clear(); request.onsuccess = () => { - const results = request.result; - this.memoryCache.clear(); - - for (const item of results) { - if (item.key && typeof item.value === 'string') { - this.memoryCache.set(item.key, item.value); + const cursor = request.result; + if (cursor) { + const key = cursor.key as string; + const value = cursor.value; + if (typeof key === 'string' && typeof value === 'string') { + this.memoryCache.set(key, value); } + cursor.continue(); + } else { + console.log(`[IndexedDB] Loaded ${this.memoryCache.size} items into cache`); + resolve(); } - - console.log(`[IndexedDB] Loaded ${this.memoryCache.size} items into cache`); - resolve(); }; request.onerror = () => { console.warn('[IndexedDB] Failed to load cache from DB:', request.error); - resolve(); // Don't fail initialization + resolve(); }; }); } @@ -199,12 +201,15 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache { async set(key: string, value: any): Promise { await this.initPromise; - this.setItem(key, JSON.stringify(value)); + const stringValue = JSON.stringify(value); + this.memoryCache.set(key, stringValue); + await this.persistToDB(key, stringValue); } async remove(key: string): Promise { await this.initPromise; - this.removeItem(key); + this.memoryCache.delete(key); + await this.persistToDB(key, null); } // Additional methods for enhanced functionality -- 2.49.1 From b5b82db532caa07aca59f4f59239bcfc7deddfc4 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Sun, 15 Feb 2026 09:06:40 -0600 Subject: [PATCH 8/9] fix: resolve auth callback failure from IndexedDB cache issues (refs #188) Add allKeys() to IndexedDBStorage to eliminate Auth0 CacheKeyManifest fallback, revert set()/remove() to non-blocking persist, add auth error display on callback route, remove leaky force-auth-check interceptor, and migrate debug console calls to centralized logger. Co-Authored-By: Claude Opus 4.6 --- frontend/src/App.tsx | 23 ++++- frontend/src/core/auth/Auth0Provider.tsx | 102 +++---------------- frontend/src/core/utils/indexeddb-storage.ts | 35 +++++-- 3 files changed, 58 insertions(+), 102 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 64be162..fb95ed8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,7 +4,7 @@ import React, { useState, useEffect, useTransition, useCallback, lazy } from 'react'; import { useQueryClient } from '@tanstack/react-query'; -import { Routes, Route, Navigate, useLocation } from 'react-router-dom'; +import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom'; import { useAuth0 } from '@auth0/auth0-react'; import { useIsAuthInitialized } from './core/auth/auth-gate'; import { motion, AnimatePresence } from 'framer-motion'; @@ -310,11 +310,11 @@ const EditVehicleScreen: React.FC = ({ vehicle, onBack, }; function App() { - const { isLoading, isAuthenticated, user } = useAuth0(); + const { isLoading, isAuthenticated, user, error: authError } = useAuth0(); const location = useLocation(); + const navigate = useNavigate(); const isAuthGateReady = useIsAuthInitialized(); const [_isPending, startTransition] = useTransition(); - console.log('[DEBUG App] Render check - isLoading:', isLoading, 'isAuthenticated:', isAuthenticated, 'isAuthGateReady:', isAuthGateReady); // Initialize data synchronization const { prefetchForNavigation } = useDataSync(); @@ -486,7 +486,6 @@ function App() { } }, [navigateToScreen, navigateToVehicleSubScreen]); - console.log('MotoVaultPro status:', { isLoading, isAuthenticated, mobileMode, activeScreen, vehicleSubScreen, userAgent: navigator.userAgent }); const isGarageRoute = location.pathname === '/garage' || location.pathname.startsWith('/garage/'); const isCallbackRoute = location.pathname === '/callback'; @@ -568,6 +567,21 @@ function App() { } if (isCallbackRoute) { + if (authError) { + return ( + +
+
Login failed: {authError.message}
+ +
+
+ ); + } if (isAuthenticated) { return ( @@ -669,7 +683,6 @@ function App() { // Wait for auth gate to be ready before rendering protected routes // This prevents a race condition where the page renders before the auth token is ready if (!isAuthGateReady) { - console.log('[DEBUG App] Auth gate not ready yet, showing loading state'); if (mobileMode) { return ( diff --git a/frontend/src/core/auth/Auth0Provider.tsx b/frontend/src/core/auth/Auth0Provider.tsx index 9ee6477..746f5ad 100644 --- a/frontend/src/core/auth/Auth0Provider.tsx +++ b/frontend/src/core/auth/Auth0Provider.tsx @@ -8,6 +8,7 @@ import { useNavigate } from 'react-router-dom'; import { apiClient, setAuthReady } from '../api/client'; import { createIndexedDBAdapter } from '../utils/indexeddb-storage'; import { setAuthInitialized } from './auth-gate'; +import logger from '../../utils/logger'; interface Auth0ProviderProps { children: React.ReactNode; @@ -20,12 +21,8 @@ export const Auth0Provider: React.FC = ({ children }) => { const clientId = import.meta.env.VITE_AUTH0_CLIENT_ID; const audience = import.meta.env.VITE_AUTH0_AUDIENCE; - // Basic component loading debug - console.log('[Auth0Provider] Component loaded', { domain, clientId, audience }); - - const onRedirectCallback = (appState?: { returnTo?: string }) => { - console.log('[Auth0Provider] Redirect callback triggered', { appState, returnTo: appState?.returnTo }); + logger.debug('Auth0 redirect callback triggered', { returnTo: appState?.returnTo }); // Route to callback page which will check user status and redirect appropriately // Pass the intended destination as state for after status check navigate('/callback', { @@ -57,19 +54,10 @@ export const Auth0Provider: React.FC = ({ children }) => { // Component to inject token into API client with mobile support const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const { getAccessTokenSilently, isAuthenticated, isLoading, user, logout } = useAuth0(); + const { getAccessTokenSilently, isAuthenticated, isLoading, logout } = useAuth0(); const [retryCount, setRetryCount] = React.useState(0); const validatingRef = React.useRef(false); - // Basic component loading debug - console.log('[TokenInjector] Component loaded'); - - // Debug mobile authentication state - React.useEffect(() => { - const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); - console.log(`[Auth Debug] Mobile: ${isMobile}, Loading: ${isLoading}, Authenticated: ${isAuthenticated}, User: ${user ? 'present' : 'null'}`); - }, [isAuthenticated, isLoading, user]); - // Helper function to get token with enhanced retry logic for mobile devices const getTokenWithRetry = async (maxRetries = 5, delayMs = 300): Promise => { for (let attempt = 0; attempt < maxRetries; attempt++) { @@ -94,26 +82,21 @@ const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) => } const token = await getAccessTokenSilently(tokenOptions); - console.log(`[Mobile Auth] Token acquired successfully on attempt ${attempt + 1}`, { - cacheMode: tokenOptions.cacheMode, - timeout: tokenOptions.timeoutInSeconds - }); + logger.debug(`Token acquired on attempt ${attempt + 1}`); return token; } catch (error: any) { - console.warn(`[Mobile Auth] Attempt ${attempt + 1}/${maxRetries} failed:`, { - error: error.message || error, - cacheMode: attempt <= 2 ? 'on' : 'off' + logger.warn(`Token attempt ${attempt + 1}/${maxRetries} failed`, { + error: error.message || String(error), }); // Mobile-specific: longer delays and more attempts if (attempt < maxRetries - 1) { - const delay = delayMs * Math.pow(1.5, attempt); // Gentler exponential backoff - console.log(`[Mobile Auth] Waiting ${Math.round(delay)}ms before retry...`); + const delay = delayMs * Math.pow(1.5, attempt); await new Promise(resolve => setTimeout(resolve, delay)); } } } - console.error('[Mobile Auth] All token acquisition attempts failed - authentication may be broken'); + logger.error('All token acquisition attempts failed'); return null; }; @@ -130,7 +113,7 @@ const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) => const errorType = error?.error || error?.message || ''; if (errorType.includes('login_required') || errorType.includes('consent_required') || errorType.includes('invalid_grant')) { - console.warn('[Auth] Stale token detected, clearing auth state'); + logger.warn('Stale token detected, clearing auth state'); const { indexedDBStorage } = await import('../utils/indexeddb-storage'); await indexedDBStorage.clearAll(); logout({ openUrl: false }); @@ -143,55 +126,6 @@ const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) => validateToken(); }, [isAuthenticated, isLoading, getAccessTokenSilently, logout]); - // Force authentication check for devices when user seems logged in but isAuthenticated is false - React.useEffect(() => { - const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); - - // Debug current state - console.log('[Auth Debug] State check:', { - isMobile, - isLoading, - isAuthenticated, - pathname: window.location.pathname, - userAgent: navigator.userAgent.substring(0, 50) + '...' - }); - - // Trigger for mobile devices OR any device on protected route without authentication - if (!isLoading && !isAuthenticated && window.location.pathname !== '/') { - console.log('[Auth Debug] User on protected route but not authenticated, forcing token check...'); - - // Aggressive token check - const forceAuthCheck = async () => { - try { - // Try multiple approaches to get token - const token = await getAccessTokenSilently({ - cacheMode: 'off' as const, - timeoutInSeconds: 10 - }); - console.log('[Auth Debug] Force auth successful, token acquired'); - - // Manually add to API client since isAuthenticated might still be false - if (token) { - console.log('[Auth Debug] Manually adding token to API client'); - // Force add the token to subsequent requests - apiClient.interceptors.request.use((config) => { - if (!config.headers.Authorization) { - config.headers.Authorization = `Bearer ${token}`; - console.log('[Auth Debug] Token manually added to request'); - } - return config; - }); - setAuthReady(true); - } - } catch (error: any) { - console.log('[Auth Debug] Force auth failed:', error.message); - } - }; - - forceAuthCheck(); - } - }, [isLoading, isAuthenticated, getAccessTokenSilently]); - React.useEffect(() => { let interceptorId: number | undefined; @@ -202,34 +136,30 @@ const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) => try { const { indexedDBStorage } = await import('../utils/indexeddb-storage'); await indexedDBStorage.waitForReady(); - console.log('[Auth] IndexedDB storage is ready'); + logger.debug('IndexedDB storage is ready'); } catch (error) { - console.warn('[Auth] IndexedDB not ready, proceeding anyway:', error); + logger.warn('IndexedDB not ready, proceeding anyway', { error: String(error) }); } // Minimal delay only for mobile devices (desktop needs no delay since IndexedDB is already ready) const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); if (isMobile) { - // Small delay for mobile browsers to settle after IndexedDB init - console.log('[Mobile Auth] Initializing token cache (mobile: true, delay: 50ms)'); await new Promise(resolve => setTimeout(resolve, 50)); - } else { - console.log('[Auth] Initializing token cache (desktop, no delay)'); } try { const token = await getTokenWithRetry(); if (token) { - console.log('[Mobile Auth] Token pre-warming successful'); + logger.debug('Token pre-warming successful'); setRetryCount(0); setAuthReady(true); setAuthInitialized(true); // Signal that auth is fully ready } else { - console.error('[Mobile Auth] Failed to acquire token after retries - will retry on API calls'); + logger.error('Failed to acquire token after retries'); setRetryCount(prev => prev + 1); } } catch (error) { - console.error('[Mobile Auth] Token initialization failed:', error); + logger.error('Token initialization failed', { error: String(error) }); setRetryCount(prev => prev + 1); } }; @@ -248,11 +178,11 @@ const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) => config.headers.Authorization = `Bearer ${token}`; setAuthReady(true); } else { - console.error('No token available for request to:', config.url); + logger.error('No token available for request', { url: config.url }); // Allow request to proceed - backend will return 401 if needed } } catch (error: any) { - console.error('Failed to get access token for request:', error.message || error); + logger.error('Failed to get access token for request', { error: error.message || String(error) }); // Allow request to proceed - backend will return 401 if needed } return config; diff --git a/frontend/src/core/utils/indexeddb-storage.ts b/frontend/src/core/utils/indexeddb-storage.ts index 2dc10d0..417a67e 100644 --- a/frontend/src/core/utils/indexeddb-storage.ts +++ b/frontend/src/core/utils/indexeddb-storage.ts @@ -3,6 +3,8 @@ * @ai-context Replaces localStorage with IndexedDB for mobile browser compatibility */ +import logger from '../../utils/logger'; + interface StorageAdapter { getItem(key: string): string | null; setItem(key: string, value: string): void; @@ -36,9 +38,9 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache { this.db = await this.openDatabase(); await this.loadCacheFromDB(); this.isReady = true; - console.log('[IndexedDB] Storage initialized successfully'); + logger.debug('IndexedDB storage initialized successfully'); } catch (error) { - console.error('[IndexedDB] Initialization failed, using memory only:', error); + logger.error('IndexedDB initialization failed, using memory only', { error: String(error) }); this.isReady = false; } } @@ -48,7 +50,7 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache { const request = indexedDB.open(this.dbName, this.dbVersion); request.onerror = () => { - console.error(`IndexedDB open failed: ${request.error?.message}`); + logger.error(`IndexedDB open failed: ${request.error?.message}`); resolve(null as any); // Fallback to memory-only mode }; @@ -84,13 +86,13 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache { } cursor.continue(); } else { - console.log(`[IndexedDB] Loaded ${this.memoryCache.size} items into cache`); + logger.debug(`IndexedDB loaded ${this.memoryCache.size} items into cache`); resolve(); } }; request.onerror = () => { - console.warn('[IndexedDB] Failed to load cache from DB:', request.error); + logger.warn('IndexedDB failed to load cache from DB', { error: String(request.error) }); resolve(); }; }); @@ -107,14 +109,14 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache { const request = store.delete(key); request.onsuccess = () => resolve(); request.onerror = () => { - console.warn(`[IndexedDB] Failed to delete ${key}:`, request.error); + logger.warn(`IndexedDB failed to delete ${key}`, { error: String(request.error) }); resolve(); }; } else { const request = store.put(value, key); request.onsuccess = () => resolve(); request.onerror = () => { - console.warn(`[IndexedDB] Failed to persist ${key}:`, request.error); + logger.warn(`IndexedDB failed to persist ${key}`, { error: String(request.error) }); resolve(); }; } @@ -132,7 +134,7 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache { // Async persist to IndexedDB (non-blocking) if (this.isReady) { this.persistToDB(key, value).catch(error => { - console.warn(`[IndexedDB] Background persist failed for ${key}:`, error); + logger.warn(`IndexedDB background persist failed for ${key}`, { error: String(error) }); }); } } @@ -143,7 +145,7 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache { // Async remove from IndexedDB (non-blocking) if (this.isReady) { this.persistToDB(key, null).catch(error => { - console.warn(`[IndexedDB] Background removal failed for ${key}:`, error); + logger.warn(`IndexedDB background removal failed for ${key}`, { error: String(error) }); }); } } @@ -193,6 +195,11 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache { } // Auth0 Cache interface implementation + // allKeys() eliminates Auth0 SDK's CacheKeyManifest fallback (auth0-spa-js line 2319) + allKeys(): string[] { + return Array.from(this.memoryCache.keys()); + } + async get(key: string): Promise { await this.initPromise; const value = this.getItem(key); @@ -203,13 +210,19 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache { await this.initPromise; const stringValue = JSON.stringify(value); this.memoryCache.set(key, stringValue); - await this.persistToDB(key, stringValue); + // Fire-and-forget: persist to IndexedDB for page reload survival + this.persistToDB(key, stringValue).catch(error => { + logger.warn(`IndexedDB background persist failed for ${key}`, { error: String(error) }); + }); } async remove(key: string): Promise { await this.initPromise; this.memoryCache.delete(key); - await this.persistToDB(key, null); + // Fire-and-forget: remove from IndexedDB + this.persistToDB(key, null).catch(error => { + logger.warn(`IndexedDB background removal failed for ${key}`, { error: String(error) }); + }); } // Additional methods for enhanced functionality -- 2.49.1 From 850f713310132dd26acadbec67434d36bfe23124 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Sun, 15 Feb 2026 09:24:56 -0600 Subject: [PATCH 9/9] fix: prevent URL sync effects from stripping Auth0 callback params (refs #188) Root cause: React fires child effects before parent effects. App's URL sync effect called history.replaceState() on /callback, stripping the ?code= and &state= query params before Auth0Provider's useEffect could read them via hasAuthParams(). The SDK fell through to checkSession() instead of handleRedirectCallback(), silently failing with no error. Guard both URL sync effects to skip on /callback, /signup, /verify-email. Co-Authored-By: Claude Opus 4.6 --- frontend/src/App.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fb95ed8..fbbd914 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -365,17 +365,24 @@ function App() { const [showAddVehicle, setShowAddVehicle] = useState(false); // Sync browser URL to Zustand screen state on mount (enables direct URL navigation on mobile) + // Skip on auth routes -- their query params must survive until Auth0 SDK processes them useEffect(() => { - const screen = routeToScreen[window.location.pathname]; + const path = window.location.pathname; + if (path === '/callback' || path === '/signup' || path === '/verify-email') return; + const screen = routeToScreen[path]; if (screen && screen !== activeScreen) { navigateToScreen(screen, { source: 'url-sync' }); } }, []); // eslint-disable-line react-hooks/exhaustive-deps -- intentionally runs once on mount // Sync Zustand screen changes back to browser URL (enables bookmarks and URL sharing) + // Skip on auth routes -- replaceState would strip ?code= and &state= params that + // Auth0 SDK needs for handleRedirectCallback (child effects fire before parent effects) useEffect(() => { + const path = window.location.pathname; + if (path === '/callback' || path === '/signup' || path === '/verify-email') return; const targetPath = screenToRoute[activeScreen]; - if (targetPath && window.location.pathname !== targetPath) { + if (targetPath && path !== targetPath) { window.history.replaceState(null, '', targetPath); } }, [activeScreen]); -- 2.49.1