k8s redesign complete

This commit is contained in:
Eric Gullickson
2025-09-18 22:44:30 -05:00
parent cb98336d5e
commit 040da4c759
12 changed files with 1803 additions and 445 deletions

View File

@@ -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;

View File

@@ -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 thirdparty 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}</>;
};
};

View File

@@ -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 = {
});
}
},
};
};

View File

@@ -3,6 +3,7 @@
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useAuth0 } from '@auth0/auth0-react';
import { vehiclesApi } from '../api/vehicles.api';
import { CreateVehicleRequest, UpdateVehicleRequest } from '../types/vehicles.types';
import toast from 'react-hot-toast';
@@ -17,17 +18,20 @@ interface ApiError {
}
export const useVehicles = () => {
const { isAuthenticated, isLoading } = useAuth0();
return useQuery({
queryKey: ['vehicles'],
queryFn: vehiclesApi.getAll,
enabled: isAuthenticated && !isLoading,
});
};
export const useVehicle = (id: string) => {
const { isAuthenticated, isLoading } = useAuth0();
return useQuery({
queryKey: ['vehicles', id],
queryFn: () => vehiclesApi.getById(id),
enabled: !!id,
enabled: !!id && isAuthenticated && !isLoading,
});
};
@@ -75,4 +79,4 @@ export const useDeleteVehicle = () => {
toast.error(error.response?.data?.error || 'Failed to delete vehicle');
},
});
};
};

View File

@@ -17,7 +17,7 @@ const CarThumb: React.FC<{ color?: string }> = ({ color = "#F2EAEA" }) => (
sx={{
height: 120,
bgcolor: color,
borderRadius: 3,
borderRadius: 2,
mb: 2,
display: 'flex',
alignItems: 'center',
@@ -38,7 +38,7 @@ export const VehicleMobileCard: React.FC<VehicleMobileCardProps> = ({
return (
<Card
sx={{
borderRadius: 18,
borderRadius: 2,
overflow: 'hidden',
minWidth: compact ? 260 : 'auto',
width: compact ? 260 : '100%'
@@ -62,4 +62,4 @@ export const VehicleMobileCard: React.FC<VehicleMobileCardProps> = ({
</CardActionArea>
</Card>
);
};
};