Files
motovaultpro/frontend/src/core/auth/Auth0Provider.tsx
2025-09-18 22:44:30 -05:00

224 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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';
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 Safari/ITP: use localstorage + refresh tokens to avoid thirdparty cookie silent auth failures
cacheLocation="localstorage"
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
const initializeToken = async () => {
// 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);
} 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
interceptorId = apiClient.interceptors.request.use(async (config) => {
try {
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);
}
// Cleanup function to remove interceptor
return () => {
if (interceptorId !== undefined) {
apiClient.interceptors.request.eject(interceptorId);
}
};
}, [isAuthenticated, getAccessTokenSilently, retryCount]);
return <>{children}</>;
};