Files
TaskMate/backend/routes/auth.js
2026-01-04 00:24:11 +00:00

366 Zeilen
11 KiB
JavaScript

/**
* 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;