diff --git a/middleware/authentication.ts b/middleware/authentication.ts index af54805..2c73914 100644 --- a/middleware/authentication.ts +++ b/middleware/authentication.ts @@ -20,7 +20,7 @@ export default defineNuxtRouteMiddleware(async (to) => { // Use a cached auth state to avoid excessive API calls const nuxtApp = useNuxtApp(); const cacheKey = 'auth:session:cache'; - const cacheExpiry = 30000; // 30 seconds cache + const cacheExpiry = 5 * 60 * 1000; // 5 minutes cache (increased from 30 seconds) // Check if we have a cached session const cachedSession = nuxtApp.payload.data?.[cacheKey]; @@ -44,14 +44,17 @@ export default defineNuxtRouteMiddleware(async (to) => { } try { - // Check Keycloak authentication via session API with timeout + // Check Keycloak authentication via session API with timeout and retries const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 5000); // 5 second timeout + const timeout = setTimeout(() => controller.abort(), 10000); // 10 second timeout (increased from 5) const sessionData = await $fetch('/api/auth/session', { signal: controller.signal, - retry: 1, - retryDelay: 500 + retry: 2, // Increased retry count + retryDelay: 1000, // Increased retry delay + onRetry: ({ retries }: { retries: number }) => { + console.log(`[MIDDLEWARE] Retrying auth check (attempt ${retries + 1})`) + } }) as any; clearTimeout(timeout); @@ -100,11 +103,11 @@ export default defineNuxtRouteMiddleware(async (to) => { 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') { + 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) < 300000) { // 5 minutes - console.log('[MIDDLEWARE] Using recent cache despite network error'); + if (recentCache && recentCache.timestamp && (now - recentCache.timestamp) < 30 * 60 * 1000) { // 30 minutes grace period + 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) { @@ -115,6 +118,13 @@ export default defineNuxtRouteMiddleware(async (to) => { authenticated: recentCache.authenticated, groups: recentCache.groups || [] }; + + // Show a warning toast if cache is older than 10 minutes + if ((now - recentCache.timestamp) > 10 * 60 * 1000) { + const toast = useToast(); + toast.warning('Network connectivity issue - using cached authentication'); + } + return; } } diff --git a/plugins/01.auth-refresh.client.ts b/plugins/01.auth-refresh.client.ts index 99b59e3..33a9f3b 100644 --- a/plugins/01.auth-refresh.client.ts +++ b/plugins/01.auth-refresh.client.ts @@ -4,6 +4,8 @@ export default defineNuxtPlugin(() => { let refreshTimer: NodeJS.Timeout | null = null let isRefreshing = false + let retryCount = 0 + const maxRetries = 3 const scheduleTokenRefresh = (expiresAt: number) => { // Clear existing timer @@ -12,11 +14,13 @@ export default defineNuxtPlugin(() => { refreshTimer = null } - // Calculate time until refresh (refresh 2 minutes before expiry) - const refreshBuffer = 2 * 60 * 1000 // 2 minutes in milliseconds + // 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) { @@ -28,20 +32,37 @@ export default defineNuxtPlugin(() => { console.log('[AUTH_REFRESH] Attempting automatic token refresh...') const response = await $fetch<{ success: boolean; expiresAt?: number }>('/api/auth/refresh', { - method: 'POST' + 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) { + } catch (error: any) { console.error('[AUTH_REFRESH] Token refresh error:', error) - // If refresh fails, redirect to login - await navigateTo('/login') + + // 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 } @@ -56,11 +77,14 @@ export default defineNuxtPlugin(() => { console.log('[AUTH_REFRESH] Token expired, attempting immediate refresh...') const response = await $fetch<{ success: boolean; expiresAt?: number }>('/api/auth/refresh', { - method: 'POST' + 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') @@ -68,7 +92,19 @@ export default defineNuxtPlugin(() => { } } catch (error) { console.error('[AUTH_REFRESH] Immediate refresh error:', error) - await navigateTo('/login') + + // 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 } @@ -127,10 +163,20 @@ export default defineNuxtPlugin(() => { // Listen for visibility changes to refresh when tab becomes active if (typeof document !== 'undefined') { + let lastVisibilityChange = Date.now() + document.addEventListener('visibilitychange', () => { if (!document.hidden) { - // Tab became visible, check if we need to refresh - checkAndScheduleRefresh() + 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 } }) } diff --git a/server/api/expenses/generate-pdf.ts b/server/api/expenses/generate-pdf.ts index b7309ee..d73f774 100644 --- a/server/api/expenses/generate-pdf.ts +++ b/server/api/expenses/generate-pdf.ts @@ -655,9 +655,6 @@ async function addReceiptImages(doc: PDFKit.PDFDocument, expenses: Expense[]) { async function fetchReceiptImage(receipt: any): Promise { try { - const client = getMinioClient(); - const bucketName = useRuntimeConfig().minio.bucketName; - // Determine the file path - try multiple possible sources let rawPath = null; @@ -683,7 +680,70 @@ async function fetchReceiptImage(receipt: any): Promise { console.log('[expenses/generate-pdf] Raw path from receipt:', rawPath); - // Extract MinIO path from S3 URL or use as-is if it's already a path + // Check if this is an S3 URL (HTTP/HTTPS) + if (rawPath.startsWith('http://') || rawPath.startsWith('https://')) { + console.log('[expenses/generate-pdf] Detected S3 URL, fetching directly...'); + + try { + // Fetch image directly from S3 URL + const response = await fetch(rawPath, { + method: 'GET', + headers: { + 'Accept': 'image/*' + }, + // Add timeout to prevent hanging + signal: AbortSignal.timeout(30000) // 30 second timeout + }); + + if (!response.ok) { + console.error(`[expenses/generate-pdf] Failed to fetch image from S3: ${response.status} ${response.statusText}`); + return null; + } + + // Convert response to buffer + const arrayBuffer = await response.arrayBuffer(); + const imageBuffer = Buffer.from(arrayBuffer); + + console.log('[expenses/generate-pdf] Successfully fetched image from S3 URL, Size:', imageBuffer.length); + return imageBuffer; + + } catch (fetchError: any) { + console.error('[expenses/generate-pdf] Error fetching from S3 URL:', fetchError.message); + + // If it's a timeout, try once more with a longer timeout + if (fetchError.name === 'TimeoutError' || fetchError.name === 'AbortError') { + console.log('[expenses/generate-pdf] Retrying with longer timeout...'); + try { + const retryResponse = await fetch(rawPath, { + method: 'GET', + headers: { + 'Accept': 'image/*' + }, + signal: AbortSignal.timeout(60000) // 60 second timeout for retry + }); + + if (retryResponse.ok) { + const arrayBuffer = await retryResponse.arrayBuffer(); + const imageBuffer = Buffer.from(arrayBuffer); + console.log('[expenses/generate-pdf] Successfully fetched image on retry, Size:', imageBuffer.length); + return imageBuffer; + } + } catch (retryError) { + console.error('[expenses/generate-pdf] Retry also failed:', retryError); + } + } + + return null; + } + } + + // If not an S3 URL, try MinIO as fallback + console.log('[expenses/generate-pdf] Not an S3 URL, trying MinIO fallback...'); + + const client = getMinioClient(); + const bucketName = useRuntimeConfig().minio.bucketName; + + // Extract MinIO path from the raw path let minioPath = extractMinioPath(rawPath); if (!minioPath) {