495 lines
14 KiB
JavaScript
495 lines
14 KiB
JavaScript
/**
|
|
* 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 = `
|
|
<strong>Authentication Error</strong><br>
|
|
${this.escapeHtml(errorInfo.message)}
|
|
<button class="auth-error-close" onclick="this.parentElement.remove()">×</button>
|
|
`;
|
|
|
|
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;
|
|
} |