This commit is contained in:
2025-07-20 20:43:06 +02:00
parent 0abee5b794
commit 29592c7fc8
93 changed files with 23400 additions and 131 deletions

View 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()">&times;</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
View 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
View 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;
}
}

View 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>

View 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>

View 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

File diff suppressed because it is too large Load Diff

86
frontend/login.html Normal file
View 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
View 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>

View 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

File diff suppressed because it is too large Load Diff

4416
frontend/styles.css Normal file

File diff suppressed because it is too large Load Diff

113
frontend/verify-email.html Normal file
View 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>