From bf2361050f9dde6cf5ed73e3f495056814f2ee34 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 11 Jul 2025 11:58:38 -0400 Subject: [PATCH] feat: Enhance authentication middleware with reduced cache expiry, improved session validation, and global error handling for auth-related issues --- .gitignore | 1 + middleware/authentication.ts | 22 ++-- pages/dashboard/interest-berth-status.vue | 4 +- plugins/01.auth-refresh.client.ts | 72 +++++++++++-- plugins/02.auth-error-handler.client.ts | 119 ++++++++++++++++++++++ remove.txt | 2 + 6 files changed, 204 insertions(+), 16 deletions(-) create mode 100644 plugins/02.auth-error-handler.client.ts create mode 100644 remove.txt diff --git a/.gitignore b/.gitignore index 1241048..e867f33 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ logs .env .env.* !.env.example +nul diff --git a/middleware/authentication.ts b/middleware/authentication.ts index 2c4bda0..6006adf 100644 --- a/middleware/authentication.ts +++ b/middleware/authentication.ts @@ -20,7 +20,7 @@ export default defineNuxtRouteMiddleware(async (to) => { // Use a cached auth state to avoid excessive API calls const nuxtApp = useNuxtApp(); const cacheKey = 'auth:session:cache'; - const cacheExpiry = 15 * 60 * 1000; // 15 minutes cache (increased for better UX) + const cacheExpiry = 2 * 60 * 1000; // 2 minutes cache - reduced to prevent stale auth state // Check if we have a cached session const cachedSession = nuxtApp.payload.data?.[cacheKey]; @@ -46,14 +46,22 @@ export default defineNuxtRouteMiddleware(async (to) => { try { // Check Keycloak authentication via session API with timeout and retries const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 10000); // 10 second timeout (increased from 5) + const timeout = setTimeout(() => controller.abort(), 10000); // 10 second timeout const sessionData = await $fetch('/api/auth/session', { signal: controller.signal, - retry: 2, // Increased retry count - retryDelay: 1000, // Increased retry delay + retry: 2, + retryDelay: 1000, onRetry: ({ retries }: { retries: number }) => { console.log(`[MIDDLEWARE] Retrying auth check (attempt ${retries + 1})`) + }, + onResponseError({ response }) { + // Clear cache on auth errors + if (response.status === 401 || response.status === 403) { + console.log('[MIDDLEWARE] Auth error detected, clearing cache') + delete nuxtApp.payload.data[cacheKey]; + delete nuxtApp.payload.data.authState; + } } }) as any; @@ -106,7 +114,7 @@ export default defineNuxtRouteMiddleware(async (to) => { if (error.name === 'AbortError' || error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') { console.log('[MIDDLEWARE] Network error, checking for recent cache'); const recentCache = nuxtApp.payload.data?.[cacheKey]; - if (recentCache && recentCache.timestamp && (now - recentCache.timestamp) < 30 * 60 * 1000) { // 30 minutes grace period + if (recentCache && recentCache.timestamp && (now - recentCache.timestamp) < 5 * 60 * 1000) { // 5 minutes grace period - reduced from 30 console.log('[MIDDLEWARE] Using recent cache despite network error (age:', Math.round((now - recentCache.timestamp) / 1000), 'seconds)'); if (recentCache.authenticated && recentCache.user) { // Store auth state for components @@ -119,8 +127,8 @@ export default defineNuxtRouteMiddleware(async (to) => { groups: recentCache.groups || [] }; - // Show a warning toast if cache is older than 10 minutes - if ((now - recentCache.timestamp) > 10 * 60 * 1000) { + // Show a warning toast if cache is older than 2 minutes + if ((now - recentCache.timestamp) > 2 * 60 * 1000) { const toast = useToast(); toast.warning('Network connectivity issue - using cached authentication'); } diff --git a/pages/dashboard/interest-berth-status.vue b/pages/dashboard/interest-berth-status.vue index 24a809c..04aa1d6 100644 --- a/pages/dashboard/interest-berth-status.vue +++ b/pages/dashboard/interest-berth-status.vue @@ -116,12 +116,12 @@ -
+
{ refreshTimer = null } - // Calculate time until refresh (refresh 10 minutes before expiry for better safety margin) - const refreshBuffer = 10 * 60 * 1000 // 10 minutes in milliseconds (increased from 5) + // Calculate time until refresh (refresh 5 minutes before expiry) + const refreshBuffer = 5 * 60 * 1000 // 5 minutes in milliseconds const timeUntilRefresh = expiresAt - Date.now() - refreshBuffer console.log('[AUTH_REFRESH] Scheduling token refresh in:', Math.max(0, timeUntilRefresh), 'ms') @@ -165,15 +165,43 @@ export default defineNuxtPlugin(() => { if (typeof document !== 'undefined') { let lastVisibilityChange = Date.now() - document.addEventListener('visibilitychange', () => { + document.addEventListener('visibilitychange', async () => { if (!document.hidden) { const now = Date.now() const timeSinceLastCheck = now - lastVisibilityChange - // If tab was hidden for more than 1 minute, check auth status - if (timeSinceLastCheck > 60000) { + // If tab was hidden for more than 30 seconds, check auth status + if (timeSinceLastCheck > 30000) { console.log('[AUTH_REFRESH] Tab became visible after', Math.round(timeSinceLastCheck / 1000), 'seconds, checking auth status') - checkAndScheduleRefresh() + + // Force immediate session validation + try { + const response = await fetch('/api/auth/session', { + headers: { + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache' + } + }) + + if (!response.ok || response.status === 401) { + console.log('[AUTH_REFRESH] Session expired while tab was hidden') + await navigateTo('/login') + return + } + + const sessionData = await response.json() + if (!sessionData.authenticated) { + console.log('[AUTH_REFRESH] Not authenticated after tab visibility') + await navigateTo('/login') + return + } + + // Re-schedule refresh if session is valid + checkAndScheduleRefresh() + } catch (error) { + console.error('[AUTH_REFRESH] Failed to check session on visibility change:', error) + await navigateTo('/login') + } } lastVisibilityChange = now @@ -181,11 +209,41 @@ export default defineNuxtPlugin(() => { }) } - // Clean up timer on plugin destruction + // Add periodic session validation (every 2 minutes) + let validationInterval: NodeJS.Timeout | null = null + + onMounted(() => { + validationInterval = setInterval(async () => { + console.log('[AUTH_REFRESH] Performing periodic session validation') + + try { + const response = await fetch('/api/auth/session', { + headers: { + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache' + } + }) + + if (!response.ok || response.status === 401) { + console.log('[AUTH_REFRESH] Session invalid during periodic check') + clearInterval(validationInterval!) + await navigateTo('/login') + } + } catch (error) { + console.error('[AUTH_REFRESH] Periodic validation error:', error) + } + }, 2 * 60 * 1000) // Every 2 minutes + }) + + // Clean up timers on plugin destruction onBeforeUnmount(() => { if (refreshTimer) { clearTimeout(refreshTimer) refreshTimer = null } + if (validationInterval) { + clearInterval(validationInterval) + validationInterval = null + } }) }) diff --git a/plugins/02.auth-error-handler.client.ts b/plugins/02.auth-error-handler.client.ts new file mode 100644 index 0000000..68ade63 --- /dev/null +++ b/plugins/02.auth-error-handler.client.ts @@ -0,0 +1,119 @@ +export default defineNuxtPlugin(() => { + // Only run on client side + if (import.meta.server) return + + const nuxtApp = useNuxtApp() + const toast = useToast() + + // Global error handler for API requests + nuxtApp.hook('app:error', (error: any) => { + console.error('[AUTH_ERROR_HANDLER] Application error:', error) + + // Handle authentication errors + if (error.statusCode === 401 || error.statusCode === 403) { + handleAuthError(error) + } + }) + + // Intercept $fetch errors globally + const originalFetch = globalThis.$fetch + globalThis.$fetch = $fetch.create({ + onResponseError({ response }) { + console.log('[AUTH_ERROR_HANDLER] Response error:', { + status: response.status, + url: response.url, + statusText: response.statusText + }) + + // Handle authentication errors (401, 403) + if (response.status === 401 || response.status === 403) { + handleAuthError({ + statusCode: response.status, + statusMessage: response.statusText, + data: response._data + }) + } + + // Handle 404 errors that might be auth-related + if (response.status === 404 && isProtectedRoute()) { + console.warn('[AUTH_ERROR_HANDLER] 404 on protected route, may be auth-related') + // Check if session is still valid + checkAndHandleSession() + } + } + }) + + const handleAuthError = async (error: any) => { + console.error('[AUTH_ERROR_HANDLER] Authentication error detected:', error) + + // Clear all auth-related caches + clearAuthCaches() + + // Only show toast and redirect if we're not already on the login page + const route = useRoute() + if (route.path !== '/login' && !route.path.startsWith('/auth')) { + toast.error('Your session has expired. Please log in again.') + + // Delay navigation slightly to ensure toast is visible + setTimeout(() => { + navigateTo('/login') + }, 500) + } + } + + const clearAuthCaches = () => { + console.log('[AUTH_ERROR_HANDLER] Clearing authentication caches') + + // Clear Nuxt app payload caches + if (nuxtApp.payload.data) { + delete nuxtApp.payload.data['auth:session:cache'] + delete nuxtApp.payload.data.authState + } + + // Clear session cookie + const sessionCookie = useCookie('nuxt-oidc-auth') + sessionCookie.value = null + } + + const isProtectedRoute = () => { + const route = useRoute() + // Check if current route requires authentication + return route.meta.auth !== false && + !route.path.startsWith('/login') && + !route.path.startsWith('/auth') + } + + const checkAndHandleSession = async () => { + try { + // Force a fresh session check without cache + const response = await fetch('/api/auth/session', { + headers: { + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache' + } + }) + + if (!response.ok) { + throw new Error(`Session check failed: ${response.status}`) + } + + const sessionData = await response.json() + + if (!sessionData.authenticated) { + handleAuthError({ + statusCode: 401, + statusMessage: 'Session expired' + }) + } + } catch (error) { + console.error('[AUTH_ERROR_HANDLER] Failed to check session:', error) + handleAuthError({ + statusCode: 401, + statusMessage: 'Session check failed' + }) + } + } + + // Expose clearAuthCaches for manual use + nuxtApp.provide('clearAuthCaches', clearAuthCaches) +}) diff --git a/remove.txt b/remove.txt new file mode 100644 index 0000000..8fc5278 --- /dev/null +++ b/remove.txt @@ -0,0 +1,2 @@ +mkdir: cannot create directory ‘C:\\Users\\mpcia\\Documents\\Cline\\MCP’: File exists +hello \ No newline at end of file