241 lines
9.5 KiB
TypeScript
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}</>;
|
|
};
|