diff --git a/docs/404-and-session-fixes.md b/docs/404-and-session-fixes.md index 54a4eef..a387536 100644 --- a/docs/404-and-session-fixes.md +++ b/docs/404-and-session-fixes.md @@ -4,6 +4,7 @@ 1. **404 Error on Expenses Page** - The expenses page was returning a 404 error 2. **Session Expiration After 404** - Users were getting logged out after encountering the 404 error +3. **Immediate Session Expiration** - Users were getting logged out immediately after logging in ## Root Cause Analysis @@ -16,6 +17,11 @@ - The authentication middleware was incorrectly clearing the session cache on ALL errors (including 404s) - This caused a valid session to be invalidated when encountering any page error +### Immediate Logout Cause +- The authorization middleware was making its own API call, bypassing the session cache +- The auth refresh plugin's 2-minute periodic validation was conflicting with the 3-minute session cache +- Multiple concurrent session checks were causing race conditions + ## Fixes Implemented ### 1. Fixed Expenses Page Metadata @@ -76,6 +82,29 @@ Created a full dashboard layout with: - App bar showing user info and role badges - Proper logout functionality - Responsive design with rail mode +- Safe auth state access to prevent initialization errors + +### 5. Fixed Authorization Middleware +**File**: `middleware/authorization.ts` + +Updated to use cached auth state instead of making API calls: +```javascript +// Get auth state from authentication middleware (already cached) +const nuxtApp = useNuxtApp(); +const authState = nuxtApp.payload?.data?.authState; +``` + +This prevents: +- Duplicate API calls +- Race conditions between middlewares +- Session cache conflicts + +### 6. Adjusted Auth Refresh Plugin +**File**: `plugins/01.auth-refresh.client.ts` + +- Changed periodic validation from 2 to 5 minutes to avoid conflicts with 3-minute cache +- Added failure counting - only logs out after 3 consecutive failures +- Increased random offset to prevent thundering herd ## Expected Results @@ -83,6 +112,8 @@ Created a full dashboard layout with: 2. **404 errors won't cause session expiration** - only actual authentication failures (401) will clear the session 3. **Better error handling** - 403 errors (insufficient permissions) will redirect to dashboard with a message instead of logging out 4. **Consistent layout** across all dashboard pages +5. **No immediate logout** - Session checks are properly coordinated and cached +6. **Stable session management** - No conflicts between different auth checking mechanisms ## Testing Steps @@ -95,6 +126,18 @@ Created a full dashboard layout with: ## Additional Improvements - The authorization middleware now stores error messages that are displayed via toast -- The dashboard layout shows the current user and their role +- The authorization middleware uses cached auth state instead of making API calls +- The dashboard layout shows the current user and their role with safe access patterns - Navigation menu dynamically shows/hides items based on user roles - Session validation continues to work with the 3-minute cache + jitter to prevent race conditions +- Auth refresh plugin runs validation every 5 minutes to avoid cache conflicts +- Multiple failure tolerance prevents transient issues from logging users out + +## Timing Configuration Summary + +- **Session Cache**: 3 minutes (with 0-10 second jitter) +- **Auth Refresh Validation**: Every 5 minutes (with 0-10 second offset) +- **Token Refresh**: 5 minutes before token expiry +- **Failure Tolerance**: 3 consecutive failures before logout + +This configuration ensures no timing conflicts between different auth mechanisms. diff --git a/layouts/dashboard.vue b/layouts/dashboard.vue index 096cfde..20a2584 100644 --- a/layouts/dashboard.vue +++ b/layouts/dashboard.vue @@ -115,8 +115,15 @@ const nuxtApp = useNuxtApp(); const drawer = ref(true); const rail = ref(false); -// Get auth state -const authState = computed(() => nuxtApp.payload.data?.authState); +// Get auth state - with fallback to prevent errors +const authState = computed(() => { + const data = nuxtApp.payload?.data?.authState; + // Only return data if it's properly initialized + if (data && data.authenticated !== undefined) { + return data; + } + return null; +}); // Page title based on current route const pageTitle = computed(() => { diff --git a/middleware/authorization.ts b/middleware/authorization.ts index 16d3453..7b9b3c3 100644 --- a/middleware/authorization.ts +++ b/middleware/authorization.ts @@ -10,17 +10,29 @@ export default defineNuxtRouteMiddleware(async (to) => { console.log('[AUTHORIZATION] Checking route access for:', to.path, 'Required roles:', to.meta.roles); try { - // Get current session data with groups - const sessionData = await $fetch('/api/auth/session') as any; + // Get auth state from authentication middleware (already cached) + const nuxtApp = useNuxtApp(); + const authState = nuxtApp.payload?.data?.authState; - if (!sessionData.authenticated || !sessionData.user) { - console.log('[AUTHORIZATION] User not authenticated, redirecting to login'); - return navigateTo('/login'); + // If auth state not available, authentication middleware hasn't run or failed + if (!authState || !authState.authenticated || !authState.user) { + console.log('[AUTHORIZATION] No auth state found from authentication middleware'); + + // Try to get from session cache as fallback + const sessionCache = nuxtApp.payload?.data?.['auth:session:cache']; + if (!sessionCache || !sessionCache.authenticated) { + console.log('[AUTHORIZATION] User not authenticated, redirecting to login'); + return navigateTo('/login'); + } + + // Use cached session + authState.user = sessionCache.user; + authState.groups = sessionCache.groups || []; } // Get required roles for this route const requiredRoles = Array.isArray(to.meta.roles) ? to.meta.roles : [to.meta.roles]; - const userGroups = sessionData.groups || []; + const userGroups = authState.groups || []; // Check if user has any of the required roles const hasRequiredRole = requiredRoles.some(role => userGroups.includes(role)); @@ -29,29 +41,20 @@ export default defineNuxtRouteMiddleware(async (to) => { console.log('[AUTHORIZATION] Access denied. User groups:', userGroups, 'Required roles:', requiredRoles); // Store the error in nuxtApp to show toast on redirect - const nuxtApp = useNuxtApp(); nuxtApp.payload.authError = `Access denied. This page requires one of the following roles: ${requiredRoles.join(', ')}`; // Redirect to dashboard instead of login since user is authenticated return navigateTo('/dashboard'); } - // Store auth state in nuxtApp for use by components - const nuxtApp = useNuxtApp(); - if (!nuxtApp.payload.data) { - nuxtApp.payload.data = {}; - } - nuxtApp.payload.data.authState = { - user: sessionData.user, - authenticated: sessionData.authenticated, - groups: sessionData.groups || [] - }; - console.log('[AUTHORIZATION] Access granted for route:', to.path); } catch (error) { console.error('[AUTHORIZATION] Error checking route access:', error); - // If session check fails, redirect to login - return navigateTo('/login'); + // Don't automatically redirect to login on errors + // Let the authentication middleware handle auth failures + const toast = useToast(); + toast.error('Failed to verify permissions. Please try again.'); + return navigateTo('/dashboard'); } }); diff --git a/plugins/01.auth-refresh.client.ts b/plugins/01.auth-refresh.client.ts index 7ff550f..f5452cf 100644 --- a/plugins/01.auth-refresh.client.ts +++ b/plugins/01.auth-refresh.client.ts @@ -209,13 +209,14 @@ export default defineNuxtPlugin(() => { }) } - // Add periodic session validation (every 2 minutes with offset) + // Add periodic session validation (every 5 minutes instead of 2) let validationInterval: NodeJS.Timeout | null = null let isValidating = false // Prevent concurrent validations + let failureCount = 0 // Track consecutive failures onMounted(() => { // Add random offset to prevent all clients checking at once - const randomOffset = Math.floor(Math.random() * 5000) // 0-5 seconds + const randomOffset = Math.floor(Math.random() * 10000) // 0-10 seconds setTimeout(() => { validationInterval = setInterval(async () => { @@ -233,17 +234,34 @@ export default defineNuxtPlugin(() => { }) if (!response.ok || response.status === 401) { - console.log('[AUTH_REFRESH] Session invalid during periodic check') - clearInterval(validationInterval!) - await navigateTo('/login') + failureCount++ + console.log(`[AUTH_REFRESH] Session check failed (attempt ${failureCount}/3)`) + + // Only logout after 3 consecutive failures + if (failureCount >= 3) { + console.log('[AUTH_REFRESH] Session invalid after 3 attempts, redirecting to login') + clearInterval(validationInterval!) + await navigateTo('/login') + } + } else { + // Reset failure count on success + failureCount = 0 } } catch (error) { console.error('[AUTH_REFRESH] Periodic validation error:', error) // Don't logout on network errors - let middleware handle it + // But count it as a failure for resilience + failureCount++ + + if (failureCount >= 3) { + console.log('[AUTH_REFRESH] Too many validation errors, redirecting to login') + clearInterval(validationInterval!) + await navigateTo('/login') + } } finally { isValidating = false } - }, 2 * 60 * 1000) // Keep at 2 minutes + }, 5 * 60 * 1000) // Changed to 5 minutes to avoid conflicts with 3-minute cache }, randomOffset) })