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.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
nul
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||||
// Use a cached auth state to avoid excessive API calls
|
// Use a cached auth state to avoid excessive API calls
|
||||||
const nuxtApp = useNuxtApp();
|
const nuxtApp = useNuxtApp();
|
||||||
const cacheKey = 'auth:session:cache';
|
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
|
// Check if we have a cached session
|
||||||
const cachedSession = nuxtApp.payload.data?.[cacheKey];
|
const cachedSession = nuxtApp.payload.data?.[cacheKey];
|
||||||
|
|
@ -46,14 +46,22 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||||
try {
|
try {
|
||||||
// Check Keycloak authentication via session API with timeout and retries
|
// Check Keycloak authentication via session API with timeout and retries
|
||||||
const controller = new AbortController();
|
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', {
|
const sessionData = await $fetch('/api/auth/session', {
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
retry: 2, // Increased retry count
|
retry: 2,
|
||||||
retryDelay: 1000, // Increased retry delay
|
retryDelay: 1000,
|
||||||
onRetry: ({ retries }: { retries: number }) => {
|
onRetry: ({ retries }: { retries: number }) => {
|
||||||
console.log(`[MIDDLEWARE] Retrying auth check (attempt ${retries + 1})`)
|
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;
|
}) as any;
|
||||||
|
|
||||||
|
|
@ -106,7 +114,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||||
if (error.name === 'AbortError' || error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
|
if (error.name === 'AbortError' || error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
|
||||||
console.log('[MIDDLEWARE] Network error, checking for recent cache');
|
console.log('[MIDDLEWARE] Network error, checking for recent cache');
|
||||||
const recentCache = nuxtApp.payload.data?.[cacheKey];
|
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)');
|
console.log('[MIDDLEWARE] Using recent cache despite network error (age:', Math.round((now - recentCache.timestamp) / 1000), 'seconds)');
|
||||||
if (recentCache.authenticated && recentCache.user) {
|
if (recentCache.authenticated && recentCache.user) {
|
||||||
// Store auth state for components
|
// Store auth state for components
|
||||||
|
|
@ -119,8 +127,8 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||||
groups: recentCache.groups || []
|
groups: recentCache.groups || []
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show a warning toast if cache is older than 10 minutes
|
// Show a warning toast if cache is older than 2 minutes
|
||||||
if ((now - recentCache.timestamp) > 10 * 60 * 1000) {
|
if ((now - recentCache.timestamp) > 2 * 60 * 1000) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
toast.warning('Network connectivity issue - using cached authentication');
|
toast.warning('Network connectivity issue - using cached authentication');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,12 +116,12 @@
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-divider />
|
<v-divider />
|
||||||
<v-card-text class="pa-4" style="max-height: 600px; overflow-y: auto;">
|
<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-card
|
||||||
v-for="berth in getBerthsByStatus(status.value)"
|
v-for="berth in getBerthsByStatus(status.value)"
|
||||||
:key="berth.Id"
|
:key="berth.Id"
|
||||||
@click="handleBerthClick(berth)"
|
@click="handleBerthClick(berth)"
|
||||||
class="berth-kanban-card"
|
class="berth-kanban-card mb-4"
|
||||||
:color="status.color"
|
:color="status.color"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
elevation="0"
|
elevation="0"
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ export default defineNuxtPlugin(() => {
|
||||||
refreshTimer = null
|
refreshTimer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate time until refresh (refresh 10 minutes before expiry for better safety margin)
|
// Calculate time until refresh (refresh 5 minutes before expiry)
|
||||||
const refreshBuffer = 10 * 60 * 1000 // 10 minutes in milliseconds (increased from 5)
|
const refreshBuffer = 5 * 60 * 1000 // 5 minutes in milliseconds
|
||||||
const timeUntilRefresh = expiresAt - Date.now() - refreshBuffer
|
const timeUntilRefresh = expiresAt - Date.now() - refreshBuffer
|
||||||
|
|
||||||
console.log('[AUTH_REFRESH] Scheduling token refresh in:', Math.max(0, timeUntilRefresh), 'ms')
|
console.log('[AUTH_REFRESH] Scheduling token refresh in:', Math.max(0, timeUntilRefresh), 'ms')
|
||||||
|
|
@ -165,15 +165,43 @@ export default defineNuxtPlugin(() => {
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== 'undefined') {
|
||||||
let lastVisibilityChange = Date.now()
|
let lastVisibilityChange = Date.now()
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', async () => {
|
||||||
if (!document.hidden) {
|
if (!document.hidden) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const timeSinceLastCheck = now - lastVisibilityChange
|
const timeSinceLastCheck = now - lastVisibilityChange
|
||||||
|
|
||||||
// If tab was hidden for more than 1 minute, check auth status
|
// If tab was hidden for more than 30 seconds, check auth status
|
||||||
if (timeSinceLastCheck > 60000) {
|
if (timeSinceLastCheck > 30000) {
|
||||||
console.log('[AUTH_REFRESH] Tab became visible after', Math.round(timeSinceLastCheck / 1000), 'seconds, checking auth status')
|
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
|
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(() => {
|
onBeforeUnmount(() => {
|
||||||
if (refreshTimer) {
|
if (refreshTimer) {
|
||||||
clearTimeout(refreshTimer)
|
clearTimeout(refreshTimer)
|
||||||
refreshTimer = null
|
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