Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)

This commit is contained in:
InnovA AI
2026-03-16 21:47:37 +00:00
commit 42772a31ed
365 changed files with 103572 additions and 0 deletions

206
static/js/csrf-refresh.js Normal file
View 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);
});