File Manager

Path: /home/liffinac1/domains/impactxtmtrading.com/public_html/public/js/

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();
    });
});