feat: Enhance authentication middleware with reduced cache expiry, improved session validation, and global error handling for auth-related issues
This commit is contained in:
parent
242e33f7b9
commit
bf2361050f
|
|
@ -13,3 +13,4 @@ logs
|
|||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
nul
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
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
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
mkdir: cannot create directory ‘C:\\Users\\mpcia\\Documents\\Cline\\MCP’: File exists
|
||||
hello
|
||||
Loading…
Reference in New Issue