feat: Address 404 errors and session management issues, improve authorization middleware to use cached auth state, and adjust auth refresh plugin for better session validation
This commit is contained in:
parent
7ee2cb3368
commit
eb1d853327
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
1. **404 Error on Expenses Page** - The expenses page was returning a 404 error
|
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
|
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
|
## Root Cause Analysis
|
||||||
|
|
||||||
|
|
@ -16,6 +17,11 @@
|
||||||
- The authentication middleware was incorrectly clearing the session cache on ALL errors (including 404s)
|
- 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
|
- 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
|
## Fixes Implemented
|
||||||
|
|
||||||
### 1. Fixed Expenses Page Metadata
|
### 1. Fixed Expenses Page Metadata
|
||||||
|
|
@ -76,6 +82,29 @@ Created a full dashboard layout with:
|
||||||
- App bar showing user info and role badges
|
- App bar showing user info and role badges
|
||||||
- Proper logout functionality
|
- Proper logout functionality
|
||||||
- Responsive design with rail mode
|
- 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
|
## 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
|
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
|
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
|
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
|
## Testing Steps
|
||||||
|
|
||||||
|
|
@ -95,6 +126,18 @@ Created a full dashboard layout with:
|
||||||
## Additional Improvements
|
## Additional Improvements
|
||||||
|
|
||||||
- The authorization middleware now stores error messages that are displayed via toast
|
- 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
|
- 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
|
- 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.
|
||||||
|
|
|
||||||
|
|
@ -115,8 +115,15 @@ const nuxtApp = useNuxtApp();
|
||||||
const drawer = ref(true);
|
const drawer = ref(true);
|
||||||
const rail = ref(false);
|
const rail = ref(false);
|
||||||
|
|
||||||
// Get auth state
|
// Get auth state - with fallback to prevent errors
|
||||||
const authState = computed(() => nuxtApp.payload.data?.authState);
|
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
|
// Page title based on current route
|
||||||
const pageTitle = computed(() => {
|
const pageTitle = computed(() => {
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,29 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||||
console.log('[AUTHORIZATION] Checking route access for:', to.path, 'Required roles:', to.meta.roles);
|
console.log('[AUTHORIZATION] Checking route access for:', to.path, 'Required roles:', to.meta.roles);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get current session data with groups
|
// Get auth state from authentication middleware (already cached)
|
||||||
const sessionData = await $fetch('/api/auth/session') as any;
|
const nuxtApp = useNuxtApp();
|
||||||
|
const authState = nuxtApp.payload?.data?.authState;
|
||||||
|
|
||||||
if (!sessionData.authenticated || !sessionData.user) {
|
// If auth state not available, authentication middleware hasn't run or failed
|
||||||
console.log('[AUTHORIZATION] User not authenticated, redirecting to login');
|
if (!authState || !authState.authenticated || !authState.user) {
|
||||||
return navigateTo('/login');
|
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
|
// Get required roles for this route
|
||||||
const requiredRoles = Array.isArray(to.meta.roles) ? to.meta.roles : [to.meta.roles];
|
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
|
// Check if user has any of the required roles
|
||||||
const hasRequiredRole = requiredRoles.some(role => userGroups.includes(role));
|
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);
|
console.log('[AUTHORIZATION] Access denied. User groups:', userGroups, 'Required roles:', requiredRoles);
|
||||||
|
|
||||||
// Store the error in nuxtApp to show toast on redirect
|
// 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(', ')}`;
|
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
|
// Redirect to dashboard instead of login since user is authenticated
|
||||||
return navigateTo('/dashboard');
|
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);
|
console.log('[AUTHORIZATION] Access granted for route:', to.path);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[AUTHORIZATION] Error checking route access:', error);
|
console.error('[AUTHORIZATION] Error checking route access:', error);
|
||||||
|
|
||||||
// If session check fails, redirect to login
|
// Don't automatically redirect to login on errors
|
||||||
return navigateTo('/login');
|
// Let the authentication middleware handle auth failures
|
||||||
|
const toast = useToast();
|
||||||
|
toast.error('Failed to verify permissions. Please try again.');
|
||||||
|
return navigateTo('/dashboard');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 validationInterval: NodeJS.Timeout | null = null
|
||||||
let isValidating = false // Prevent concurrent validations
|
let isValidating = false // Prevent concurrent validations
|
||||||
|
let failureCount = 0 // Track consecutive failures
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Add random offset to prevent all clients checking at once
|
// 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(() => {
|
setTimeout(() => {
|
||||||
validationInterval = setInterval(async () => {
|
validationInterval = setInterval(async () => {
|
||||||
|
|
@ -233,17 +234,34 @@ export default defineNuxtPlugin(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok || response.status === 401) {
|
if (!response.ok || response.status === 401) {
|
||||||
console.log('[AUTH_REFRESH] Session invalid during periodic check')
|
failureCount++
|
||||||
clearInterval(validationInterval!)
|
console.log(`[AUTH_REFRESH] Session check failed (attempt ${failureCount}/3)`)
|
||||||
await navigateTo('/login')
|
|
||||||
|
// 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) {
|
} catch (error) {
|
||||||
console.error('[AUTH_REFRESH] Periodic validation error:', error)
|
console.error('[AUTH_REFRESH] Periodic validation error:', error)
|
||||||
// Don't logout on network errors - let middleware handle it
|
// 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 {
|
} finally {
|
||||||
isValidating = false
|
isValidating = false
|
||||||
}
|
}
|
||||||
}, 2 * 60 * 1000) // Keep at 2 minutes
|
}, 5 * 60 * 1000) // Changed to 5 minutes to avoid conflicts with 3-minute cache
|
||||||
}, randomOffset)
|
}, randomOffset)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue