/** * 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'); /** * Standard-Upload-Einstellungen */ const DEFAULT_UPLOAD_SETTINGS = { maxFileSizeMB: 15, allowedTypes: { images: { enabled: true, types: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'] }, documents: { enabled: true, types: ['application/pdf'] }, office: { enabled: true, types: [ 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation' ] }, text: { enabled: true, types: ['text/plain', 'text/csv', 'text/markdown'] }, archives: { enabled: true, types: ['application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed'] }, data: { enabled: true, types: ['application/json'] } } }; // 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); 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, allowedTypes } = 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 (!allowedTypes || typeof allowedTypes !== 'object') { return res.status(400).json({ error: 'Ungültige Dateityp-Konfiguration' }); } const settings = { maxFileSizeMB, allowedTypes }; 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`); 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) { return JSON.parse(setting.value); } return DEFAULT_UPLOAD_SETTINGS; } catch (error) { logger.error('Fehler beim Abrufen der Upload-Einstellungen:', error); return DEFAULT_UPLOAD_SETTINGS; } } module.exports = router; module.exports.getUploadSettings = getUploadSettings; module.exports.DEFAULT_UPLOAD_SETTINGS = DEFAULT_UPLOAD_SETTINGS;