Dieser Commit ist enthalten in:
Claude Project Manager
2025-12-28 21:36:45 +00:00
Commit ab1e5be9a9
146 geänderte Dateien mit 65525 neuen und 0 gelöschten Zeilen

546
frontend/js/auth.js Normale Datei
Datei anzeigen

@ -0,0 +1,546 @@
/**
* 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;