k8s redesign complete
This commit is contained in:
@@ -16,6 +16,11 @@ export const apiClient: AxiosInstance = axios.create({
|
||||
},
|
||||
});
|
||||
|
||||
// Auth readiness flag to avoid noisy 401 toasts during mobile auth initialization
|
||||
let authReady = false;
|
||||
export const setAuthReady = (ready: boolean) => { authReady = ready; };
|
||||
export const isAuthReady = () => authReady;
|
||||
|
||||
// Request interceptor for auth token with mobile debugging
|
||||
apiClient.interceptors.request.use(
|
||||
async (config: InternalAxiosRequestConfig) => {
|
||||
@@ -44,6 +49,10 @@ apiClient.interceptors.response.use(
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
|
||||
if (error.response?.status === 401) {
|
||||
// Suppress early 401 toasts until auth is ready (mobile silent auth race)
|
||||
if (!authReady) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
// Enhanced 401 handling for mobile token issues
|
||||
const errorMessage = error.response?.data?.message || '';
|
||||
const isTokenIssue = errorMessage.includes('token') || errorMessage.includes('JWT') || errorMessage.includes('Unauthorized');
|
||||
@@ -72,4 +81,4 @@ apiClient.interceptors.response.use(
|
||||
}
|
||||
);
|
||||
|
||||
export default apiClient;
|
||||
export default apiClient;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import React from 'react';
|
||||
import { Auth0Provider as BaseAuth0Provider, useAuth0 } from '@auth0/auth0-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { apiClient } from '../api/client';
|
||||
import { apiClient, setAuthReady } from '../api/client';
|
||||
|
||||
interface Auth0ProviderProps {
|
||||
children: React.ReactNode;
|
||||
@@ -18,8 +18,12 @@ 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 });
|
||||
navigate(appState?.returnTo || '/dashboard');
|
||||
};
|
||||
|
||||
@@ -28,12 +32,16 @@ export const Auth0Provider: React.FC<Auth0ProviderProps> = ({ children }) => {
|
||||
domain={domain}
|
||||
clientId={clientId}
|
||||
authorizationParams={{
|
||||
redirect_uri: window.location.hostname === "admin.motovaultpro.com" ? "https://admin.motovaultpro.com/callback" : window.location.origin + "/callback",
|
||||
// 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>
|
||||
@@ -42,64 +50,139 @@ 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 } = useAuth0();
|
||||
const { getAccessTokenSilently, isAuthenticated, isLoading, user } = useAuth0();
|
||||
const [retryCount, setRetryCount] = React.useState(0);
|
||||
|
||||
// Helper function to get token with retry logic for mobile devices
|
||||
const getTokenWithRetry = async (maxRetries = 3, delayMs = 500): Promise<string | null> => {
|
||||
// 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 {
|
||||
// Progressive fallback strategy for mobile compatibility
|
||||
let tokenOptions;
|
||||
// Enhanced progressive strategy for mobile compatibility
|
||||
let tokenOptions: any;
|
||||
if (attempt === 0) {
|
||||
// First attempt: try cache first
|
||||
tokenOptions = { timeoutInSeconds: 15, cacheMode: 'on' as const };
|
||||
// First attempt: try cache with shorter timeout
|
||||
tokenOptions = { timeoutInSeconds: 10, cacheMode: 'on' as const };
|
||||
} else if (attempt === 1) {
|
||||
// Second attempt: force refresh
|
||||
tokenOptions = { timeoutInSeconds: 20, cacheMode: 'off' as const };
|
||||
// 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 longer timeout
|
||||
tokenOptions = { timeoutInSeconds: 30 };
|
||||
// Final attempt: default behavior with maximum timeout
|
||||
tokenOptions = { timeoutInSeconds: 45 };
|
||||
}
|
||||
|
||||
const token = await getAccessTokenSilently(tokenOptions);
|
||||
console.log(`Token acquired successfully on attempt ${attempt + 1}`);
|
||||
console.log(`[Mobile Auth] Token acquired successfully on attempt ${attempt + 1}`, {
|
||||
cacheMode: tokenOptions.cacheMode,
|
||||
timeout: tokenOptions.timeoutInSeconds
|
||||
});
|
||||
return token;
|
||||
} catch (error: any) {
|
||||
console.warn(`Token acquisition attempt ${attempt + 1} failed:`, error.message || error);
|
||||
|
||||
// On mobile, Auth0 might need more time - wait and retry
|
||||
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(2, attempt); // Exponential backoff
|
||||
console.log(`Waiting ${delay}ms before retry...`);
|
||||
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('All token acquisition attempts failed');
|
||||
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) {
|
||||
// Pre-warm token cache for mobile devices with delay
|
||||
// Enhanced pre-warm token cache for mobile devices
|
||||
const initializeToken = async () => {
|
||||
// Give Auth0 a moment to fully initialize on mobile
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 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('Token pre-warming successful');
|
||||
console.log('[Mobile Auth] Token pre-warming successful');
|
||||
setRetryCount(0);
|
||||
setAuthReady(true);
|
||||
} else {
|
||||
console.error('Failed to acquire token after retries - will retry on API calls');
|
||||
console.error('[Mobile Auth] Failed to acquire token after retries - will retry on API calls');
|
||||
setRetryCount(prev => prev + 1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token initialization failed:', error);
|
||||
console.error('[Mobile Auth] Token initialization failed:', error);
|
||||
setRetryCount(prev => prev + 1);
|
||||
}
|
||||
};
|
||||
@@ -112,6 +195,7 @@ const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) =>
|
||||
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
|
||||
@@ -124,6 +208,7 @@ const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) =>
|
||||
});
|
||||
} else {
|
||||
setRetryCount(0);
|
||||
setAuthReady(false);
|
||||
}
|
||||
|
||||
// Cleanup function to remove interceptor
|
||||
@@ -135,4 +220,4 @@ const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) =>
|
||||
}, [isAuthenticated, getAccessTokenSilently, retryCount]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import { QueryClient, QueryCache, MutationCache } from '@tanstack/react-query';
|
||||
import { isAuthReady } from '../api/client';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
// Mobile detection utility
|
||||
@@ -17,7 +18,7 @@ const handleQueryError = (error: any) => {
|
||||
|
||||
if (error?.response?.status === 401) {
|
||||
// Token refresh handled by Auth0Provider
|
||||
if (isMobile) {
|
||||
if (isMobile && isAuthReady()) {
|
||||
toast.error('Refreshing session...', {
|
||||
duration: 2000,
|
||||
id: 'mobile-auth-refresh'
|
||||
@@ -145,4 +146,4 @@ export const queryPerformanceMonitor = {
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user