Files
motovaultpro/frontend/src/core/auth/Auth0Provider.tsx
Eric Gullickson 8fd7973656 Fix Auth Errors
2025-09-22 10:27:10 -05:00

241 lines
9.5 KiB
TypeScript

/**
* @ai-summary Auth0 provider wrapper with API token injection
*/
import React from 'react';
import { Auth0Provider as BaseAuth0Provider, useAuth0 } from '@auth0/auth0-react';
import { useNavigate } from 'react-router-dom';
import { apiClient, setAuthReady } from '../api/client';
import { createIndexedDBAdapter } from '../utils/indexeddb-storage';
import { setAuthInitialized } from './auth-gate';
interface Auth0ProviderProps {
children: React.ReactNode;
}
export const Auth0Provider: React.FC<Auth0ProviderProps> = ({ children }) => {
const navigate = useNavigate();
const domain = import.meta.env.VITE_AUTH0_DOMAIN;
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 });
navigate(appState?.returnTo || '/dashboard');
};
return (
<BaseAuth0Provider
domain={domain}
clientId={clientId}
authorizationParams={{
// Production domain; ensure mobile devices resolve this host during testing
redirect_uri: "https://admin.motovaultpro.com/callback",
audience: audience,
scope: 'openid profile email offline_access',
}}
onRedirectCallback={onRedirectCallback}
// Mobile-optimized: use IndexedDB for better mobile compatibility
cache={createIndexedDBAdapter()}
useRefreshTokens={true}
useRefreshTokensFallback={true}
>
<TokenInjector>{children}</TokenInjector>
</BaseAuth0Provider>
);
};
// 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 [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]);
// 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++) {
try {
// Enhanced progressive strategy for mobile compatibility
let tokenOptions: any;
if (attempt === 0) {
// First attempt: try cache with shorter timeout
tokenOptions = { timeoutInSeconds: 10, cacheMode: 'on' as const };
} else if (attempt === 1) {
// Second attempt: cache with longer timeout
tokenOptions = { timeoutInSeconds: 20, cacheMode: 'on' as const };
} else if (attempt === 2) {
// Third attempt: force refresh with reasonable timeout
tokenOptions = { timeoutInSeconds: 15, cacheMode: 'off' as const };
} else if (attempt === 3) {
// Fourth attempt: force refresh with longer timeout
tokenOptions = { timeoutInSeconds: 30, cacheMode: 'off' as const };
} else {
// Final attempt: default behavior with maximum timeout
tokenOptions = { timeoutInSeconds: 45 };
}
const token = await getAccessTokenSilently(tokenOptions);
console.log(`[Mobile Auth] Token acquired successfully on attempt ${attempt + 1}`, {
cacheMode: tokenOptions.cacheMode,
timeout: tokenOptions.timeoutInSeconds
});
return token;
} catch (error: any) {
console.warn(`[Mobile Auth] Attempt ${attempt + 1}/${maxRetries} failed:`, {
error: error.message || error,
cacheMode: attempt <= 2 ? 'on' : 'off'
});
// 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...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
console.error('[Mobile Auth] All token acquisition attempts failed - authentication may be broken');
return null;
};
// 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;
if (isAuthenticated) {
// Enhanced pre-warm token cache for mobile devices with IndexedDB wait
const initializeToken = async () => {
// Wait for IndexedDB to be ready first
try {
const { indexedDBStorage } = await import('../utils/indexeddb-storage');
await indexedDBStorage.waitForReady();
console.log('[Auth] IndexedDB storage is ready');
} catch (error) {
console.warn('[Auth] IndexedDB not ready, proceeding anyway:', error);
}
// Give Auth0 more time to fully initialize on mobile devices
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
const initDelay = isMobile ? 500 : 100; // Longer delay for mobile
console.log(`[Mobile Auth] Initializing token cache (mobile: ${isMobile}, delay: ${initDelay}ms)`);
await new Promise(resolve => setTimeout(resolve, initDelay));
try {
const token = await getTokenWithRetry();
if (token) {
console.log('[Mobile Auth] 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');
setRetryCount(prev => prev + 1);
}
} catch (error) {
console.error('[Mobile Auth] Token initialization failed:', error);
setRetryCount(prev => prev + 1);
}
};
initializeToken();
// Add token to all API requests with enhanced error handling and IndexedDB wait
interceptorId = apiClient.interceptors.request.use(async (config) => {
try {
// Ensure IndexedDB is ready before getting tokens
const { indexedDBStorage } = await import('../utils/indexeddb-storage');
await indexedDBStorage.waitForReady();
const token = await getTokenWithRetry();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
setAuthReady(true);
} else {
console.error('No token available for request to:', 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);
// Allow request to proceed - backend will return 401 if needed
}
return config;
});
} else {
setRetryCount(0);
setAuthReady(false);
setAuthInitialized(false); // Reset auth gate when not authenticated
}
// Cleanup function to remove interceptor
return () => {
if (interceptorId !== undefined) {
apiClient.interceptors.request.eject(interceptorId);
}
};
}, [isAuthenticated, getAccessTokenSilently, retryCount]);
return <>{children}</>;
};