/** * TASKMATE - Authentication Module * ================================ */ import api from './api.js'; import { $, $$ } from './utils.js'; class AuthManager { constructor() { this.user = null; this.isAuthenticated = false; this.loginAttempts = 0; this.maxLoginAttempts = 5; this.lockoutDuration = 5 * 60 * 1000; // 5 minutes this.lockoutUntil = null; } // Initialize authentication state async init() { const token = api.getToken(); if (token) { try { // Verify token by making a request const users = await api.getUsers(); this.isAuthenticated = true; // Get current user from stored data const storedUser = localStorage.getItem('current_user'); if (storedUser) { this.user = JSON.parse(storedUser); } return true; } catch (error) { // Token invalid this.logout(); return false; } } return false; } // Login async login(username, password) { // Check lockout if (this.isLockedOut()) { const remainingTime = Math.ceil((this.lockoutUntil - Date.now()) / 1000); throw new Error(`Zu viele Fehlversuche. Bitte warten Sie ${remainingTime} Sekunden.`); } try { const response = await api.login(username, password); this.user = response.user; this.isAuthenticated = true; this.loginAttempts = 0; // Store user data localStorage.setItem('current_user', JSON.stringify(response.user)); // Dispatch login event window.dispatchEvent(new CustomEvent('auth:login', { detail: { user: this.user } })); return response; } catch (error) { this.loginAttempts++; if (this.loginAttempts >= this.maxLoginAttempts) { this.lockoutUntil = Date.now() + this.lockoutDuration; } throw error; } } // Logout async logout() { try { if (this.isAuthenticated) { await api.logout(); } } catch (error) { // Ignore logout errors } finally { this.user = null; this.isAuthenticated = false; // Clear stored data localStorage.removeItem('current_user'); localStorage.removeItem('auth_token'); // Dispatch logout event window.dispatchEvent(new CustomEvent('auth:logout')); } } // Change Password async changePassword(currentPassword, newPassword) { return api.changePassword(currentPassword, newPassword); } // Get Current User getUser() { return this.user; } // Check if logged in isLoggedIn() { return this.isAuthenticated && this.user !== null; } // Check lockout status isLockedOut() { if (!this.lockoutUntil) return false; if (Date.now() >= this.lockoutUntil) { this.lockoutUntil = null; this.loginAttempts = 0; return false; } return true; } // Get user initials getUserInitials() { if (!this.user) return '?'; return this.user.username .split(' ') .map(part => part.charAt(0).toUpperCase()) .slice(0, 2) .join(''); } // Get user color getUserColor() { if (!this.user) return '#888888'; return this.user.color || '#00D4FF'; } // Update user color updateUserColor(color) { if (this.user) { this.user.color = color; // Also update localStorage so color persists after refresh localStorage.setItem('current_user', JSON.stringify(this.user)); } } // Check if user is admin isAdmin() { return this.user?.role === 'admin'; } // Check if user has a specific permission hasPermission(permission) { if (!this.user) return false; const permissions = this.user.permissions || []; return permissions.includes(permission); } // Get user role getRole() { return this.user?.role || 'user'; } // Get user permissions getPermissions() { return this.user?.permissions || []; } } // Login Form Handler class LoginFormHandler { constructor(authManager) { this.auth = authManager; this.form = $('#login-form'); this.usernameInput = $('#login-username'); this.passwordInput = $('#login-password'); this.submitButton = this.form?.querySelector('button[type="submit"]'); this.errorMessage = $('#login-error'); this.bindEvents(); } bindEvents() { if (!this.form) return; this.form.addEventListener('submit', (e) => this.handleSubmit(e)); // Enter key handling this.passwordInput?.addEventListener('keypress', (e) => { if (e.key === 'Enter') { this.form.dispatchEvent(new Event('submit')); } }); // Clear error on input [this.usernameInput, this.passwordInput].forEach(input => { input?.addEventListener('input', () => this.clearError()); }); } async handleSubmit(e) { e.preventDefault(); const username = this.usernameInput?.value.trim(); const password = this.passwordInput?.value; if (!username || !password) { this.showError('Bitte Benutzername und Passwort eingeben.'); return; } this.setLoading(true); this.clearError(); try { await this.auth.login(username, password); // Success - app will handle the redirect } catch (error) { this.showError(error.message || 'Anmeldung fehlgeschlagen.'); this.passwordInput.value = ''; this.passwordInput.focus(); } finally { this.setLoading(false); } } showError(message) { if (this.errorMessage) { this.errorMessage.textContent = message; this.errorMessage.classList.remove('hidden'); } } clearError() { if (this.errorMessage) { this.errorMessage.textContent = ''; this.errorMessage.classList.add('hidden'); } } setLoading(loading) { if (this.submitButton) { this.submitButton.disabled = loading; this.submitButton.classList.toggle('loading', loading); } if (this.usernameInput) this.usernameInput.disabled = loading; if (this.passwordInput) this.passwordInput.disabled = loading; } reset() { if (this.form) this.form.reset(); this.clearError(); this.setLoading(false); } } // User Menu Handler class UserMenuHandler { constructor(authManager) { this.auth = authManager; this.userMenu = $('.user-menu'); this.userAvatar = $('#user-avatar'); this.userDropdown = $('.user-dropdown'); this.userName = $('#user-name'); this.userRole = $('#user-role'); this.logoutButton = $('#btn-logout'); this.changePasswordButton = $('#btn-change-password'); this.isOpen = false; this.bindEvents(); } bindEvents() { // Toggle dropdown this.userAvatar?.addEventListener('click', (e) => { e.stopPropagation(); this.toggle(); }); // Close on outside click document.addEventListener('click', (e) => { if (!this.userMenu?.contains(e.target)) { this.close(); } }); // Logout this.logoutButton?.addEventListener('click', () => this.handleLogout()); // Change password this.changePasswordButton?.addEventListener('click', () => { this.close(); window.dispatchEvent(new CustomEvent('modal:open', { detail: { modalId: 'change-password-modal' } })); }); // Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isOpen) { this.close(); } }); } update() { const user = this.auth.getUser(); if (this.userAvatar) { this.userAvatar.textContent = this.auth.getUserInitials(); this.userAvatar.style.backgroundColor = this.auth.getUserColor(); } if (this.userName) { this.userName.textContent = user?.displayName || user?.username || 'Benutzer'; } if (this.userRole) { let roleText = 'Benutzer'; if (user?.role === 'admin') { roleText = 'Administrator'; } else if (user?.permissions?.length > 0) { roleText = user.permissions.map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(', '); } this.userRole.textContent = roleText; } } toggle() { if (this.isOpen) { this.close(); } else { this.open(); } } open() { if (this.userDropdown) { this.userDropdown.classList.remove('hidden'); this.isOpen = true; } } close() { if (this.userDropdown) { this.userDropdown.classList.add('hidden'); this.isOpen = false; } } async handleLogout() { this.close(); try { await this.auth.logout(); } catch (error) { console.error('Logout error:', error); } } } // Change Password Modal Handler class ChangePasswordHandler { constructor(authManager) { this.auth = authManager; this.modal = $('#change-password-modal'); this.form = $('#change-password-form'); this.currentPassword = $('#current-password'); this.newPassword = $('#new-password'); this.confirmPassword = $('#confirm-password'); this.errorMessage = this.modal?.querySelector('.error-message'); this.submitButton = this.form?.querySelector('button[type="submit"]'); this.bindEvents(); } bindEvents() { this.form?.addEventListener('submit', (e) => this.handleSubmit(e)); // Password strength indicator this.newPassword?.addEventListener('input', () => { this.updatePasswordStrength(); }); // Confirm password validation this.confirmPassword?.addEventListener('input', () => { this.validateConfirmPassword(); }); } async handleSubmit(e) { e.preventDefault(); const currentPassword = this.currentPassword?.value; const newPassword = this.newPassword?.value; const confirmPassword = this.confirmPassword?.value; // Validation if (!currentPassword || !newPassword || !confirmPassword) { this.showError('Bitte alle Felder ausfüllen.'); return; } if (newPassword.length < 8) { this.showError('Das neue Passwort muss mindestens 8 Zeichen lang sein.'); return; } if (newPassword !== confirmPassword) { this.showError('Die Passwörter stimmen nicht überein.'); return; } this.setLoading(true); this.clearError(); try { await this.auth.changePassword(currentPassword, newPassword); // Success this.reset(); this.closeModal(); window.dispatchEvent(new CustomEvent('toast:show', { detail: { message: 'Passwort erfolgreich geändert.', type: 'success' } })); } catch (error) { this.showError(error.message || 'Fehler beim Ändern des Passworts.'); } finally { this.setLoading(false); } } updatePasswordStrength() { const password = this.newPassword?.value || ''; const strengthIndicator = this.modal?.querySelector('.password-strength'); if (!strengthIndicator) return; let strength = 0; if (password.length >= 8) strength++; if (password.length >= 12) strength++; if (/[A-Z]/.test(password)) strength++; if (/[a-z]/.test(password)) strength++; if (/[0-9]/.test(password)) strength++; if (/[^A-Za-z0-9]/.test(password)) strength++; const levels = ['weak', 'fair', 'good', 'strong']; const level = Math.min(Math.floor(strength / 1.5), 3); strengthIndicator.className = `password-strength ${levels[level]}`; strengthIndicator.dataset.strength = levels[level]; } validateConfirmPassword() { const newPassword = this.newPassword?.value; const confirmPassword = this.confirmPassword?.value; if (confirmPassword && newPassword !== confirmPassword) { this.confirmPassword.setCustomValidity('Passwörter stimmen nicht überein'); } else { this.confirmPassword.setCustomValidity(''); } } showError(message) { if (this.errorMessage) { this.errorMessage.textContent = message; this.errorMessage.classList.remove('hidden'); } } clearError() { if (this.errorMessage) { this.errorMessage.textContent = ''; this.errorMessage.classList.add('hidden'); } } setLoading(loading) { if (this.submitButton) { this.submitButton.disabled = loading; this.submitButton.classList.toggle('loading', loading); } } reset() { if (this.form) this.form.reset(); this.clearError(); this.setLoading(false); } closeModal() { window.dispatchEvent(new CustomEvent('modal:close', { detail: { modalId: 'change-password-modal' } })); } } // Create singleton instances const authManager = new AuthManager(); let loginFormHandler = null; let userMenuHandler = null; let changePasswordHandler = null; // Initialize handlers when DOM is ready function initAuthHandlers() { loginFormHandler = new LoginFormHandler(authManager); userMenuHandler = new UserMenuHandler(authManager); changePasswordHandler = new ChangePasswordHandler(authManager); } // Listen for DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initAuthHandlers); } else { initAuthHandlers(); } // Listen for login event to update UI window.addEventListener('auth:login', () => { userMenuHandler?.update(); }); export { authManager, loginFormHandler, userMenuHandler, changePasswordHandler }; export default authManager;