/** * TASKMATE - Auth Routes * ====================== * Login, Logout, Token-Refresh */ const express = require('express'); const router = express.Router(); const bcrypt = require('bcryptjs'); const { getDb } = require('../database'); const { generateToken, generateRefreshToken, refreshAccessToken, revokeAllRefreshTokens, authenticateToken } = require('../middleware/auth'); const { getTokenForUser } = require('../middleware/csrf'); const { validatePassword } = require('../middleware/validation'); const logger = require('../utils/logger'); // Konfiguration const MAX_LOGIN_ATTEMPTS = parseInt(process.env.MAX_LOGIN_ATTEMPTS) || 5; const LOCKOUT_DURATION = (parseInt(process.env.LOCKOUT_DURATION_MINUTES) || 15) * 60 * 1000; /** * POST /api/auth/login * Benutzer anmelden * - Admin-User loggt sich mit username "admin" ein * - Alle anderen User loggen sich mit E-Mail ein */ router.post('/login', async (req, res) => { try { const { username, password } = req.body; const ip = req.ip || req.connection.remoteAddress; const userAgent = req.headers['user-agent']; if (!username || !password) { return res.status(400).json({ error: 'E-Mail/Benutzername und Passwort erforderlich' }); } const db = getDb(); // Benutzer suchen: Zuerst nach Username "admin", dann nach E-Mail let user; // User per Username suchen (kann E-Mail-Adresse oder admin sein) user = db.prepare('SELECT * FROM users WHERE username = ?').get(username); // Audit-Log Eintrag vorbereiten const logAttempt = (userId, success) => { db.prepare(` INSERT INTO login_audit (user_id, ip_address, success, user_agent) VALUES (?, ?, ?, ?) `).run(userId, ip, success ? 1 : 0, userAgent); }; if (!user) { logger.warn(`Login fehlgeschlagen: Benutzer nicht gefunden - ${username}`); return res.status(401).json({ error: 'Ungültige Anmeldedaten' }); } // Prüfen ob Account gesperrt ist if (user.locked_until) { const lockedUntil = new Date(user.locked_until).getTime(); if (Date.now() < lockedUntil) { const remainingMinutes = Math.ceil((lockedUntil - Date.now()) / 60000); logger.warn(`Login blockiert: Account gesperrt - ${username}`); return res.status(423).json({ error: `Account ist gesperrt. Versuche es in ${remainingMinutes} Minuten erneut.` }); } else { // Sperre aufheben db.prepare('UPDATE users SET locked_until = NULL, failed_attempts = 0 WHERE id = ?') .run(user.id); } } // Passwort prüfen const validPassword = await bcrypt.compare(password, user.password_hash); if (!validPassword) { // Fehlversuche erhöhen const newFailedAttempts = (user.failed_attempts || 0) + 1; if (newFailedAttempts >= MAX_LOGIN_ATTEMPTS) { // Account sperren const lockUntil = new Date(Date.now() + LOCKOUT_DURATION).toISOString(); db.prepare('UPDATE users SET failed_attempts = ?, locked_until = ? WHERE id = ?') .run(newFailedAttempts, lockUntil, user.id); logger.warn(`Account gesperrt nach ${MAX_LOGIN_ATTEMPTS} Fehlversuchen: ${username}`); } else { db.prepare('UPDATE users SET failed_attempts = ? WHERE id = ?') .run(newFailedAttempts, user.id); } logAttempt(user.id, false); logger.warn(`Login fehlgeschlagen: Falsches Passwort - ${username} (Versuch ${newFailedAttempts})`); const remainingAttempts = MAX_LOGIN_ATTEMPTS - newFailedAttempts; return res.status(401).json({ error: 'Ungültige Anmeldedaten', remainingAttempts: remainingAttempts > 0 ? remainingAttempts : 0 }); } // Login erfolgreich - Fehlversuche zurücksetzen db.prepare(` UPDATE users SET failed_attempts = 0, locked_until = NULL, last_login = CURRENT_TIMESTAMP WHERE id = ? `).run(user.id); logAttempt(user.id, true); // JWT Access-Token generieren (kurze Lebensdauer) const accessToken = generateToken(user); // Refresh-Token generieren (lange Lebensdauer) const refreshToken = generateRefreshToken(user.id, ip, userAgent); // CSRF-Token generieren const csrfToken = getTokenForUser(user.id); logger.info(`Login erfolgreich: ${username}`); // Permissions parsen let permissions = []; try { permissions = JSON.parse(user.permissions || '[]'); } catch (e) { permissions = []; } res.json({ token: accessToken, refreshToken, csrfToken, user: { id: user.id, username: user.username, displayName: user.display_name, color: user.color, role: user.role || 'user', permissions: permissions } }); } catch (error) { logger.error('Login-Fehler:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * POST /api/auth/logout * Benutzer abmelden und Refresh-Tokens widerrufen */ router.post('/logout', authenticateToken, (req, res) => { try { // Alle Refresh-Tokens des Benutzers löschen revokeAllRefreshTokens(req.user.id); logger.info(`Logout: ${req.user.username}`); res.json({ message: 'Erfolgreich abgemeldet' }); } catch (error) { logger.error('Logout-Fehler:', { error: error.message }); res.status(500).json({ error: 'Logout fehlgeschlagen' }); } }); /** * GET /api/auth/me * Aktuellen Benutzer abrufen */ router.get('/me', authenticateToken, (req, res) => { try { const db = getDb(); const user = db.prepare('SELECT id, username, display_name, color, role, permissions FROM users WHERE id = ?') .get(req.user.id); if (!user) { return res.status(404).json({ error: 'Benutzer nicht gefunden' }); } // CSRF-Token erneuern const csrfToken = getTokenForUser(user.id); // Permissions parsen let permissions = []; try { permissions = JSON.parse(user.permissions || '[]'); } catch (e) { permissions = []; } res.json({ user: { id: user.id, username: user.username, displayName: user.display_name, color: user.color, role: user.role || 'user', permissions: permissions }, csrfToken }); } catch (error) { logger.error('Fehler bei /me:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * POST /api/auth/refresh * Token mit Refresh-Token erneuern */ router.post('/refresh', async (req, res) => { try { const { refreshToken } = req.body; const ip = req.ip || req.connection.remoteAddress; const userAgent = req.headers['user-agent']; if (!refreshToken) { // Fallback für alte Clients - mit Access Token authentifizieren if (req.headers.authorization) { return legacyRefresh(req, res); } return res.status(400).json({ error: 'Refresh-Token erforderlich' }); } // Neuen Access-Token mit Refresh-Token generieren const accessToken = await refreshAccessToken(refreshToken, ip, userAgent); const db = getDb(); // User-Daten für CSRF-Token abrufen const decoded = require('jsonwebtoken').decode(accessToken); const csrfToken = getTokenForUser(decoded.id); res.json({ token: accessToken, csrfToken }); } catch (error) { logger.error('Token-Refresh Fehler:', { error: error.message }); res.status(401).json({ error: 'Token-Erneuerung fehlgeschlagen' }); } }); // Legacy Refresh für Rückwärtskompatibilität function legacyRefresh(req, res) { // Prüfe Authorization Header const authHeader = req.headers['authorization']; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Nicht authentifiziert' }); } const token = authHeader.substring(7); const user = require('../middleware/auth').verifyToken(token); if (!user) { return res.status(401).json({ error: 'Token ungültig' }); } const db = getDb(); const dbUser = db.prepare('SELECT * FROM users WHERE id = ?').get(user.id); if (!dbUser) { return res.status(404).json({ error: 'Benutzer nicht gefunden' }); } const newToken = generateToken(dbUser); const csrfToken = getTokenForUser(dbUser.id); res.json({ token: newToken, csrfToken }); } /** * PUT /api/auth/password * Passwort ändern */ router.put('/password', authenticateToken, async (req, res) => { try { const { currentPassword, newPassword } = req.body; if (!currentPassword || !newPassword) { return res.status(400).json({ error: 'Aktuelles und neues Passwort erforderlich' }); } // Passwort-Richtlinien prüfen const passwordErrors = validatePassword(newPassword); if (passwordErrors.length > 0) { return res.status(400).json({ error: passwordErrors.join('. ') }); } const db = getDb(); const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.user.id); // Aktuelles Passwort prüfen const validPassword = await bcrypt.compare(currentPassword, user.password_hash); if (!validPassword) { return res.status(401).json({ error: 'Aktuelles Passwort ist falsch' }); } // Neues Passwort hashen und speichern const newHash = await bcrypt.hash(newPassword, 12); db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(newHash, user.id); logger.info(`Passwort geändert: ${user.username}`); res.json({ message: 'Passwort erfolgreich geändert' }); } catch (error) { logger.error('Passwort-Änderung Fehler:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * PUT /api/auth/color * Benutzerfarbe ändern */ router.put('/color', authenticateToken, (req, res) => { try { const { color } = req.body; if (!color) { return res.status(400).json({ error: 'Farbe erforderlich' }); } // Validate hex color format const hexColorRegex = /^#[0-9A-Fa-f]{6}$/; if (!hexColorRegex.test(color)) { return res.status(400).json({ error: 'Ungültiges Farbformat (erwartet: #RRGGBB)' }); } const db = getDb(); db.prepare('UPDATE users SET color = ? WHERE id = ?').run(color, req.user.id); logger.info(`Farbe geändert: ${req.user.username} -> ${color}`); res.json({ message: 'Farbe erfolgreich geändert', color }); } catch (error) { logger.error('Fehler beim Ändern der Farbe:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * GET /api/auth/users * Alle Benutzer abrufen (für Zuweisung) * Admin-Benutzer werden ausgeschlossen, da sie nur fuer die Benutzerverwaltung sind */ router.get('/users', authenticateToken, (req, res) => { try { const db = getDb(); // Nur regulaere Benutzer (nicht Admins) fuer Aufgaben-Zuweisung const users = db.prepare(` SELECT id, username, display_name, color FROM users WHERE role != 'admin' OR role IS NULL `).all(); res.json(users.map(u => ({ id: u.id, username: u.username, displayName: u.display_name, color: u.color }))); } catch (error) { logger.error('Fehler beim Abrufen der Benutzer:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); module.exports = router;