/** * Client-side error boundaries for authentication failures * Provides centralized error handling for authentication-related errors */ class AuthErrorHandler { constructor() { this.errorContainer = null; this.retryAttempts = 0; this.maxRetryAttempts = 3; this.retryDelay = 1000; // 1 second this.init(); } /** * Initialize error handler */ init() { // Create error container if it doesn't exist this.createErrorContainer(); // Set up global error handlers this.setupGlobalErrorHandlers(); // Set up API interceptors this.setupAPIInterceptors(); } /** * Create error display container */ createErrorContainer() { if (document.getElementById('auth-error-container')) { return; } const container = document.createElement('div'); container.id = 'auth-error-container'; container.className = 'auth-error-container'; container.style.cssText = ` position: fixed; top: 20px; right: 20px; max-width: 400px; z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; `; document.body.appendChild(container); this.errorContainer = container; } /** * Set up global error handlers */ setupGlobalErrorHandlers() { // Handle unhandled promise rejections window.addEventListener('unhandledrejection', (event) => { if (this.isAuthError(event.reason)) { this.handleAuthError(event.reason); event.preventDefault(); } }); // Handle general errors window.addEventListener('error', (event) => { if (this.isAuthError(event.error)) { this.handleAuthError(event.error); } }); } /** * Set up API request interceptors */ setupAPIInterceptors() { // Override fetch to intercept API calls const originalFetch = window.fetch; window.fetch = async (...args) => { try { const response = await originalFetch(...args); // Check for authentication errors if (response.status === 401 || response.status === 403) { const errorData = await response.clone().json().catch(() => ({})); this.handleAuthError({ status: response.status, message: errorData.error || 'Authentication failed', code: errorData.code, url: args[0] }); } return response; } catch (error) { if (this.isAuthError(error)) { this.handleAuthError(error); } throw error; } }; } /** * Check if error is authentication-related */ isAuthError(error) { if (!error) return false; const authErrorCodes = [ 'INVALID_TOKEN', 'TOKEN_EXPIRED', 'TOKEN_NOT_ACTIVE', 'AUTH_ERROR', 'INVALID_CREDENTIALS', 'EMAIL_NOT_VERIFIED', 'RATE_LIMIT_EXCEEDED' ]; const authErrorMessages = [ 'authentication', 'unauthorized', 'token', 'login', 'session' ]; // Check error code if (error.code && authErrorCodes.includes(error.code)) { return true; } // Check error message if (error.message) { const message = error.message.toLowerCase(); return authErrorMessages.some(keyword => message.includes(keyword)); } // Check HTTP status if (error.status === 401 || error.status === 403) { return true; } return false; } /** * Handle authentication errors */ handleAuthError(error) { console.error('Authentication error:', error); const errorInfo = this.parseError(error); // Show error message this.showError(errorInfo); // Handle specific error types switch (errorInfo.code) { case 'TOKEN_EXPIRED': this.handleTokenExpired(); break; case 'INVALID_TOKEN': case 'AUTH_ERROR': this.handleInvalidAuth(); break; case 'EMAIL_NOT_VERIFIED': this.handleEmailNotVerified(); break; case 'RATE_LIMIT_EXCEEDED': this.handleRateLimit(errorInfo); break; default: this.handleGenericAuthError(errorInfo); } } /** * Parse error object */ parseError(error) { return { message: error.message || error.error || 'Authentication failed', code: error.code || 'AUTH_ERROR', status: error.status, url: error.url, timestamp: new Date().toISOString() }; } /** * Show error message to user */ showError(errorInfo) { const errorElement = document.createElement('div'); errorElement.className = 'auth-error-message'; errorElement.style.cssText = ` background: #fee; border: 1px solid #fcc; border-radius: 4px; padding: 12px 16px; margin-bottom: 10px; color: #c33; position: relative; animation: slideIn 0.3s ease-out; `; // Add animation keyframes if (!document.getElementById('auth-error-styles')) { const style = document.createElement('style'); style.id = 'auth-error-styles'; style.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } .auth-error-close { position: absolute; top: 8px; right: 12px; background: none; border: none; font-size: 18px; cursor: pointer; color: #c33; } `; document.head.appendChild(style); } errorElement.innerHTML = ` Authentication Error
${this.escapeHtml(errorInfo.message)} `; this.errorContainer.appendChild(errorElement); // Auto-remove after 10 seconds setTimeout(() => { if (errorElement.parentElement) { errorElement.remove(); } }, 10000); } /** * Handle token expired error */ handleTokenExpired() { // Try to refresh token this.attemptTokenRefresh() .then(success => { if (!success) { this.redirectToLogin('Your session has expired. Please log in again.'); } }) .catch(() => { this.redirectToLogin('Your session has expired. Please log in again.'); }); } /** * Handle invalid authentication */ handleInvalidAuth() { this.clearAuthData(); this.redirectToLogin('Please log in to continue.'); } /** * Handle email not verified error */ handleEmailNotVerified() { this.showError({ message: 'Please verify your email address before continuing.', code: 'EMAIL_NOT_VERIFIED' }); // Optionally redirect to verification page setTimeout(() => { if (confirm('Would you like to go to the email verification page?')) { window.location.href = '/verify-email.html'; } }, 2000); } /** * Handle rate limit error */ handleRateLimit(errorInfo) { const retryAfter = this.calculateRetryDelay(); this.showError({ message: `${errorInfo.message} Please try again in ${Math.ceil(retryAfter / 1000)} seconds.`, code: 'RATE_LIMIT_EXCEEDED' }); // Disable forms temporarily this.disableAuthForms(retryAfter); } /** * Handle generic authentication error */ handleGenericAuthError(errorInfo) { // Log error for debugging console.error('Generic auth error:', errorInfo); // Show user-friendly message this.showError({ message: 'An authentication error occurred. Please try again.', code: errorInfo.code }); } /** * Attempt to refresh authentication token */ async attemptTokenRefresh() { if (this.retryAttempts >= this.maxRetryAttempts) { return false; } this.retryAttempts++; try { const response = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'include' }); if (response.ok) { this.retryAttempts = 0; return true; } return false; } catch (error) { console.error('Token refresh failed:', error); return false; } } /** * Clear authentication data */ clearAuthData() { // Clear any stored auth tokens localStorage.removeItem('authToken'); sessionStorage.removeItem('authToken'); // Clear any user data localStorage.removeItem('userData'); sessionStorage.removeItem('userData'); // Clear cookies by making a logout request fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => { // Ignore errors during cleanup }); } /** * Redirect to login page */ redirectToLogin(message) { // Store message for display on login page if (message) { sessionStorage.setItem('loginMessage', message); } // Store current page for redirect after login const currentPath = window.location.pathname; if (currentPath !== '/login.html' && currentPath !== '/register.html') { sessionStorage.setItem('redirectAfterLogin', currentPath); } // Redirect to login window.location.href = '/login.html'; } /** * Calculate retry delay for rate limiting */ calculateRetryDelay() { return Math.min(this.retryDelay * Math.pow(2, this.retryAttempts), 30000); // Max 30 seconds } /** * Disable authentication forms temporarily */ disableAuthForms(duration) { const forms = document.querySelectorAll('form[data-auth-form]'); const buttons = document.querySelectorAll('button[data-auth-button]'); forms.forEach(form => { form.style.opacity = '0.5'; form.style.pointerEvents = 'none'; }); buttons.forEach(button => { button.disabled = true; const originalText = button.textContent; let countdown = Math.ceil(duration / 1000); const updateButton = () => { button.textContent = `Try again in ${countdown}s`; countdown--; if (countdown < 0) { button.disabled = false; button.textContent = originalText; return; } setTimeout(updateButton, 1000); }; updateButton(); }); setTimeout(() => { forms.forEach(form => { form.style.opacity = ''; form.style.pointerEvents = ''; }); }, duration); } /** * Escape HTML to prevent XSS */ escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Check authentication status */ async checkAuthStatus() { try { const response = await fetch('/api/user/verify-token', { credentials: 'include' }); if (!response.ok) { throw new Error('Authentication check failed'); } const data = await response.json(); return data.valid; } catch (error) { this.handleAuthError(error); return false; } } /** * Initialize authentication check on page load */ initAuthCheck() { // Skip auth check on public pages const publicPages = ['/login.html', '/register.html', '/forgot-password.html', '/reset-password.html']; const currentPath = window.location.pathname; if (publicPages.includes(currentPath)) { return; } // Check authentication status this.checkAuthStatus().then(isAuthenticated => { if (!isAuthenticated) { this.redirectToLogin('Please log in to access this page.'); } }); } } // Initialize error handler when DOM is loaded document.addEventListener('DOMContentLoaded', () => { window.authErrorHandler = new AuthErrorHandler(); window.authErrorHandler.initAuthCheck(); }); // Export for use in other scripts if (typeof module !== 'undefined' && module.exports) { module.exports = AuthErrorHandler; }