280 lines
9.8 KiB
TypeScript
280 lines
9.8 KiB
TypeScript
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
|
|
}
|
|
})
|
|
})
|