Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
377
resources/js/modules/csrf-auto-refresh.js
Normal file
377
resources/js/modules/csrf-auto-refresh.js
Normal file
@@ -0,0 +1,377 @@
|
||||
/**
|
||||
* CSRF Token Auto-Refresh System
|
||||
*
|
||||
* Automatically refreshes CSRF tokens before they expire to prevent
|
||||
* form submission errors when users keep forms open for extended periods.
|
||||
*
|
||||
* Features:
|
||||
* - Automatic token refresh every 105 minutes (15 minutes before expiry)
|
||||
* - Visual feedback when tokens are refreshed
|
||||
* - Graceful error handling with fallback strategies
|
||||
* - Page visibility optimization (pause when tab is inactive)
|
||||
* - Multiple form support
|
||||
*
|
||||
* Usage:
|
||||
* import { CsrfAutoRefresh } from './modules/csrf-auto-refresh.js';
|
||||
*
|
||||
* // Initialize for contact form
|
||||
* const csrfRefresh = new CsrfAutoRefresh({
|
||||
* formId: 'contact_form',
|
||||
* refreshInterval: 105 * 60 * 1000 // 105 minutes
|
||||
* });
|
||||
*
|
||||
* // Or auto-detect all forms with CSRF tokens
|
||||
* CsrfAutoRefresh.initializeAll();
|
||||
*/
|
||||
|
||||
export class CsrfAutoRefresh {
|
||||
|
||||
/**
|
||||
* Default configuration
|
||||
*/
|
||||
static DEFAULT_CONFIG = {
|
||||
formId: 'contact_form',
|
||||
refreshInterval: 105 * 60 * 1000, // 105 minutes (15 min before 2h expiry)
|
||||
apiEndpoint: '/api/csrf/refresh',
|
||||
tokenSelector: 'input[name="_token"]',
|
||||
formIdSelector: 'input[name="_form_id"]',
|
||||
enableVisualFeedback: true,
|
||||
enableConsoleLogging: true,
|
||||
pauseWhenHidden: true, // Pause refresh when tab is not visible
|
||||
maxRetries: 3,
|
||||
retryDelay: 5000 // 5 seconds
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} config Configuration options
|
||||
*/
|
||||
constructor(config = {}) {
|
||||
this.config = { ...CsrfAutoRefresh.DEFAULT_CONFIG, ...config };
|
||||
this.intervalId = null;
|
||||
this.isActive = false;
|
||||
this.retryCount = 0;
|
||||
this.lastRefreshTime = null;
|
||||
|
||||
// Track page visibility for optimization
|
||||
this.isPageVisible = !document.hidden;
|
||||
|
||||
this.log('CSRF Auto-Refresh initialized', this.config);
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
|
||||
// Start auto-refresh if page is visible
|
||||
if (this.isPageVisible) {
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners for page visibility and unload
|
||||
*/
|
||||
setupEventListeners() {
|
||||
if (this.config.pauseWhenHidden) {
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
this.isPageVisible = !document.hidden;
|
||||
|
||||
if (this.isPageVisible) {
|
||||
this.log('Page became visible, resuming CSRF refresh');
|
||||
this.start();
|
||||
} else {
|
||||
this.log('Page became hidden, pausing CSRF refresh');
|
||||
this.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
this.stop();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the auto-refresh timer
|
||||
*/
|
||||
start() {
|
||||
if (this.isActive) {
|
||||
this.log('Auto-refresh already active');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isActive = true;
|
||||
|
||||
// Set up interval for token refresh
|
||||
this.intervalId = setInterval(() => {
|
||||
this.refreshToken();
|
||||
}, this.config.refreshInterval);
|
||||
|
||||
this.log(`Auto-refresh started. Next refresh in ${this.config.refreshInterval / 1000 / 60} minutes`);
|
||||
|
||||
// Show visual feedback if enabled
|
||||
if (this.config.enableVisualFeedback) {
|
||||
this.showStatusMessage('CSRF protection enabled - tokens will refresh automatically', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the auto-refresh timer
|
||||
*/
|
||||
stop() {
|
||||
if (!this.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
|
||||
this.isActive = false;
|
||||
this.log('Auto-refresh stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the CSRF token via API call
|
||||
*/
|
||||
async refreshToken() {
|
||||
if (!this.isPageVisible && this.config.pauseWhenHidden) {
|
||||
this.log('Page not visible, skipping refresh');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.log('Refreshing CSRF token...');
|
||||
|
||||
const response = await fetch(`${this.config.apiEndpoint}?form_id=${encodeURIComponent(this.config.formId)}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success || !data.token) {
|
||||
throw new Error(data.error || 'Invalid response from server');
|
||||
}
|
||||
|
||||
// Update token in all matching forms
|
||||
this.updateTokenInForms(data.token);
|
||||
|
||||
this.lastRefreshTime = new Date();
|
||||
this.retryCount = 0; // Reset retry count on success
|
||||
|
||||
this.log('CSRF token refreshed successfully', {
|
||||
token: data.token.substring(0, 8) + '...',
|
||||
formId: data.form_id,
|
||||
expiresIn: data.expires_in
|
||||
});
|
||||
|
||||
// Show visual feedback
|
||||
if (this.config.enableVisualFeedback) {
|
||||
this.showStatusMessage('Security token refreshed', 'success');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.handleRefreshError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update CSRF token in all forms on the page
|
||||
*/
|
||||
updateTokenInForms(newToken) {
|
||||
const tokenInputs = document.querySelectorAll(this.config.tokenSelector);
|
||||
let updatedCount = 0;
|
||||
|
||||
tokenInputs.forEach(input => {
|
||||
// Check if this token belongs to our form
|
||||
const form = input.closest('form');
|
||||
if (form) {
|
||||
const formIdInput = form.querySelector(this.config.formIdSelector);
|
||||
if (formIdInput && formIdInput.value === this.config.formId) {
|
||||
input.value = newToken;
|
||||
updatedCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.log(`Updated ${updatedCount} token input(s)`);
|
||||
|
||||
if (updatedCount === 0) {
|
||||
console.warn('CsrfAutoRefresh: No token inputs found to update. Check your selectors.');
|
||||
}
|
||||
|
||||
return updatedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle refresh errors with retry logic
|
||||
*/
|
||||
handleRefreshError(error) {
|
||||
this.retryCount++;
|
||||
|
||||
console.error('CSRF token refresh failed:', error);
|
||||
|
||||
if (this.retryCount <= this.config.maxRetries) {
|
||||
this.log(`Retrying in ${this.config.retryDelay / 1000}s (attempt ${this.retryCount}/${this.config.maxRetries})`);
|
||||
|
||||
setTimeout(() => {
|
||||
this.refreshToken();
|
||||
}, this.config.retryDelay);
|
||||
|
||||
// Show visual feedback for retry
|
||||
if (this.config.enableVisualFeedback) {
|
||||
this.showStatusMessage(`Token refresh failed, retrying... (${this.retryCount}/${this.config.maxRetries})`, 'warning');
|
||||
}
|
||||
} else {
|
||||
// Max retries reached
|
||||
this.log('Max retries reached, stopping auto-refresh');
|
||||
this.stop();
|
||||
|
||||
if (this.config.enableVisualFeedback) {
|
||||
this.showStatusMessage('Token refresh failed. Please refresh the page if you encounter errors.', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show visual status message to user
|
||||
*/
|
||||
showStatusMessage(message, type = 'info') {
|
||||
// Create or update status element
|
||||
let statusEl = document.getElementById('csrf-status-message');
|
||||
|
||||
if (!statusEl) {
|
||||
statusEl = document.createElement('div');
|
||||
statusEl.id = 'csrf-status-message';
|
||||
statusEl.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
z-index: 10000;
|
||||
max-width: 300px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
transition: opacity 0.3s ease;
|
||||
`;
|
||||
document.body.appendChild(statusEl);
|
||||
}
|
||||
|
||||
// Set message and styling based on type
|
||||
statusEl.textContent = message;
|
||||
statusEl.className = `csrf-status-${type}`;
|
||||
|
||||
// Style based on type
|
||||
const styles = {
|
||||
info: 'background: #e3f2fd; color: #1976d2; border-left: 4px solid #2196f3;',
|
||||
success: 'background: #e8f5e8; color: #2e7d32; border-left: 4px solid #4caf50;',
|
||||
warning: 'background: #fff3e0; color: #f57c00; border-left: 4px solid #ff9800;',
|
||||
error: 'background: #ffebee; color: #d32f2f; border-left: 4px solid #f44336;'
|
||||
};
|
||||
|
||||
statusEl.style.cssText += styles[type] || styles.info;
|
||||
statusEl.style.opacity = '1';
|
||||
|
||||
// Auto-hide after delay (except for errors)
|
||||
if (type !== 'error') {
|
||||
setTimeout(() => {
|
||||
if (statusEl) {
|
||||
statusEl.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
if (statusEl && statusEl.parentNode) {
|
||||
statusEl.parentNode.removeChild(statusEl);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, type === 'success' ? 3000 : 5000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log messages if console logging is enabled
|
||||
*/
|
||||
log(message, data = null) {
|
||||
if (this.config.enableConsoleLogging) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
if (data) {
|
||||
console.log(`[${timestamp}] CsrfAutoRefresh: ${message}`, data);
|
||||
} else {
|
||||
console.log(`[${timestamp}] CsrfAutoRefresh: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current status information
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
isActive: this.isActive,
|
||||
formId: this.config.formId,
|
||||
lastRefreshTime: this.lastRefreshTime,
|
||||
retryCount: this.retryCount,
|
||||
isPageVisible: this.isPageVisible,
|
||||
nextRefreshIn: this.isActive ?
|
||||
Math.max(0, this.config.refreshInterval - (Date.now() - (this.lastRefreshTime?.getTime() || Date.now()))) :
|
||||
null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual token refresh (useful for debugging)
|
||||
*/
|
||||
async manualRefresh() {
|
||||
this.log('Manual refresh triggered');
|
||||
await this.refreshToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to initialize auto-refresh for all forms with CSRF tokens
|
||||
*/
|
||||
static initializeAll() {
|
||||
const tokenInputs = document.querySelectorAll('input[name="_token"]');
|
||||
const formIds = new Set();
|
||||
|
||||
// Collect unique form IDs
|
||||
tokenInputs.forEach(input => {
|
||||
const form = input.closest('form');
|
||||
if (form) {
|
||||
const formIdInput = form.querySelector('input[name="_form_id"]');
|
||||
if (formIdInput && formIdInput.value) {
|
||||
formIds.add(formIdInput.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize auto-refresh for each unique form ID
|
||||
const instances = [];
|
||||
formIds.forEach(formId => {
|
||||
const instance = new CsrfAutoRefresh({ formId });
|
||||
instances.push(instance);
|
||||
});
|
||||
|
||||
console.log(`CsrfAutoRefresh: Initialized for ${instances.length} forms:`, Array.from(formIds));
|
||||
return instances;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
CsrfAutoRefresh.initializeAll();
|
||||
});
|
||||
} else {
|
||||
CsrfAutoRefresh.initializeAll();
|
||||
}
|
||||
|
||||
// Export for manual usage
|
||||
export default CsrfAutoRefresh;
|
||||
Reference in New Issue
Block a user