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