File Manager
Viewing File: csrf-token-refresh.js
/**
* CSRF Token Auto-Refresh Handler
* Prevents 419 Page Expired errors by refreshing CSRF tokens before they expire
*/
class CsrfTokenManager {
constructor(config = {}) {
// Default configuration - these will be overridden by backend config
this.refreshInterval = (config.refreshInterval || 60) * 60 * 1000; // Convert minutes to milliseconds
this.warningTime = (config.warningTime || 10) * 60 * 1000;
this.maxRetries = config.maxRetries || 3;
this.autoRefreshEnabled = config.autoRefreshEnabled !== false;
this.retryOn419 = config.retryOn419 !== false;
this.showWarnings = config.showWarnings !== false;
this.retryCount = 0;
this.isRefreshing = false;
this.init();
}
init() {
if (this.autoRefreshEnabled) {
// Auto-refresh CSRF token periodically
setInterval(() => {
this.refreshToken();
}, this.refreshInterval);
}
if (this.retryOn419) {
// Handle form submissions with retry on 419 errors
this.setupFormHandlers();
}
// Handle page visibility changes (refresh when user comes back)
this.setupVisibilityHandler();
}
async refreshToken() {
if (this.isRefreshing) return;
this.isRefreshing = true;
try {
const response = await fetch('/refresh-csrf', {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
this.updateTokens(data.csrf_token);
this.retryCount = 0;
console.log('CSRF token refreshed successfully');
} else {
throw new Error('Failed to refresh CSRF token');
}
} catch (error) {
console.error('Error refreshing CSRF token:', error);
this.retryCount++;
// Retry with exponential backoff
if (this.retryCount < this.maxRetries) {
setTimeout(() => {
this.isRefreshing = false;
this.refreshToken();
}, Math.pow(2, this.retryCount) * 1000);
}
} finally {
if (this.retryCount >= this.maxRetries) {
this.isRefreshing = false;
}
}
}
updateTokens(newToken) {
// Update meta tag
const metaTag = document.querySelector('meta[name="csrf-token"]');
if (metaTag) {
metaTag.setAttribute('content', newToken);
}
// Update all CSRF token inputs
document.querySelectorAll('input[name="_token"]').forEach(input => {
input.value = newToken;
});
// Update axios default header if available
if (typeof axios !== 'undefined') {
axios.defaults.headers.common['X-CSRF-TOKEN'] = newToken;
}
// Update jQuery CSRF header if available
if (typeof $ !== 'undefined') {
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': newToken
}
});
}
}
setupFormHandlers() {
document.addEventListener('submit', async (event) => {
const form = event.target;
// Only handle forms with CSRF tokens
if (!form.querySelector('input[name="_token"]')) {
return;
}
event.preventDefault();
const formData = new FormData(form);
const submitBtn = form.querySelector('button[type="submit"]');
const originalText = submitBtn ? submitBtn.textContent : '';
try {
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.textContent = 'Please wait...';
}
const response = await fetch(form.action, {
method: form.method,
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.status === 419) {
// CSRF token expired, refresh and retry
await this.refreshToken();
// Update form token
const tokenInput = form.querySelector('input[name="_token"]');
if (tokenInput) {
const metaTag = document.querySelector('meta[name="csrf-token"]');
if (metaTag) {
tokenInput.value = metaTag.getAttribute('content');
}
}
// Retry submission
const retryFormData = new FormData(form);
const retryResponse = await fetch(form.action, {
method: form.method,
body: retryFormData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (retryResponse.ok) {
// Handle successful response
this.handleResponse(retryResponse, form);
} else {
throw new Error('Retry submission failed');
}
} else if (response.ok) {
// Handle successful response
this.handleResponse(response, form);
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (error) {
console.error('Form submission error:', error);
this.showError('An error occurred. Please try again.');
} finally {
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = originalText;
}
}
});
}
async handleResponse(response, form) {
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const data = await response.json();
if (data.redirect) {
window.location.href = data.redirect;
} else if (data.success) {
this.showSuccess(data.message || 'Operation completed successfully!');
} else if (data.errors) {
this.showValidationErrors(data.errors);
}
} else {
// Handle HTML response (redirect or page reload)
const redirectUrl = response.headers.get('location') || response.url;
if (redirectUrl && redirectUrl !== window.location.href) {
window.location.href = redirectUrl;
} else {
// Reload current page
window.location.reload();
}
}
}
setupVisibilityHandler() {
let wasHidden = false;
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
wasHidden = true;
} else if (wasHidden) {
// User came back to the tab, refresh token if it's been a while
wasHidden = false;
setTimeout(() => {
this.refreshToken();
}, 1000);
}
});
}
showSessionWarning() {
if (!this.showWarnings) return;
const warningHtml = `
<div id="session-warning" class="fixed top-4 right-4 z-50 max-w-sm bg-yellow-500/90 border border-yellow-400 rounded-2xl p-4 shadow-2xl backdrop-blur-sm">
<div class="flex items-start gap-3">
<i data-lucide="clock" class="h-5 w-5 text-yellow-900 flex-shrink-0 mt-0.5"></i>
<div class="flex-1">
<h4 class="text-sm font-semibold text-yellow-900 mb-1">Session Expiring Soon</h4>
<p class="text-xs text-yellow-800 mb-3">Your session will expire in ${Math.floor(this.warningTime / 60000)} minutes. Save your work!</p>
<div class="flex gap-2">
<button onclick="window.csrfManager.refreshToken(); document.getElementById('session-warning').remove();"
class="px-3 py-1 bg-yellow-600 text-white text-xs rounded-lg hover:bg-yellow-700 transition-colors">
Extend Session
</button>
<button onclick="document.getElementById('session-warning').remove();"
class="px-3 py-1 bg-yellow-200 text-yellow-800 text-xs rounded-lg hover:bg-yellow-300 transition-colors">
Dismiss
</button>
</div>
</div>
<button onclick="document.getElementById('session-warning').remove();"
class="text-yellow-700 hover:text-yellow-900 transition-colors">
<i data-lucide="x" class="h-4 w-4"></i>
</button>
</div>
</div>
`;
// Remove existing warning
const existingWarning = document.getElementById('session-warning');
if (existingWarning) {
existingWarning.remove();
}
// Add warning to page
document.body.insertAdjacentHTML('beforeend', warningHtml);
// Initialize lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
showError(message) {
const alertHtml = `
<div class="mb-6 p-4 bg-red-500/20 border border-red-400/30 rounded-2xl alert-auto-dismiss" role="alert">
<div class="flex items-center gap-3">
<i data-lucide="alert-circle" class="h-5 w-5 text-red-400 flex-shrink-0"></i>
<div class="text-sm text-red-100">${message}</div>
</div>
</div>
`;
this.showAlert(alertHtml);
}
showSuccess(message) {
const alertHtml = `
<div class="mb-6 p-4 bg-green-500/20 border border-green-400/30 rounded-2xl alert-auto-dismiss" role="alert">
<div class="flex items-center gap-3">
<i data-lucide="check-circle" class="h-5 w-5 text-green-400 flex-shrink-0"></i>
<div class="text-sm text-green-100">${message}</div>
</div>
</div>
`;
this.showAlert(alertHtml);
}
showValidationErrors(errors) {
Object.keys(errors).forEach(field => {
const fieldElement = document.querySelector(`[name="${field}"]`);
if (fieldElement) {
const errorHtml = `
<div class="mt-2 p-3 bg-red-500/20 border border-red-400/30 rounded-xl alert-auto-dismiss">
<div class="flex items-center gap-2">
<i data-lucide="alert-circle" class="h-4 w-4 text-red-400 flex-shrink-0"></i>
<div class="text-xs text-red-100">${errors[field][0]}</div>
</div>
</div>
`;
fieldElement.parentNode.insertAdjacentHTML('afterend', errorHtml);
}
});
}
showAlert(alertHtml) {
// Find form container
const formContainer = document.querySelector('form').closest('div');
// Remove existing alerts
document.querySelectorAll('.alert-auto-dismiss').forEach(alert => alert.remove());
// Add new alert
formContainer.insertAdjacentHTML('afterbegin', alertHtml);
// Initialize lucide icons if available
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Auto-dismiss after 5 seconds
setTimeout(() => {
document.querySelectorAll('.alert-auto-dismiss').forEach(alert => {
alert.style.opacity = '0';
alert.style.transform = 'translateY(-10px)';
setTimeout(() => alert.remove(), 300);
});
}, 5000);
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
// Load configuration from backend and initialize
fetch('/refresh-csrf', {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
// Initialize with backend configuration and expose globally
window.csrfManager = new CsrfTokenManager(data.config || {});
// Set up session warning timer if warnings are enabled
if (data.config && data.config.showWarnings) {
const sessionLifetime = 120 * 60 * 1000; // 120 minutes in milliseconds
const warningTime = data.config.warningTime * 60 * 1000;
setTimeout(() => {
window.csrfManager.showSessionWarning();
}, sessionLifetime - warningTime);
}
})
.catch(error => {
console.warn('Could not load CSRF configuration, using defaults:', error);
// Initialize with default configuration if fetch fails
window.csrfManager = new CsrfTokenManager();
});
});