feat: Enhance authentication middleware with reduced cache expiry, improved session validation, and global error handling for auth-related issues
This commit is contained in:
@@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
119
plugins/02.auth-error-handler.client.ts
Normal file
119
plugins/02.auth-error-handler.client.ts
Normal 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)
|
||||
})
|
||||
Reference in New Issue
Block a user