/** * TASKMATE - Admin Routes * ======================= * API-Endpunkte für Benutzerverwaltung */ const express = require('express'); const bcrypt = require('bcryptjs'); const router = express.Router(); const { getDb } = require('../database'); const { authenticateToken, requireAdmin } = require('../middleware/auth'); const logger = require('../utils/logger'); const backup = require('../utils/backup'); /** * Standard-Upload-Einstellungen (neues Format mit Dateiendungen) */ const DEFAULT_UPLOAD_SETTINGS = { maxFileSizeMB: 15, allowedExtensions: ['pdf', 'docx', 'txt'] }; // Alle Admin-Routes erfordern Authentifizierung und Admin-Rolle router.use(authenticateToken); router.use(requireAdmin); /** * GET /api/admin/users - Alle Benutzer abrufen */ router.get('/users', (req, res) => { try { const db = getDb(); const users = db.prepare(` SELECT id, username, display_name, color, role, permissions, email, created_at, last_login, failed_attempts, locked_until FROM users ORDER BY id `).all(); // Permissions parsen const parsedUsers = users.map(user => ({ ...user, permissions: JSON.parse(user.permissions || '[]') })); res.json(parsedUsers); } catch (error) { logger.error('Fehler beim Abrufen der Benutzer:', error); res.status(500).json({ error: 'Fehler beim Abrufen der Benutzer' }); } }); /** * POST /api/admin/users - Neuen Benutzer anlegen */ router.post('/users', async (req, res) => { try { const { username, password, displayName, email, role, permissions } = req.body; // Validierung if (!username || !password || !displayName || !email) { return res.status(400).json({ error: 'Kürzel, Passwort, Anzeigename und E-Mail erforderlich' }); } // E-Mail-Validierung const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { return res.status(400).json({ error: 'Ungültige E-Mail-Adresse' }); } // Kürzel muss genau 2 Buchstaben sein const usernameUpper = username.toUpperCase(); if (!/^[A-Z]{2}$/.test(usernameUpper)) { return res.status(400).json({ error: 'Kürzel muss genau 2 Buchstaben sein (z.B. HG)' }); } if (password.length < 8) { return res.status(400).json({ error: 'Passwort muss mindestens 8 Zeichen haben' }); } const db = getDb(); // Prüfen ob Kürzel bereits existiert const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(usernameUpper); if (existing) { return res.status(400).json({ error: 'Kürzel bereits vergeben' }); } // Prüfen ob E-Mail bereits existiert const existingEmail = db.prepare('SELECT id FROM users WHERE email = ?').get(email.toLowerCase()); if (existingEmail) { return res.status(400).json({ error: 'E-Mail bereits vergeben' }); } // Passwort hashen const passwordHash = await bcrypt.hash(password, 12); // Standardfarbe Grau const defaultColor = '#808080'; // Benutzer erstellen const result = db.prepare(` INSERT INTO users (username, password_hash, display_name, color, role, permissions, email) VALUES (?, ?, ?, ?, ?, ?, ?) `).run( usernameUpper, passwordHash, displayName, defaultColor, role || 'user', JSON.stringify(permissions || []), email.toLowerCase() ); logger.info(`Admin ${req.user.username} hat Benutzer ${usernameUpper} erstellt`); res.status(201).json({ id: result.lastInsertRowid, username: usernameUpper, displayName, email: email.toLowerCase(), color: defaultColor, role: role || 'user', permissions: permissions || [] }); } catch (error) { logger.error('Fehler beim Erstellen des Benutzers:', error); res.status(500).json({ error: 'Fehler beim Erstellen des Benutzers' }); } }); /** * PUT /api/admin/users/:id - Benutzer bearbeiten */ router.put('/users/:id', async (req, res) => { try { const userId = parseInt(req.params.id); const { displayName, color, role, permissions, password, unlockAccount, email } = req.body; const db = getDb(); // Benutzer prüfen const user = db.prepare('SELECT * FROM users WHERE id = ?').get(userId); if (!user) { return res.status(404).json({ error: 'Benutzer nicht gefunden' }); } // Verhindern, dass der einzige Admin seine Rolle ändert if (user.role === 'admin' && role !== 'admin') { const adminCount = db.prepare("SELECT COUNT(*) as count FROM users WHERE role = 'admin'").get(); if (adminCount.count <= 1) { return res.status(400).json({ error: 'Mindestens ein Admin muss existieren' }); } } // Update-Felder sammeln const updates = []; const params = []; if (displayName !== undefined) { updates.push('display_name = ?'); params.push(displayName); } if (color !== undefined) { updates.push('color = ?'); params.push(color); } if (role !== undefined) { updates.push('role = ?'); params.push(role); } if (permissions !== undefined) { updates.push('permissions = ?'); params.push(JSON.stringify(permissions)); } if (password) { if (password.length < 8) { return res.status(400).json({ error: 'Passwort muss mindestens 8 Zeichen haben' }); } const passwordHash = await bcrypt.hash(password, 12); updates.push('password_hash = ?'); params.push(passwordHash); } if (unlockAccount) { updates.push('failed_attempts = 0'); updates.push('locked_until = NULL'); } if (email !== undefined) { // E-Mail-Validierung const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { return res.status(400).json({ error: 'Ungültige E-Mail-Adresse' }); } // Prüfen ob E-Mail bereits von anderem Benutzer verwendet wird const existingEmail = db.prepare('SELECT id FROM users WHERE email = ? AND id != ?').get(email.toLowerCase(), userId); if (existingEmail) { return res.status(400).json({ error: 'E-Mail bereits vergeben' }); } updates.push('email = ?'); params.push(email.toLowerCase()); } if (updates.length === 0) { return res.status(400).json({ error: 'Keine Änderungen angegeben' }); } params.push(userId); db.prepare(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`).run(...params); logger.info(`Admin ${req.user.username} hat Benutzer ${user.username} bearbeitet`); // Aktualisierten Benutzer zurueckgeben const updatedUser = db.prepare(` SELECT id, username, display_name, color, role, permissions, email, created_at, last_login, failed_attempts, locked_until FROM users WHERE id = ? `).get(userId); res.json({ ...updatedUser, permissions: JSON.parse(updatedUser.permissions || '[]') }); } catch (error) { logger.error('Fehler beim Bearbeiten des Benutzers:', error); res.status(500).json({ error: 'Fehler beim Bearbeiten des Benutzers' }); } }); /** * DELETE /api/admin/users/:id - Benutzer löschen */ router.delete('/users/:id', (req, res) => { try { const userId = parseInt(req.params.id); const db = getDb(); // Benutzer prüfen const user = db.prepare('SELECT * FROM users WHERE id = ?').get(userId); if (!user) { return res.status(404).json({ error: 'Benutzer nicht gefunden' }); } // Verhindern, dass der letzte Admin gelöscht wird if (user.role === 'admin') { const adminCount = db.prepare("SELECT COUNT(*) as count FROM users WHERE role = 'admin'").get(); if (adminCount.count <= 1) { return res.status(400).json({ error: 'Der letzte Admin kann nicht gelöscht werden' }); } } // Verhindern, dass man sich selbst löscht if (userId === req.user.id) { return res.status(400).json({ error: 'Sie können sich nicht selbst löschen' }); } // Alle Referenzen auf den Benutzer behandeln // Tasks - assigned_to und created_by auf NULL setzen (erlaubt NULL) db.prepare('UPDATE tasks SET assigned_to = NULL WHERE assigned_to = ?').run(userId); db.prepare('UPDATE tasks SET created_by = NULL WHERE created_by = ?').run(userId); // Task-Assignees löschen (Mehrfachzuweisung) db.prepare('DELETE FROM task_assignees WHERE user_id = ?').run(userId); // Kommentare löschen (user_id NOT NULL) db.prepare('DELETE FROM comments WHERE user_id = ?').run(userId); // Historie löschen (user_id NOT NULL) db.prepare('DELETE FROM history WHERE user_id = ?').run(userId); // Vorschläge: Votes zuerst löschen, dann Vorschläge des Benutzers db.prepare('DELETE FROM proposal_votes WHERE user_id = ?').run(userId); db.prepare('DELETE FROM proposals WHERE created_by = ?').run(userId); // approved_by kann NULL sein db.prepare('UPDATE proposals SET approved_by = NULL WHERE approved_by = ?').run(userId); // Projekte - created_by auf NULL setzen (erlaubt NULL) db.prepare('UPDATE projects SET created_by = NULL WHERE created_by = ?').run(userId); // Anhänge - uploaded_by auf NULL setzen (erlaubt NULL) db.prepare('UPDATE attachments SET uploaded_by = NULL WHERE uploaded_by = ?').run(userId); // Links - created_by auf NULL setzen (erlaubt NULL) db.prepare('UPDATE links SET created_by = NULL WHERE created_by = ?').run(userId); // Applications - created_by auf NULL setzen (erlaubt NULL) db.prepare('UPDATE applications SET created_by = NULL WHERE created_by = ?').run(userId); // Login-Audit löschen db.prepare('DELETE FROM login_audit WHERE user_id = ?').run(userId); // Benachrichtigungen löschen (wird auch durch CASCADE gelöscht, aber sicherheitshalber) db.prepare('DELETE FROM notifications WHERE user_id = ?').run(userId); db.prepare('UPDATE notifications SET actor_id = NULL WHERE actor_id = ?').run(userId); // Benutzer löschen db.prepare('DELETE FROM users WHERE id = ?').run(userId); logger.info(`Admin ${req.user.username} hat Benutzer ${user.username} gelöscht`); res.json({ success: true }); } catch (error) { logger.error('Fehler beim Löschen des Benutzers:', error); res.status(500).json({ error: 'Fehler beim Löschen des Benutzers' }); } }); /** * GET /api/admin/upload-settings - Upload-Einstellungen abrufen */ router.get('/upload-settings', (req, res) => { try { const db = getDb(); const setting = db.prepare('SELECT value FROM settings WHERE key = ?').get('upload_settings'); if (setting) { const settings = JSON.parse(setting.value); // Migration: Altes Format (allowedTypes) auf neues Format (allowedExtensions) umstellen if (settings.allowedTypes && !settings.allowedExtensions) { // Altes Format erkannt - auf Standard-Einstellungen zurücksetzen logger.info('Migriere Upload-Einstellungen auf neues Format (allowedExtensions)'); db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)') .run('upload_settings', JSON.stringify(DEFAULT_UPLOAD_SETTINGS)); res.json(DEFAULT_UPLOAD_SETTINGS); return; } res.json(settings); } else { // Standard-Einstellungen zurückgeben und speichern db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)') .run('upload_settings', JSON.stringify(DEFAULT_UPLOAD_SETTINGS)); res.json(DEFAULT_UPLOAD_SETTINGS); } } catch (error) { logger.error('Fehler beim Abrufen der Upload-Einstellungen:', error); res.status(500).json({ error: 'Fehler beim Abrufen der Upload-Einstellungen' }); } }); /** * PUT /api/admin/upload-settings - Upload-Einstellungen speichern */ router.put('/upload-settings', (req, res) => { try { const { maxFileSizeMB, allowedExtensions } = req.body; // Validierung if (typeof maxFileSizeMB !== 'number' || maxFileSizeMB < 1 || maxFileSizeMB > 100) { return res.status(400).json({ error: 'Maximale Dateigröße muss zwischen 1 und 100 MB liegen' }); } if (!Array.isArray(allowedExtensions) || allowedExtensions.length === 0) { return res.status(400).json({ error: 'Mindestens eine Dateiendung muss erlaubt sein' }); } // Endungen validieren (nur alphanumerisch, 1-10 Zeichen) const validExtensions = allowedExtensions .map(ext => ext.toLowerCase().replace(/^\./, '')) // Punkt am Anfang entfernen .filter(ext => /^[a-z0-9]{1,10}$/.test(ext)); if (validExtensions.length === 0) { return res.status(400).json({ error: 'Keine gültigen Dateiendungen angegeben' }); } // Duplikate entfernen const uniqueExtensions = [...new Set(validExtensions)]; const settings = { maxFileSizeMB, allowedExtensions: uniqueExtensions }; const db = getDb(); db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)') .run('upload_settings', JSON.stringify(settings)); logger.info(`Admin ${req.user.username} hat Upload-Einstellungen geändert: ${uniqueExtensions.join(', ')}`); res.json(settings); } catch (error) { logger.error('Fehler beim Speichern der Upload-Einstellungen:', error); res.status(500).json({ error: 'Fehler beim Speichern der Upload-Einstellungen' }); } }); /** * Hilfsfunktion zum Abrufen der aktuellen Upload-Einstellungen */ function getUploadSettings() { try { const db = getDb(); const setting = db.prepare('SELECT value FROM settings WHERE key = ?').get('upload_settings'); if (setting) { const settings = JSON.parse(setting.value); // Bei altem Format oder fehlendem allowedExtensions: Standard verwenden if (!settings.allowedExtensions || !Array.isArray(settings.allowedExtensions)) { return DEFAULT_UPLOAD_SETTINGS; } return settings; } return DEFAULT_UPLOAD_SETTINGS; } catch (error) { logger.error('Fehler beim Abrufen der Upload-Einstellungen:', error); return DEFAULT_UPLOAD_SETTINGS; } } /** * POST /api/admin/backup/create - Sofortiges verschlüsseltes Backup erstellen */ router.post('/backup/create', (req, res) => { try { const backupPath = backup.createBackup(); if (backupPath) { logger.info(`Admin ${req.user.username} hat manuelles Backup erstellt`); res.json({ success: true, message: 'Verschlüsseltes Backup erfolgreich erstellt', backupPath: backupPath.split('/').pop() // Nur Dateiname zurückgeben }); } else { res.status(500).json({ error: 'Backup-Erstellung fehlgeschlagen' }); } } catch (error) { logger.error('Backup-Erstellung durch Admin fehlgeschlagen:', error); res.status(500).json({ error: 'Interner Fehler beim Erstellen des Backups' }); } }); /** * GET /api/admin/backup/list - Liste aller verschlüsselten Backups */ router.get('/backup/list', (req, res) => { try { const backups = backup.listBackups(); res.json(backups); } catch (error) { logger.error('Fehler beim Auflisten der Backups:', error); res.status(500).json({ error: 'Fehler beim Auflisten der Backups' }); } }); module.exports = router; module.exports.getUploadSettings = getUploadSettings; module.exports.DEFAULT_UPLOAD_SETTINGS = DEFAULT_UPLOAD_SETTINGS;