fix: resolve auth callback failure from IndexedDB cache issues (refs #188)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 3m23s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 52s
Deploy to Staging / Verify Staging (pull_request) Successful in 9s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 3m23s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 52s
Deploy to Staging / Verify Staging (pull_request) Successful in 9s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<EditVehicleScreenProps> = ({ 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 (
|
||||
<ThemeProvider>
|
||||
<div className="flex flex-col items-center justify-center min-h-screen gap-4 px-4">
|
||||
<div className="text-lg text-red-600 text-center">Login failed: {authError.message}</div>
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Return to Homepage
|
||||
</button>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
if (isAuthenticated) {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
@@ -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 (
|
||||
<ThemeProvider>
|
||||
|
||||
@@ -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<Auth0ProviderProps> = ({ 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<Auth0ProviderProps> = ({ 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<any> => {
|
||||
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;
|
||||
|
||||
@@ -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<any> {
|
||||
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<void> {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user