port-nimara-client-portal/plugins/01.auth-refresh.client.ts

192 lines
6.6 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', () => {
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) {
console.log('[AUTH_REFRESH] Tab became visible after', Math.round(timeSinceLastCheck / 1000), 'seconds, checking auth status')
checkAndScheduleRefresh()
}
lastVisibilityChange = now
}
})
}
// Clean up timer on plugin destruction
onBeforeUnmount(() => {
if (refreshTimer) {
clearTimeout(refreshTimer)
refreshTimer = null
}
})
})