Files
TaskMate/backend/routes/admin.js
hendrik_gebhardt@gmx.de 623bbdf5dd Gitea-Repo fix
2026-01-04 21:21:11 +00:00

471 Zeilen
16 KiB
JavaScript

/**
* 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, email, initials, display_name, color, role, permissions,
created_at, last_login, failed_attempts, locked_until, custom_initials
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 { initials, password, displayName, email, role, permissions } = req.body;
// Validierung
if (!initials || !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 initialsUpper = initials.toUpperCase();
if (!/^[A-Z]{2}$/.test(initialsUpper)) {
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 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' });
}
// Prüfen ob Kürzel bereits existiert
const existingInitials = db.prepare('SELECT id FROM users WHERE initials = ?').get(initialsUpper);
if (existingInitials) {
return res.status(400).json({ error: 'Kürzel 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 (email, initials, password_hash, display_name, color, role, permissions, custom_initials)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
email.toLowerCase(),
initialsUpper,
passwordHash,
displayName,
defaultColor,
role || 'user',
JSON.stringify(permissions || []),
initialsUpper
);
logger.info(`Admin ${req.user.email} hat Benutzer ${initialsUpper} erstellt`);
res.status(201).json({
id: result.lastInsertRowid,
initials: initialsUpper,
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, initials } = 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 (initials !== undefined) {
// Validierung: Genau 2 Buchstaben
const initialsUpper = initials.toUpperCase();
if (!/^[A-Z]{2}$/.test(initialsUpper)) {
return res.status(400).json({ error: 'Kürzel muss genau 2 Buchstaben sein (z.B. HG)' });
}
// Prüfen ob Kürzel bereits von anderem Benutzer verwendet wird
const existingInitials = db.prepare('SELECT id FROM users WHERE initials = ? AND id != ?').get(initialsUpper, userId);
if (existingInitials) {
return res.status(400).json({ error: 'Kürzel bereits vergeben' });
}
updates.push('initials = ?');
params.push(initialsUpper);
// Auch custom_initials aktualisieren für Kompatibilität
updates.push('custom_initials = ?');
params.push(initialsUpper);
}
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, custom_initials
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;