feat: Enhance authentication middleware with reduced cache expiry, improved session validation, and global error handling for auth-related issues

This commit is contained in:
Matt 2025-07-11 11:58:38 -04:00
parent 242e33f7b9
commit bf2361050f
6 changed files with 204 additions and 16 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ logs
.env
.env.*
!.env.example
nul

View File

@ -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');
}

View File

@ -116,12 +116,12 @@
</v-card-title>
<v-divider />
<v-card-text class="pa-4" style="max-height: 600px; overflow-y: auto;">
<div class="d-flex flex-column gap-6">
<div class="d-flex flex-column">
<v-card
v-for="berth in getBerthsByStatus(status.value)"
:key="berth.Id"
@click="handleBerthClick(berth)"
class="berth-kanban-card"
class="berth-kanban-card mb-4"
:color="status.color"
variant="tonal"
elevation="0"

View File

@ -14,8 +14,8 @@ export default defineNuxtPlugin(() => {
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')
// 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
}
})
})

View File

@ -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)
})

2
remove.txt Normal file
View File

@ -0,0 +1,2 @@
mkdir: cannot create directory C:\\Users\\mpcia\\Documents\\Cline\\MCP: File exists
hello