/**
* 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;
}