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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user