WIP
This commit is contained in:
495
frontend/auth-error-handler.js
Normal file
495
frontend/auth-error-handler.js
Normal file
@ -0,0 +1,495 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
514
frontend/auth-script.js
Normal file
514
frontend/auth-script.js
Normal file
@ -0,0 +1,514 @@
|
||||
class AuthManager {
|
||||
constructor() {
|
||||
this.apiBaseUrl = '/api'; // Backend API base URL
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.initializePasswordValidation();
|
||||
this.handleEmailVerification();
|
||||
this.handlePasswordReset();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Login form
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', (e) => this.handleLogin(e));
|
||||
}
|
||||
|
||||
// Registration form
|
||||
const registerForm = document.getElementById('registerForm');
|
||||
if (registerForm) {
|
||||
registerForm.addEventListener('submit', (e) => this.handleRegistration(e));
|
||||
}
|
||||
|
||||
// Forgot password form
|
||||
const forgotPasswordForm = document.getElementById('forgotPasswordForm');
|
||||
if (forgotPasswordForm) {
|
||||
forgotPasswordForm.addEventListener('submit', (e) => this.handleForgotPassword(e));
|
||||
}
|
||||
|
||||
// Reset password form
|
||||
const resetPasswordForm = document.getElementById('resetPasswordForm');
|
||||
if (resetPasswordForm) {
|
||||
resetPasswordForm.addEventListener('submit', (e) => this.handleResetPassword(e));
|
||||
}
|
||||
|
||||
// Resend verification button
|
||||
const resendVerificationBtn = document.getElementById('resendVerificationBtn');
|
||||
if (resendVerificationBtn) {
|
||||
resendVerificationBtn.addEventListener('click', (e) => this.handleResendVerification(e));
|
||||
}
|
||||
}
|
||||
|
||||
initializePasswordValidation() {
|
||||
const passwordInputs = document.querySelectorAll('input[type="password"]');
|
||||
passwordInputs.forEach(input => {
|
||||
if (input.id === 'password' || input.id === 'newPassword') {
|
||||
input.addEventListener('input', (e) => this.validatePassword(e.target.value));
|
||||
}
|
||||
});
|
||||
|
||||
// Confirm password validation
|
||||
const confirmPasswordInputs = document.querySelectorAll('#confirmPassword, #confirmNewPassword');
|
||||
confirmPasswordInputs.forEach(input => {
|
||||
input.addEventListener('input', (e) => this.validatePasswordMatch(e.target));
|
||||
});
|
||||
}
|
||||
|
||||
validatePassword(password) {
|
||||
const requirements = {
|
||||
'req-length': password.length >= 8,
|
||||
'req-uppercase': /[A-Z]/.test(password),
|
||||
'req-lowercase': /[a-z]/.test(password),
|
||||
'req-number': /\d/.test(password),
|
||||
'req-special': /[!@#$%^&*(),.?":{}|<>]/.test(password)
|
||||
};
|
||||
|
||||
Object.entries(requirements).forEach(([reqId, isValid]) => {
|
||||
const reqElement = document.getElementById(reqId);
|
||||
if (reqElement) {
|
||||
reqElement.classList.toggle('valid', isValid);
|
||||
reqElement.classList.toggle('invalid', !isValid);
|
||||
}
|
||||
});
|
||||
|
||||
return Object.values(requirements).every(req => req);
|
||||
}
|
||||
|
||||
validatePasswordMatch(confirmInput) {
|
||||
const passwordInput = document.getElementById('password') || document.getElementById('newPassword');
|
||||
const isMatch = confirmInput.value === passwordInput.value;
|
||||
|
||||
confirmInput.setCustomValidity(isMatch ? '' : 'Passwords do not match');
|
||||
return isMatch;
|
||||
}
|
||||
|
||||
async handleLogin(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const formData = new FormData(form);
|
||||
const loginBtn = document.getElementById('loginBtn');
|
||||
|
||||
this.setButtonLoading(loginBtn, true);
|
||||
this.hideMessages();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: formData.get('email'),
|
||||
password: formData.get('password'),
|
||||
rememberMe: formData.get('rememberMe') === 'on'
|
||||
}),
|
||||
credentials: 'include' // Include cookies for session management
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.showSuccess('Login successful! Redirecting...');
|
||||
|
||||
// Store user info if needed
|
||||
if (data.user) {
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
}
|
||||
|
||||
// Redirect to main application
|
||||
setTimeout(() => {
|
||||
window.location.href = 'index.html';
|
||||
}, 1500);
|
||||
} else {
|
||||
// Check if error is due to unverified email
|
||||
if (data.code === 'EMAIL_NOT_VERIFIED') {
|
||||
this.showEmailNotVerifiedError(formData.get('email'));
|
||||
} else {
|
||||
this.showError(data.error || 'Login failed. Please try again.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
this.showError('Network error. Please check your connection and try again.');
|
||||
} finally {
|
||||
this.setButtonLoading(loginBtn, false);
|
||||
}
|
||||
}
|
||||
|
||||
async handleRegistration(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const formData = new FormData(form);
|
||||
const registerBtn = document.getElementById('registerBtn');
|
||||
|
||||
// Validate password requirements
|
||||
const password = formData.get('password');
|
||||
if (!this.validatePassword(password)) {
|
||||
this.showError('Please ensure your password meets all requirements.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate password confirmation
|
||||
const confirmPassword = formData.get('confirmPassword');
|
||||
if (password !== confirmPassword) {
|
||||
this.showError('Passwords do not match.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setButtonLoading(registerBtn, true);
|
||||
this.hideMessages();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: formData.get('email'),
|
||||
password: password
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.showSuccess('Account created successfully! Please check your email to verify your account.');
|
||||
form.reset();
|
||||
|
||||
// Optionally redirect to login page after a delay
|
||||
setTimeout(() => {
|
||||
window.location.href = 'login.html';
|
||||
}, 3000);
|
||||
} else {
|
||||
this.showError(data.error || 'Registration failed. Please try again.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
this.showError('Network error. Please check your connection and try again.');
|
||||
} finally {
|
||||
this.setButtonLoading(registerBtn, false);
|
||||
}
|
||||
}
|
||||
|
||||
async handleForgotPassword(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const formData = new FormData(form);
|
||||
const resetBtn = document.getElementById('resetBtn');
|
||||
|
||||
this.setButtonLoading(resetBtn, true);
|
||||
this.hideMessages();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/auth/forgot-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: formData.get('email')
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.showSuccess('Password reset link sent! Please check your email.');
|
||||
form.reset();
|
||||
} else {
|
||||
this.showError(data.error || 'Failed to send reset link. Please try again.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Forgot password error:', error);
|
||||
this.showError('Network error. Please check your connection and try again.');
|
||||
} finally {
|
||||
this.setButtonLoading(resetBtn, false);
|
||||
}
|
||||
}
|
||||
|
||||
async handleResetPassword(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const formData = new FormData(form);
|
||||
const updateBtn = document.getElementById('updatePasswordBtn');
|
||||
|
||||
// Validate password requirements
|
||||
const newPassword = formData.get('newPassword');
|
||||
if (!this.validatePassword(newPassword)) {
|
||||
this.showError('Please ensure your password meets all requirements.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate password confirmation
|
||||
const confirmPassword = formData.get('confirmNewPassword');
|
||||
if (newPassword !== confirmPassword) {
|
||||
this.showError('Passwords do not match.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setButtonLoading(updateBtn, true);
|
||||
this.hideMessages();
|
||||
|
||||
try {
|
||||
const resetToken = this.getResetTokenFromUrl();
|
||||
|
||||
const response = await fetch(`${this.apiBaseUrl}/auth/reset-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: resetToken,
|
||||
newPassword: newPassword
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.showSuccess('Password updated successfully! Redirecting to login...');
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = 'login.html';
|
||||
}, 2000);
|
||||
} else {
|
||||
this.showError(data.error || 'Failed to update password. Please try again.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Reset password error:', error);
|
||||
this.showError('Network error. Please check your connection and try again.');
|
||||
} finally {
|
||||
this.setButtonLoading(updateBtn, false);
|
||||
}
|
||||
}
|
||||
|
||||
async handleEmailVerification() {
|
||||
// Only run on verify-email.html page
|
||||
if (!window.location.pathname.includes('verify-email.html')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = this.getVerificationTokenFromUrl();
|
||||
|
||||
if (!token) {
|
||||
this.showVerificationError('Invalid verification link.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/auth/verify/${token}`, {
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.showVerificationSuccess();
|
||||
} else {
|
||||
this.showVerificationError(data.error || 'Verification failed.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Email verification error:', error);
|
||||
this.showVerificationError('Network error during verification.');
|
||||
}
|
||||
}
|
||||
|
||||
async handleResendVerification(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const button = e.target;
|
||||
this.setButtonLoading(button, true);
|
||||
|
||||
try {
|
||||
// Get email from URL parameters or prompt user
|
||||
const email = this.getEmailFromUrl() || prompt('Please enter your email address:');
|
||||
|
||||
if (!email) {
|
||||
this.showError('Email address is required.');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiBaseUrl}/auth/resend-verification`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.showResendSuccess();
|
||||
} else {
|
||||
this.showError(data.error || 'Failed to resend verification email.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Resend verification error:', error);
|
||||
this.showError('Network error. Please try again.');
|
||||
} finally {
|
||||
this.setButtonLoading(button, false);
|
||||
}
|
||||
}
|
||||
|
||||
handlePasswordReset() {
|
||||
// Only run on reset-password.html page
|
||||
if (!window.location.pathname.includes('reset-password.html')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = this.getResetTokenFromUrl();
|
||||
if (token) {
|
||||
document.getElementById('resetToken').value = token;
|
||||
} else {
|
||||
this.showError('Invalid reset link.');
|
||||
}
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
getVerificationTokenFromUrl() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('token');
|
||||
}
|
||||
|
||||
getResetTokenFromUrl() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('token');
|
||||
}
|
||||
|
||||
getEmailFromUrl() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('email');
|
||||
}
|
||||
|
||||
setButtonLoading(button, isLoading) {
|
||||
const btnText = button.querySelector('.btn-text');
|
||||
const btnLoading = button.querySelector('.btn-loading');
|
||||
|
||||
if (isLoading) {
|
||||
btnText.style.display = 'none';
|
||||
btnLoading.style.display = 'flex';
|
||||
button.disabled = true;
|
||||
} else {
|
||||
btnText.style.display = 'block';
|
||||
btnLoading.style.display = 'none';
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
const errorDiv = document.getElementById('authError');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
|
||||
if (errorDiv && errorMessage) {
|
||||
errorMessage.textContent = message;
|
||||
errorDiv.style.display = 'block';
|
||||
|
||||
// Auto-hide after 5 seconds
|
||||
setTimeout(() => {
|
||||
errorDiv.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
const successDiv = document.getElementById('authSuccess');
|
||||
const successMessage = document.getElementById('successMessage');
|
||||
|
||||
if (successDiv && successMessage) {
|
||||
successMessage.textContent = message;
|
||||
successDiv.style.display = 'block';
|
||||
|
||||
// Auto-hide after 5 seconds
|
||||
setTimeout(() => {
|
||||
successDiv.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
hideMessages() {
|
||||
const errorDiv = document.getElementById('authError');
|
||||
const successDiv = document.getElementById('authSuccess');
|
||||
|
||||
if (errorDiv) errorDiv.style.display = 'none';
|
||||
if (successDiv) successDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
showVerificationSuccess() {
|
||||
document.getElementById('verificationLoading').style.display = 'none';
|
||||
document.getElementById('verificationError').style.display = 'none';
|
||||
document.getElementById('verificationSuccess').style.display = 'block';
|
||||
}
|
||||
|
||||
showVerificationError(message) {
|
||||
document.getElementById('verificationLoading').style.display = 'none';
|
||||
document.getElementById('verificationSuccess').style.display = 'none';
|
||||
document.getElementById('verificationError').style.display = 'block';
|
||||
|
||||
const errorDescription = document.getElementById('errorDescription');
|
||||
if (errorDescription) {
|
||||
errorDescription.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
showResendSuccess() {
|
||||
document.getElementById('verificationError').style.display = 'none';
|
||||
document.getElementById('resendSuccess').style.display = 'block';
|
||||
}
|
||||
|
||||
// Check if user is authenticated (for protected pages)
|
||||
static async checkAuth() {
|
||||
try {
|
||||
const response = await fetch('/api/auth/me', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const user = await response.json();
|
||||
return user;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth check error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Logout functionality
|
||||
static async logout() {
|
||||
try {
|
||||
await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
// Clear local storage
|
||||
localStorage.removeItem('user');
|
||||
|
||||
// Redirect to login
|
||||
window.location.href = 'login.html';
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
// Force redirect even if logout request fails
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize auth manager when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new AuthManager();
|
||||
});
|
||||
|
||||
// Export for use in other scripts
|
||||
window.AuthManager = AuthManager;
|
||||
590
frontend/auth-styles.css
Normal file
590
frontend/auth-styles.css
Normal file
@ -0,0 +1,590 @@
|
||||
/* Authentication Styles */
|
||||
|
||||
.auth-body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
padding: 40px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.auth-header h1 {
|
||||
color: #2c3e50;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.auth-header p {
|
||||
color: #7f8c8d;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e1e8ed;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.form-group input:invalid {
|
||||
border-color: #e74c3c;
|
||||
}
|
||||
|
||||
.form-group input:valid {
|
||||
border-color: #27ae60;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 12px;
|
||||
color: #7f8c8d;
|
||||
margin-top: 6px;
|
||||
line-height: 1.4;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
margin: 28px 0;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
flex-direction: row !important;
|
||||
flex-wrap: nowrap !important;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #2c3e50;
|
||||
line-height: 1.6;
|
||||
gap: 12px !important;
|
||||
}
|
||||
|
||||
.checkbox-text {
|
||||
display: inline !important;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #e1e8ed;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"]:checked+.checkmark {
|
||||
background-color: #667eea;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"]:checked+.checkmark::after {
|
||||
content: '✓';
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"]:focus+.checkmark {
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin: 30px 0 20px 0;
|
||||
}
|
||||
|
||||
.btn-full {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
padding: 14px 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f8f9fa;
|
||||
color: #495057;
|
||||
border: 2px solid #e1e8ed;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e9ecef;
|
||||
border-color: #adb5bd;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 2px solid white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-spinner.large {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-width: 3px;
|
||||
border-color: rgba(102, 126, 234, 0.3);
|
||||
border-top-color: #667eea;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-links {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.auth-link:hover {
|
||||
color: #5a6fd8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.auth-divider {
|
||||
text-align: center;
|
||||
margin: 30px 0 20px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.auth-divider::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: #e1e8ed;
|
||||
}
|
||||
|
||||
.auth-divider span {
|
||||
background: white;
|
||||
padding: 0 20px;
|
||||
color: #7f8c8d;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Password Requirements */
|
||||
.password-requirements {
|
||||
margin-top: 10px;
|
||||
padding: 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e9ecef;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.requirement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.requirement:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.req-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.requirement.valid {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.requirement.valid .req-icon {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.requirement.valid .req-icon::before {
|
||||
content: '✓';
|
||||
}
|
||||
|
||||
.requirement.invalid {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.requirement.invalid .req-icon {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.requirement.invalid .req-icon::before {
|
||||
content: '✗';
|
||||
}
|
||||
|
||||
/* Error and Success Messages */
|
||||
.auth-error,
|
||||
.auth-success {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.auth-error {
|
||||
border-left: 4px solid #e74c3c;
|
||||
}
|
||||
|
||||
.auth-success {
|
||||
border-left: 4px solid #27ae60;
|
||||
}
|
||||
|
||||
.error-content,
|
||||
.success-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.error-icon,
|
||||
.success-icon {
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.error-message,
|
||||
.success-message {
|
||||
color: #2c3e50;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Email Verification Styles */
|
||||
.verification-state {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.verification-icon {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.verification-icon .icon {
|
||||
font-size: 48px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.verification-icon.success .icon {
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
.verification-icon.error .icon {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.verification-icon.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.verification-content h2 {
|
||||
color: #2c3e50;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.verification-content p {
|
||||
color: #7f8c8d;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Mobile Responsive */
|
||||
@media (max-width: 480px) {
|
||||
.auth-body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.auth-header h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
font-size: 16px;
|
||||
/* Prevents zoom on iOS */
|
||||
}
|
||||
|
||||
.btn-full {
|
||||
padding: 16px 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
flex-direction: row !important;
|
||||
}
|
||||
|
||||
.checkbox-text {
|
||||
display: inline !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast mode support */
|
||||
@media (prefers-contrast: high) {
|
||||
.auth-card {
|
||||
border: 2px solid #000;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
border-width: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.loading-spinner {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover,
|
||||
.btn-secondary:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.auth-body {
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
background: #2c3e50;
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
.auth-header h1 {
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
.auth-header p {
|
||||
color: #bdc3c7;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
background: #34495e;
|
||||
border-color: #4a5f7a;
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
border-color: #667eea;
|
||||
background: #34495e;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
color: #bdc3c7;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
border-color: #4a5f7a;
|
||||
background: #34495e;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #34495e;
|
||||
color: #ecf0f1;
|
||||
border-color: #4a5f7a;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #4a5f7a;
|
||||
}
|
||||
|
||||
.auth-divider::before {
|
||||
background: #4a5f7a;
|
||||
}
|
||||
|
||||
.auth-divider span {
|
||||
background: #2c3e50;
|
||||
color: #bdc3c7;
|
||||
}
|
||||
|
||||
.password-requirements {
|
||||
background: #34495e;
|
||||
border-color: #4a5f7a;
|
||||
}
|
||||
|
||||
.requirement {
|
||||
color: #bdc3c7;
|
||||
}
|
||||
|
||||
.auth-error,
|
||||
.auth-success {
|
||||
background: #2c3e50;
|
||||
}
|
||||
|
||||
.error-message,
|
||||
.success-message {
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
.verification-content h2 {
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
.verification-content p {
|
||||
color: #bdc3c7;
|
||||
}
|
||||
}
|
||||
197
frontend/debug_favicons.html
Normal file
197
frontend/debug_favicons.html
Normal file
@ -0,0 +1,197 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Favicon Debug Tool</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.bookmark-debug {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.favicon-test {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid #ccc;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.bookmark-info {
|
||||
flex: 1;
|
||||
}
|
||||
.bookmark-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.favicon-data {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
word-break: break-all;
|
||||
}
|
||||
.status {
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.status.has-icon {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.status.no-icon {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
.status.error {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
.summary {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Favicon Debug Tool</h1>
|
||||
<p>This tool will analyze the favicon data in your bookmarks to help identify why some favicons aren't showing.</p>
|
||||
|
||||
<button onclick="analyzeBookmarks()">Analyze Bookmarks</button>
|
||||
|
||||
<div id="summary" class="summary" style="display: none;">
|
||||
<h3>Summary</h3>
|
||||
<div id="summaryContent"></div>
|
||||
</div>
|
||||
|
||||
<div id="results"></div>
|
||||
|
||||
<script>
|
||||
function analyzeBookmarks() {
|
||||
const stored = localStorage.getItem('bookmarks');
|
||||
const resultsDiv = document.getElementById('results');
|
||||
const summaryDiv = document.getElementById('summary');
|
||||
const summaryContent = document.getElementById('summaryContent');
|
||||
|
||||
if (!stored) {
|
||||
resultsDiv.innerHTML = '<p>No bookmarks found in localStorage.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let bookmarks;
|
||||
try {
|
||||
bookmarks = JSON.parse(stored);
|
||||
} catch (error) {
|
||||
resultsDiv.innerHTML = '<p>Error parsing bookmark data: ' + error.message + '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let hasIcon = 0;
|
||||
let noIcon = 0;
|
||||
let errorIcon = 0;
|
||||
let dataUrls = 0;
|
||||
let httpUrls = 0;
|
||||
|
||||
resultsDiv.innerHTML = '';
|
||||
|
||||
bookmarks.forEach((bookmark, index) => {
|
||||
const debugDiv = document.createElement('div');
|
||||
debugDiv.className = 'bookmark-debug';
|
||||
|
||||
const favicon = document.createElement('img');
|
||||
favicon.className = 'favicon-test';
|
||||
favicon.alt = 'Favicon';
|
||||
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'bookmark-info';
|
||||
|
||||
const titleDiv = document.createElement('div');
|
||||
titleDiv.className = 'bookmark-title';
|
||||
titleDiv.textContent = bookmark.title || 'Untitled';
|
||||
|
||||
const statusSpan = document.createElement('span');
|
||||
statusSpan.className = 'status';
|
||||
|
||||
const dataDiv = document.createElement('div');
|
||||
dataDiv.className = 'favicon-data';
|
||||
|
||||
if (bookmark.icon && bookmark.icon.trim() !== '') {
|
||||
hasIcon++;
|
||||
statusSpan.textContent = 'HAS ICON';
|
||||
statusSpan.classList.add('has-icon');
|
||||
|
||||
if (bookmark.icon.startsWith('data:')) {
|
||||
dataUrls++;
|
||||
dataDiv.textContent = `Data URL (${bookmark.icon.length} chars): ${bookmark.icon.substring(0, 100)}...`;
|
||||
} else {
|
||||
httpUrls++;
|
||||
dataDiv.textContent = `URL: ${bookmark.icon}`;
|
||||
}
|
||||
|
||||
favicon.src = bookmark.icon;
|
||||
|
||||
favicon.onerror = function() {
|
||||
statusSpan.textContent = 'ICON ERROR';
|
||||
statusSpan.className = 'status error';
|
||||
errorIcon++;
|
||||
hasIcon--;
|
||||
dataDiv.textContent += ' [FAILED TO LOAD]';
|
||||
};
|
||||
|
||||
} else {
|
||||
noIcon++;
|
||||
statusSpan.textContent = 'NO ICON';
|
||||
statusSpan.classList.add('no-icon');
|
||||
dataDiv.textContent = 'No favicon data found';
|
||||
|
||||
// Use default icon
|
||||
favicon.src = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZmlsbD0iIzk5OSIgZD0iTTggMEMzLjYgMCAwIDMuNiAwIDhzMy42IDggOCA4IDgtMy42IDgtOC0zLjYtOC04LTh6bTAgMTRjLTMuMyAwLTYtMi3LTYtNnMyLjctNiA2LTYgNiAyLjcgNiA2LTIuNyA2LTYgNnoiLz48L3N2Zz4=';
|
||||
}
|
||||
|
||||
infoDiv.appendChild(titleDiv);
|
||||
infoDiv.appendChild(statusSpan);
|
||||
infoDiv.appendChild(dataDiv);
|
||||
|
||||
debugDiv.appendChild(favicon);
|
||||
debugDiv.appendChild(infoDiv);
|
||||
|
||||
resultsDiv.appendChild(debugDiv);
|
||||
});
|
||||
|
||||
// Show summary
|
||||
summaryContent.innerHTML = `
|
||||
<strong>Total Bookmarks:</strong> ${bookmarks.length}<br>
|
||||
<strong>With Favicons:</strong> ${hasIcon} (${Math.round(hasIcon/bookmarks.length*100)}%)<br>
|
||||
<strong>Without Favicons:</strong> ${noIcon} (${Math.round(noIcon/bookmarks.length*100)}%)<br>
|
||||
<strong>Failed to Load:</strong> ${errorIcon} (${Math.round(errorIcon/bookmarks.length*100)}%)<br>
|
||||
<strong>Data URLs:</strong> ${dataUrls}<br>
|
||||
<strong>HTTP URLs:</strong> ${httpUrls}
|
||||
`;
|
||||
summaryDiv.style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
238
frontend/email-verified.html
Normal file
238
frontend/email-verified.html
Normal file
@ -0,0 +1,238 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Email Verified - Bookmark Manager</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="auth-styles.css">
|
||||
<style>
|
||||
.verification-success {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 4rem;
|
||||
color: #27ae60;
|
||||
margin-bottom: 24px;
|
||||
animation: bounceIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
color: #2c3e50;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
color: #7f8c8d;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 32px;
|
||||
max-width: 500px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.success-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-width: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
font-size: 14px;
|
||||
color: #95a5a6;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@keyframes bounceIn {
|
||||
0% {
|
||||
transform: scale(0.3);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
70% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.verification-success > * {
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.verification-success > *:nth-child(2) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
.verification-success > *:nth-child(3) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.verification-success > *:nth-child(4) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
.verification-success > *:nth-child(5) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.success-title {
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
color: #bdc3c7;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
color: #7f8c8d;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 480px) {
|
||||
.success-icon {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="auth-body">
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="verification-success">
|
||||
<div class="success-icon">✅</div>
|
||||
|
||||
<h1 class="success-title">Email Verified Successfully!</h1>
|
||||
|
||||
<p class="success-message">
|
||||
Great! Your email address has been verified and your account is now active.
|
||||
You can now sign in and start managing your bookmarks.
|
||||
</p>
|
||||
|
||||
<div class="success-actions">
|
||||
<a href="login.html" class="btn btn-primary btn-full" id="signInBtn">
|
||||
Sign In to Your Account
|
||||
</a>
|
||||
|
||||
<a href="index.html" class="btn btn-secondary btn-full">
|
||||
Go to Homepage
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="countdown" id="countdown">
|
||||
Redirecting to sign in page in <span id="countdownTimer">10</span> seconds...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Auto-redirect countdown
|
||||
let countdown = 10;
|
||||
const countdownElement = document.getElementById('countdownTimer');
|
||||
const countdownContainer = document.getElementById('countdown');
|
||||
|
||||
function updateCountdown() {
|
||||
countdown--;
|
||||
countdownElement.textContent = countdown;
|
||||
|
||||
if (countdown <= 0) {
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
}
|
||||
|
||||
// Start countdown
|
||||
const countdownInterval = setInterval(updateCountdown, 1000);
|
||||
|
||||
// Clear countdown if user clicks sign in button
|
||||
document.getElementById('signInBtn').addEventListener('click', () => {
|
||||
clearInterval(countdownInterval);
|
||||
countdownContainer.style.display = 'none';
|
||||
});
|
||||
|
||||
// Add some celebration confetti effect (optional)
|
||||
function createConfetti() {
|
||||
const colors = ['#f39c12', '#e74c3c', '#9b59b6', '#3498db', '#2ecc71'];
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
setTimeout(() => {
|
||||
const confetti = document.createElement('div');
|
||||
confetti.style.position = 'fixed';
|
||||
confetti.style.left = Math.random() * 100 + 'vw';
|
||||
confetti.style.top = '-10px';
|
||||
confetti.style.width = '10px';
|
||||
confetti.style.height = '10px';
|
||||
confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
|
||||
confetti.style.borderRadius = '50%';
|
||||
confetti.style.pointerEvents = 'none';
|
||||
confetti.style.zIndex = '9999';
|
||||
confetti.style.animation = `fall ${Math.random() * 3 + 2}s linear forwards`;
|
||||
|
||||
document.body.appendChild(confetti);
|
||||
|
||||
setTimeout(() => {
|
||||
confetti.remove();
|
||||
}, 5000);
|
||||
}, i * 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Add confetti animation CSS
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes fall {
|
||||
to {
|
||||
transform: translateY(100vh) rotate(360deg);
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Trigger confetti after page loads
|
||||
setTimeout(createConfetti, 500);
|
||||
|
||||
// Check if there's a success message in URL params
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const message = urlParams.get('message');
|
||||
if (message) {
|
||||
document.querySelector('.success-message').textContent = decodeURIComponent(message);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
67
frontend/forgot-password.html
Normal file
67
frontend/forgot-password.html
Normal file
@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reset Password - Bookmark Manager</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="auth-styles.css">
|
||||
</head>
|
||||
|
||||
<body class="auth-body">
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h1>Reset Password</h1>
|
||||
<p>Enter your email address and we'll send you a link to reset your password</p>
|
||||
</div>
|
||||
|
||||
<form id="forgotPasswordForm" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input type="email" id="email" name="email" required
|
||||
aria-describedby="emailHelp" autocomplete="email">
|
||||
<div id="emailHelp" class="help-text">Enter the email address associated with your account</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary btn-full" id="resetBtn">
|
||||
<span class="btn-text">Send Reset Link</span>
|
||||
<span class="btn-loading" style="display: none;">
|
||||
<span class="loading-spinner"></span>
|
||||
Sending...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="auth-divider">
|
||||
<span>Remember your password?</span>
|
||||
</div>
|
||||
|
||||
<div class="auth-footer">
|
||||
<a href="login.html" class="btn btn-secondary btn-full">Back to Sign In</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-error" id="authError" style="display: none;" role="alert" aria-live="polite">
|
||||
<div class="error-content">
|
||||
<span class="error-icon">⚠️</span>
|
||||
<span class="error-message" id="errorMessage"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-success" id="authSuccess" style="display: none;" role="alert" aria-live="polite">
|
||||
<div class="success-content">
|
||||
<span class="success-icon">✅</span>
|
||||
<span class="success-message" id="successMessage"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="auth-error-handler.js"></script>
|
||||
<script src="auth-script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
1550
frontend/index.html
Normal file
1550
frontend/index.html
Normal file
File diff suppressed because it is too large
Load Diff
86
frontend/login.html
Normal file
86
frontend/login.html
Normal file
@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - Bookmark Manager</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="auth-styles.css">
|
||||
</head>
|
||||
|
||||
<body class="auth-body">
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h1>Bookmark Manager</h1>
|
||||
<p>Sign in to access your bookmarks</p>
|
||||
</div>
|
||||
|
||||
<form id="loginForm" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input type="email" id="email" name="email" required
|
||||
aria-describedby="emailHelp" autocomplete="email">
|
||||
<div id="emailHelp" class="help-text">Enter your registered email address</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required
|
||||
aria-describedby="passwordHelp" autocomplete="current-password">
|
||||
<div id="passwordHelp" class="help-text">Enter your account password</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="rememberMe" name="rememberMe">
|
||||
<span class="checkmark"></span>
|
||||
<span class="checkbox-text">Remember me</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary btn-full" id="loginBtn">
|
||||
<span class="btn-text">Sign In</span>
|
||||
<span class="btn-loading" style="display: none;">
|
||||
<span class="loading-spinner"></span>
|
||||
Signing in...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="auth-links">
|
||||
<a href="forgot-password.html" class="auth-link">Forgot your password?</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="auth-divider">
|
||||
<span>Don't have an account?</span>
|
||||
</div>
|
||||
|
||||
<div class="auth-footer">
|
||||
<a href="register.html" class="btn btn-secondary btn-full">Create Account</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-error" id="authError" style="display: none;" role="alert" aria-live="polite">
|
||||
<div class="error-content">
|
||||
<span class="error-icon">⚠️</span>
|
||||
<span class="error-message" id="errorMessage"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-success" id="authSuccess" style="display: none;" role="alert" aria-live="polite">
|
||||
<div class="success-content">
|
||||
<span class="success-icon">✅</span>
|
||||
<span class="success-message" id="successMessage"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="auth-error-handler.js"></script>
|
||||
<script src="auth-script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
111
frontend/register.html
Normal file
111
frontend/register.html
Normal file
@ -0,0 +1,111 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Create Account - Bookmark Manager</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="auth-styles.css">
|
||||
</head>
|
||||
|
||||
<body class="auth-body">
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h1>Create Account</h1>
|
||||
<p>Join Bookmark Manager to save and sync your bookmarks</p>
|
||||
</div>
|
||||
|
||||
<form id="registerForm" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input type="email" id="email" name="email" required
|
||||
aria-describedby="emailHelp" autocomplete="email">
|
||||
<div id="emailHelp" class="help-text">We'll send a verification email to this address</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required
|
||||
aria-describedby="passwordHelp" autocomplete="new-password">
|
||||
<div id="passwordHelp" class="help-text">Password requirements:</div>
|
||||
<div class="password-requirements" id="passwordRequirements">
|
||||
<div class="requirement" id="req-length">
|
||||
<span class="req-icon">○</span>
|
||||
<span class="req-text">At least 8 characters</span>
|
||||
</div>
|
||||
<div class="requirement" id="req-uppercase">
|
||||
<span class="req-icon">○</span>
|
||||
<span class="req-text">One uppercase letter</span>
|
||||
</div>
|
||||
<div class="requirement" id="req-lowercase">
|
||||
<span class="req-icon">○</span>
|
||||
<span class="req-text">One lowercase letter</span>
|
||||
</div>
|
||||
<div class="requirement" id="req-number">
|
||||
<span class="req-icon">○</span>
|
||||
<span class="req-text">One number</span>
|
||||
</div>
|
||||
<div class="requirement" id="req-special">
|
||||
<span class="req-icon">○</span>
|
||||
<span class="req-text">One special character</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirmPassword">Confirm Password</label>
|
||||
<input type="password" id="confirmPassword" name="confirmPassword" required
|
||||
aria-describedby="confirmPasswordHelp" autocomplete="new-password">
|
||||
<div id="confirmPasswordHelp" class="help-text">Re-enter your password to confirm</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="agreeTerms" name="agreeTerms" required>
|
||||
<span class="checkmark"></span>
|
||||
<span class="checkbox-text">I agree to the <a href="#" class="auth-link">Terms of Service</a> and <a href="#" class="auth-link">Privacy Policy</a></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary btn-full" id="registerBtn">
|
||||
<span class="btn-text">Create Account</span>
|
||||
<span class="btn-loading" style="display: none;">
|
||||
<span class="loading-spinner"></span>
|
||||
Creating account...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="auth-divider">
|
||||
<span>Already have an account?</span>
|
||||
</div>
|
||||
|
||||
<div class="auth-footer">
|
||||
<a href="login.html" class="btn btn-secondary btn-full">Sign In</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-error" id="authError" style="display: none;" role="alert" aria-live="polite">
|
||||
<div class="error-content">
|
||||
<span class="error-icon">⚠️</span>
|
||||
<span class="error-message" id="errorMessage"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-success" id="authSuccess" style="display: none;" role="alert" aria-live="polite">
|
||||
<div class="success-content">
|
||||
<span class="success-icon">✅</span>
|
||||
<span class="success-message" id="successMessage"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="auth-error-handler.js"></script>
|
||||
<script src="auth-script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
93
frontend/reset-password.html
Normal file
93
frontend/reset-password.html
Normal file
@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Set New Password - Bookmark Manager</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="auth-styles.css">
|
||||
</head>
|
||||
|
||||
<body class="auth-body">
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h1>Set New Password</h1>
|
||||
<p>Enter your new password below</p>
|
||||
</div>
|
||||
|
||||
<form id="resetPasswordForm" class="auth-form">
|
||||
<input type="hidden" id="resetToken" name="resetToken">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="newPassword">New Password</label>
|
||||
<input type="password" id="newPassword" name="newPassword" required
|
||||
aria-describedby="passwordHelp" autocomplete="new-password">
|
||||
<div id="passwordHelp" class="help-text">Password requirements:</div>
|
||||
<div class="password-requirements" id="passwordRequirements">
|
||||
<div class="requirement" id="req-length">
|
||||
<span class="req-icon">○</span>
|
||||
<span class="req-text">At least 8 characters</span>
|
||||
</div>
|
||||
<div class="requirement" id="req-uppercase">
|
||||
<span class="req-icon">○</span>
|
||||
<span class="req-text">One uppercase letter</span>
|
||||
</div>
|
||||
<div class="requirement" id="req-lowercase">
|
||||
<span class="req-icon">○</span>
|
||||
<span class="req-text">One lowercase letter</span>
|
||||
</div>
|
||||
<div class="requirement" id="req-number">
|
||||
<span class="req-icon">○</span>
|
||||
<span class="req-text">One number</span>
|
||||
</div>
|
||||
<div class="requirement" id="req-special">
|
||||
<span class="req-icon">○</span>
|
||||
<span class="req-text">One special character</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirmNewPassword">Confirm New Password</label>
|
||||
<input type="password" id="confirmNewPassword" name="confirmNewPassword" required
|
||||
aria-describedby="confirmPasswordHelp" autocomplete="new-password">
|
||||
<div id="confirmPasswordHelp" class="help-text">Re-enter your new password to confirm</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary btn-full" id="updatePasswordBtn">
|
||||
<span class="btn-text">Update Password</span>
|
||||
<span class="btn-loading" style="display: none;">
|
||||
<span class="loading-spinner"></span>
|
||||
Updating...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<a href="login.html" class="btn btn-secondary btn-full">Back to Sign In</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-error" id="authError" style="display: none;" role="alert" aria-live="polite">
|
||||
<div class="error-content">
|
||||
<span class="error-icon">⚠️</span>
|
||||
<span class="error-message" id="errorMessage"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-success" id="authSuccess" style="display: none;" role="alert" aria-live="polite">
|
||||
<div class="success-content">
|
||||
<span class="success-icon">✅</span>
|
||||
<span class="success-message" id="successMessage"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="auth-script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
10769
frontend/script.js
Normal file
10769
frontend/script.js
Normal file
File diff suppressed because it is too large
Load Diff
4416
frontend/styles.css
Normal file
4416
frontend/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
113
frontend/verify-email.html
Normal file
113
frontend/verify-email.html
Normal file
@ -0,0 +1,113 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Email Verification - Bookmark Manager</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="auth-styles.css">
|
||||
</head>
|
||||
|
||||
<body class="auth-body">
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h1>Email Verification</h1>
|
||||
</div>
|
||||
|
||||
<!-- Success State -->
|
||||
<div id="verificationSuccess" class="verification-state" style="display: none;">
|
||||
<div class="verification-icon success">
|
||||
<span class="icon">✅</span>
|
||||
</div>
|
||||
<div class="verification-content">
|
||||
<h2>Email Verified Successfully!</h2>
|
||||
<p>Your email address has been verified. You can now sign in to your account.</p>
|
||||
</div>
|
||||
<div class="auth-footer">
|
||||
<a href="login.html" class="btn btn-primary btn-full">Sign In to Your Account</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div id="verificationError" class="verification-state" style="display: none;">
|
||||
<div class="verification-icon error">
|
||||
<span class="icon">❌</span>
|
||||
</div>
|
||||
<div class="verification-content">
|
||||
<h2>Verification Failed</h2>
|
||||
<p id="errorDescription">The verification link is invalid or has expired.</p>
|
||||
</div>
|
||||
<div class="auth-footer">
|
||||
<button id="resendVerificationBtn" class="btn btn-primary btn-full">
|
||||
<span class="btn-text">Resend Verification Email</span>
|
||||
<span class="btn-loading" style="display: none;">
|
||||
<span class="loading-spinner"></span>
|
||||
Sending...
|
||||
</span>
|
||||
</button>
|
||||
<a href="login.html" class="btn btn-secondary btn-full">Back to Sign In</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="verificationLoading" class="verification-state">
|
||||
<div class="verification-icon loading">
|
||||
<span class="loading-spinner large"></span>
|
||||
</div>
|
||||
<div class="verification-content">
|
||||
<h2>Verifying Your Email</h2>
|
||||
<p>Please wait while we verify your email address...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resend Success -->
|
||||
<div id="resendSuccess" class="verification-state" style="display: none;">
|
||||
<div class="verification-icon success">
|
||||
<span class="icon">📧</span>
|
||||
</div>
|
||||
<div class="verification-content">
|
||||
<h2>Verification Email Sent</h2>
|
||||
<p>We've sent a new verification email to your address. Please check your inbox and click the verification link.</p>
|
||||
</div>
|
||||
<div class="auth-footer">
|
||||
<a href="login.html" class="btn btn-secondary btn-full">Back to Sign In</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-error" id="authError" style="display: none;" role="alert" aria-live="polite">
|
||||
<div class="error-content">
|
||||
<span class="error-icon">⚠️</span>
|
||||
<span class="error-message" id="errorMessage"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="auth-script.js"></script>
|
||||
<script>
|
||||
// Check for error message in URL parameters
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const errorMessage = urlParams.get('error');
|
||||
|
||||
if (errorMessage) {
|
||||
// Hide loading state and show error
|
||||
document.getElementById('verificationLoading').style.display = 'none';
|
||||
document.getElementById('verificationError').style.display = 'block';
|
||||
|
||||
// Update error description
|
||||
const errorDescription = document.getElementById('errorDescription');
|
||||
errorDescription.textContent = decodeURIComponent(errorMessage);
|
||||
|
||||
// Clear URL parameters to clean up the URL
|
||||
if (window.history.replaceState) {
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user