// CSRF Token Management with Auto-Refresh class CSRFManager { constructor() { this.token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); this.refreshPromise = null; this.setupFetchInterceptor(); } async refreshToken() { // Prevent multiple simultaneous refresh requests if (this.refreshPromise) { return this.refreshPromise; } this.refreshPromise = this.performTokenRefresh(); try { const newToken = await this.refreshPromise; return newToken; } finally { this.refreshPromise = null; } } async performTokenRefresh() { try { console.log('Refreshing CSRF token...'); // Use the original fetch to avoid recursion const originalFetch = window.originalFetch || fetch; const response = await originalFetch('/api/csrf-token', { method: 'GET', credentials: 'same-origin', headers: { 'Accept': 'application/json' } }); if (!response.ok) { throw new Error(`Failed to refresh CSRF token: ${response.status} ${response.statusText}`); } const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { const text = await response.text(); throw new Error(`Expected JSON response but got ${contentType}. Response: ${text.substring(0, 200)}`); } const data = await response.json(); if (data.csrf_token) { this.token = data.csrf_token; // Update the meta tag for any other code that might read it const metaTag = document.querySelector('meta[name="csrf-token"]'); if (metaTag) { metaTag.setAttribute('content', this.token); } // Update Vue.js reactive token if available if (window.app && window.app.csrfToken !== undefined) { window.app.csrfToken = this.token; } console.log('CSRF token refreshed successfully'); return this.token; } else { throw new Error('No CSRF token in response'); } } catch (error) { console.error('Failed to refresh CSRF token:', error); throw error; } } setupFetchInterceptor() { // Store original fetch if not already stored if (!window.originalFetch) { window.originalFetch = window.fetch; } const originalFetch = window.originalFetch; const self = this; window.fetch = async function(url, options = {}) { // Skip CSRF token for the token refresh endpoint to avoid recursion if (url.includes('/api/csrf-token')) { return originalFetch(url, options); } // Add CSRF token to headers for API requests const newOptions = { ...options }; if (url.startsWith('/api/') || url.startsWith('/upload') || url.startsWith('/save') || url.startsWith('/recording/') || url.startsWith('/chat') || url.startsWith('/speakers')) { newOptions.headers = { 'X-CSRFToken': self.token, ...newOptions.headers }; } // Make the request let response = await originalFetch(url, newOptions); // Check for CSRF token expiration if ((response.status === 400 || response.status === 403) && (url.startsWith('/api/') || url.startsWith('/upload') || url.startsWith('/save') || url.startsWith('/recording/') || url.startsWith('/chat') || url.startsWith('/speakers'))) { try { // Try to parse as JSON first const responseClone = response.clone(); let isCSRFError = false; try { const errorData = await responseClone.json(); const errorMessage = errorData.error || ''; isCSRFError = errorMessage.toLowerCase().includes('csrf') || errorMessage.toLowerCase().includes('token'); } catch (jsonError) { // If JSON parsing fails, check if it's an HTML error page const textResponse = await response.clone().text(); isCSRFError = textResponse.toLowerCase().includes('csrf') || textResponse.toLowerCase().includes('token') || textResponse.includes(' { window.csrfManager = new CSRFManager(); // Set up periodic token refresh every 45 minutes (before 1-hour expiration) setInterval(() => { if (window.csrfManager) { console.log('Performing periodic CSRF token refresh...'); window.csrfManager.manualRefresh(); } }, 45 * 60 * 1000); // 45 minutes // Refresh CSRF token when page becomes visible again (wake from sleep, tab switch) document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible' && window.csrfManager) { console.log('[CSRF] Page visible again, refreshing token...'); window.csrfManager.manualRefresh(); } }); // Heartbeat gap detection: if setInterval drifts > 2 min, system was asleep let lastHeartbeat = Date.now(); setInterval(() => { const now = Date.now(); const drift = now - lastHeartbeat - 60000; if (drift > 120000) { console.log(`[CSRF] Heartbeat drift ${Math.round(drift / 1000)}s detected, refreshing token...`); if (window.csrfManager) { window.csrfManager.manualRefresh(); } } lastHeartbeat = now; }, 60000); });