Refactor authentication to use centralized session manager

Extract session management logic from middleware into reusable SessionManager utility to improve reliability, reduce code duplication, and prevent thundering herd issues with jittered cache expiry.
This commit is contained in:
2025-07-11 14:43:50 -04:00
parent bf2361050f
commit c6f81a6686
8 changed files with 1051 additions and 139 deletions

View File

@@ -1,3 +1,5 @@
import { sessionManager } from '~/server/utils/session-manager'
export default defineNuxtRouteMiddleware(async (to) => {
// Skip auth for SSR
if (import.meta.server) return;
@@ -17,67 +19,55 @@ export default defineNuxtRouteMiddleware(async (to) => {
console.log('[MIDDLEWARE] Checking authentication for route:', to.path);
// Use a cached auth state to avoid excessive API calls
// Use session manager for centralized session handling
const nuxtApp = useNuxtApp();
const cacheKey = 'auth:session:cache';
const cacheExpiry = 2 * 60 * 1000; // 2 minutes cache - reduced to prevent stale auth state
const baseExpiry = 3 * 60 * 1000; // 3 minutes base cache
const jitter = Math.floor(Math.random() * 10000); // 0-10 seconds jitter
const cacheExpiry = baseExpiry + jitter; // Prevent thundering herd
// Check if we have a cached session
const cachedSession = nuxtApp.payload.data?.[cacheKey];
const now = Date.now();
if (cachedSession && cachedSession.timestamp && (now - cachedSession.timestamp) < cacheExpiry) {
console.log('[MIDDLEWARE] Using cached session (age:', Math.round((now - cachedSession.timestamp) / 1000), 'seconds)');
if (cachedSession.authenticated && cachedSession.user) {
// Store auth state for components
if (!nuxtApp.payload.data) {
nuxtApp.payload.data = {};
}
nuxtApp.payload.data.authState = {
user: cachedSession.user,
authenticated: cachedSession.authenticated,
groups: cachedSession.groups || []
};
return;
}
return navigateTo('/login');
}
try {
// Check Keycloak authentication via session API with timeout and retries
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // 10 second timeout
const sessionData = await $fetch('/api/auth/session', {
signal: controller.signal,
retry: 2,
retryDelay: 1000,
onRetry: ({ retries }: { retries: number }) => {
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;
// Use SessionManager for deduped session checks
const sessionData = await sessionManager.checkSession({
nuxtApp,
cacheKey,
cacheExpiry,
fetchFn: async () => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // 10 second timeout
try {
const result = await $fetch('/api/auth/session', {
signal: controller.signal,
retry: 2,
retryDelay: 1000,
onRetry: ({ retries }: { retries: number }) => {
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')
sessionManager.clearCache();
delete nuxtApp.payload.data?.authState;
}
}
}) as any;
clearTimeout(timeout);
return result;
} catch (error) {
clearTimeout(timeout);
throw error;
}
}
}) as any;
});
clearTimeout(timeout);
// Cache the session data
// Store auth state for components
if (!nuxtApp.payload.data) {
nuxtApp.payload.data = {};
}
nuxtApp.payload.data[cacheKey] = {
...sessionData,
timestamp: now
};
// Store auth state for components
nuxtApp.payload.data.authState = {
user: sessionData.user,
authenticated: sessionData.authenticated,
@@ -88,7 +78,9 @@ export default defineNuxtRouteMiddleware(async (to) => {
authenticated: sessionData.authenticated,
hasUser: !!sessionData.user,
userId: sessionData.user?.id,
groups: sessionData.groups || []
groups: sessionData.groups || [],
fromCache: sessionData.fromCache,
reason: sessionData.reason
});
if (sessionData.authenticated && sessionData.user) {
@@ -110,32 +102,10 @@ export default defineNuxtRouteMiddleware(async (to) => {
} catch (error: any) {
console.error('[MIDDLEWARE] Auth check failed:', error);
// If it's a network error or timeout, check if we have a recent cached session
if (error.name === 'AbortError' || error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
console.log('[MIDDLEWARE] Network error, checking for recent cache');
const recentCache = nuxtApp.payload.data?.[cacheKey];
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)');
if (recentCache.authenticated && recentCache.user) {
// Store auth state for components
if (!nuxtApp.payload.data) {
nuxtApp.payload.data = {};
}
nuxtApp.payload.data.authState = {
user: recentCache.user,
authenticated: recentCache.authenticated,
groups: recentCache.groups || []
};
// Show a warning toast if cache is older than 2 minutes
if ((now - recentCache.timestamp) > 2 * 60 * 1000) {
const toast = useToast();
toast.warning('Network connectivity issue - using cached authentication');
}
return;
}
}
// Show warning for cached results due to network errors
if (error.reason === 'NETWORK_ERROR_CACHED') {
const toast = useToast();
toast.warning('Network connectivity issue - using cached authentication');
}
return navigateTo('/login');