diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0aa66e0..fbbd914 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(); @@ -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]); @@ -486,7 +493,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'; @@ -496,6 +502,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); @@ -557,18 +573,55 @@ function App() { ); } - // Callback route requires authentication - handled by CallbackPage component - if (isCallbackRoute && isAuthenticated) { + if (isCallbackRoute) { + if (authError) { + return ( + +
+
Login failed: {authError.message}
+ +
+
+ ); + } + if (isAuthenticated) { + return ( + + +
Processing login...
+ + }> + {mobileMode ? : } +
+ +
+ ); + } + if (callbackTimedOut) { + return ; + } + if (mobileMode) { + return ( + + +
+
Processing login...
+
+
+
+ ); + } return ( - -
Processing login...
- - }> - {mobileMode ? : } -
- +
+
Processing login...
+
); } @@ -637,7 +690,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 7678046..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,17 +54,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, logout } = useAuth0(); const [retryCount, setRetryCount] = React.useState(0); - - // 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]); + const validatingRef = React.useRef(false); // Helper function to get token with enhanced retry logic for mobile devices const getTokenWithRetry = async (maxRetries = 5, delayMs = 300): Promise => { @@ -93,77 +82,49 @@ 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; }; - // Force authentication check for devices when user seems logged in but isAuthenticated is false + // Prevent stale session state when cached token is no longer valid React.useEffect(() => { - const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + if (!isAuthenticated || isLoading || validatingRef.current) return; + if (window.location.pathname === '/callback') return; - // 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); + 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')) { + logger.warn('Stale token detected, clearing auth state'); + const { indexedDBStorage } = await import('../utils/indexeddb-storage'); + await indexedDBStorage.clearAll(); + logout({ openUrl: false }); } - }; + } finally { + validatingRef.current = false; + } + }; - forceAuthCheck(); - } - }, [isLoading, isAuthenticated, getAccessTokenSilently]); + validateToken(); + }, [isAuthenticated, isLoading, getAccessTokenSilently, logout]); React.useEffect(() => { let interceptorId: number | undefined; @@ -175,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); } }; @@ -221,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 6ce8dd6..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 }; @@ -71,25 +73,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 { + logger.debug(`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 + logger.warn('IndexedDB failed to load cache from DB', { error: String(request.error) }); + resolve(); }; }); } @@ -105,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(); }; } @@ -130,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) }); }); } } @@ -141,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) }); }); } } @@ -157,6 +161,30 @@ 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 = () => { + this.memoryCache.clear(); + reject(tx.error); + }; + tx.onabort = () => { + this.memoryCache.clear(); + reject(new Error('Transaction aborted')); + }; + }); + } + key(index: number): string | null { const keys = Array.from(this.memoryCache.keys()); return keys[index] || null; @@ -167,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); @@ -175,12 +208,21 @@ 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); + // 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.removeItem(key); + this.memoryCache.delete(key); + // 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 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 + )}