export default defineNuxtPlugin(() => { // Only run on client side if (import.meta.server) return let refreshTimer: NodeJS.Timeout | null = null let isRefreshing = false let retryCount = 0 const maxRetries = 3 const scheduleTokenRefresh = (expiresAt: number) => { // Clear existing timer if (refreshTimer) { clearTimeout(refreshTimer) refreshTimer = null } // 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') console.log('[AUTH_REFRESH] Token expires at:', new Date(expiresAt)) console.log('[AUTH_REFRESH] Will refresh at:', new Date(expiresAt - refreshBuffer)) // Only schedule if we have time left if (timeUntilRefresh > 0) { refreshTimer = setTimeout(async () => { if (isRefreshing) return try { isRefreshing = true console.log('[AUTH_REFRESH] Attempting automatic token refresh...') const response = await $fetch<{ success: boolean; expiresAt?: number }>('/api/auth/refresh', { method: 'POST', retry: 2, retryDelay: 1000 }) if (response.success && response.expiresAt) { console.log('[AUTH_REFRESH] Token refresh successful, scheduling next refresh') retryCount = 0 // Reset retry count on success scheduleTokenRefresh(response.expiresAt) } else { console.error('[AUTH_REFRESH] Token refresh failed, redirecting to login') await navigateTo('/login') } } catch (error: any) { console.error('[AUTH_REFRESH] Token refresh error:', error) // Implement exponential backoff retry if (retryCount < maxRetries) { retryCount++ const retryDelay = Math.min(1000 * Math.pow(2, retryCount), 10000) // Max 10 seconds console.log(`[AUTH_REFRESH] Retrying refresh in ${retryDelay}ms (attempt ${retryCount}/${maxRetries})`) setTimeout(() => { if (!isRefreshing) { scheduleTokenRefresh(expiresAt) } }, retryDelay) } else { console.error('[AUTH_REFRESH] Max retries reached, redirecting to login') await navigateTo('/login') } } finally { isRefreshing = false } }, timeUntilRefresh) } else { // Token already expired or very close to expiry, try immediate refresh setTimeout(async () => { if (isRefreshing) return try { isRefreshing = true console.log('[AUTH_REFRESH] Token expired, attempting immediate refresh...') const response = await $fetch<{ success: boolean; expiresAt?: number }>('/api/auth/refresh', { method: 'POST', retry: 2, retryDelay: 1000 }) if (response.success && response.expiresAt) { console.log('[AUTH_REFRESH] Immediate refresh successful') retryCount = 0 // Reset retry count on success scheduleTokenRefresh(response.expiresAt) } else { console.error('[AUTH_REFRESH] Immediate refresh failed, redirecting to login') await navigateTo('/login') } } catch (error) { console.error('[AUTH_REFRESH] Immediate refresh error:', error) // Try one more time before giving up if (retryCount === 0) { retryCount++ console.log('[AUTH_REFRESH] Retrying immediate refresh once more...') setTimeout(() => { if (!isRefreshing) { scheduleTokenRefresh(Date.now() - 1) // Force immediate refresh } }, 2000) } else { await navigateTo('/login') } } finally { isRefreshing = false } }, 100) // Small delay to avoid immediate execution } } const checkAndScheduleRefresh = async () => { try { // Use middleware cache instead of API call const nuxtApp = useNuxtApp() const authState = nuxtApp.payload?.data?.authState const sessionCache = nuxtApp.payload?.data?.['auth:session:cache'] const sessionData = authState || sessionCache if (sessionData?.authenticated) { // Get the session cookie to extract expiry time const sessionCookie = useCookie('nuxt-oidc-auth') if (sessionCookie.value) { try { const parsedSession = typeof sessionCookie.value === 'string' ? JSON.parse(sessionCookie.value) : sessionCookie.value if (parsedSession.expiresAt) { console.log('[AUTH_REFRESH] Found session with expiry:', new Date(parsedSession.expiresAt)) scheduleTokenRefresh(parsedSession.expiresAt) } } catch (parseError) { console.error('[AUTH_REFRESH] Failed to parse session cookie:', parseError) } } } else { console.log('[AUTH_REFRESH] No authenticated session found in cache') } } catch (error) { console.error('[AUTH_REFRESH] Failed to check session:', error) } } // Check authentication status and schedule refresh on plugin initialization onMounted(() => { checkAndScheduleRefresh() }) // Listen for route changes to re-check auth status const router = useRouter() router.afterEach((to) => { // Only check on protected routes if (to.meta.auth !== false) { checkAndScheduleRefresh() } }) // Listen for visibility changes to refresh when tab becomes active if (typeof document !== 'undefined') { let lastVisibilityChange = Date.now() document.addEventListener('visibilitychange', async () => { if (!document.hidden) { const now = Date.now() const timeSinceLastCheck = now - lastVisibilityChange // 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 } }) } // Add periodic session validation (every 5 minutes instead of 2) let validationInterval: NodeJS.Timeout | null = null let isValidating = false // Prevent concurrent validations let failureCount = 0 // Track consecutive failures onMounted(() => { // Add random offset to prevent all clients checking at once const randomOffset = Math.floor(Math.random() * 10000) // 0-10 seconds setTimeout(() => { validationInterval = setInterval(async () => { if (isValidating) return // Skip if already validating isValidating = true 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) { failureCount++ console.log(`[AUTH_REFRESH] Session check failed (attempt ${failureCount}/3)`) // 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) { console.error('[AUTH_REFRESH] Periodic validation error:', error) // 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 { isValidating = false } }, 5 * 60 * 1000) // Changed to 5 minutes to avoid conflicts with 3-minute cache }, randomOffset) }) // Clean up timers on plugin destruction onBeforeUnmount(() => { if (refreshTimer) { clearTimeout(refreshTimer) refreshTimer = null } if (validationInterval) { clearInterval(validationInterval) validationInterval = null } }) })