224 lines
8.6 KiB
TypeScript
224 lines
8.6 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';
|
||
|
||
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 third‑party 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}</>;
|
||
};
|