Initial commit
Dieser Commit ist enthalten in:
319
backend/routes/auth.js
Normale Datei
319
backend/routes/auth.js
Normale Datei
@ -0,0 +1,319 @@
|
||||
/**
|
||||
* 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, 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;
|
||||
if (username.toLowerCase() === 'admin') {
|
||||
// Admin-User per Username suchen
|
||||
user = db.prepare('SELECT * FROM users WHERE username = ?').get('admin');
|
||||
} else {
|
||||
// Normale User per E-Mail suchen
|
||||
user = db.prepare('SELECT * FROM users WHERE email = ?').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-Token generieren
|
||||
const token = generateToken(user);
|
||||
|
||||
// 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,
|
||||
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
|
||||
*/
|
||||
router.post('/logout', authenticateToken, (req, res) => {
|
||||
// Bei JWT gibt es serverseitig nichts zu tun
|
||||
// Client muss Token löschen
|
||||
logger.info(`Logout: ${req.user.username}`);
|
||||
res.json({ message: 'Erfolgreich abgemeldet' });
|
||||
});
|
||||
|
||||
/**
|
||||
* 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 erneuern
|
||||
*/
|
||||
router.post('/refresh', authenticateToken, (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.user.id);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'Benutzer nicht gefunden' });
|
||||
}
|
||||
|
||||
const token = generateToken(user);
|
||||
const csrfToken = getTokenForUser(user.id);
|
||||
|
||||
res.json({ token, csrfToken });
|
||||
} catch (error) {
|
||||
logger.error('Token-Refresh Fehler:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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;
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren