Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)
This commit is contained in:
206
static/js/csrf-refresh.js
Normal file
206
static/js/csrf-refresh.js
Normal file
@@ -0,0 +1,206 @@
|
||||
// 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('<!doctype') || // HTML error page
|
||||
textResponse.includes('<html');
|
||||
}
|
||||
|
||||
if (isCSRFError) {
|
||||
console.log('CSRF token expired, attempting refresh and retry...');
|
||||
|
||||
try {
|
||||
// Refresh the token
|
||||
await self.refreshToken();
|
||||
|
||||
// Retry the original request with the new token
|
||||
newOptions.headers['X-CSRFToken'] = self.token;
|
||||
response = await originalFetch(url, newOptions);
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Request succeeded after CSRF token refresh');
|
||||
} else {
|
||||
console.warn('Request still failed after CSRF token refresh:', response.status);
|
||||
}
|
||||
} catch (refreshError) {
|
||||
console.error('Failed to refresh CSRF token during retry:', refreshError);
|
||||
// Return the original failed response
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.warn('Could not parse error response for CSRF check:', parseError);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
}
|
||||
|
||||
// Method to manually get current token
|
||||
getToken() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
// Method to manually refresh token (for periodic refresh)
|
||||
async manualRefresh() {
|
||||
try {
|
||||
await this.refreshToken();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Manual CSRF token refresh failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize CSRF manager when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user