Initial commit
Dieser Commit ist enthalten in:
409
backend/routes/admin.js
Normale Datei
409
backend/routes/admin.js
Normale Datei
@ -0,0 +1,409 @@
|
||||
/**
|
||||
* 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 auf NULL setzen oder löschen
|
||||
// Tasks
|
||||
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);
|
||||
|
||||
// Kommentare
|
||||
db.prepare('UPDATE comments SET user_id = NULL WHERE user_id = ?').run(userId);
|
||||
|
||||
// Historie
|
||||
db.prepare('UPDATE history SET user_id = NULL WHERE user_id = ?').run(userId);
|
||||
|
||||
// Vorschläge
|
||||
db.prepare('UPDATE proposals SET created_by = NULL WHERE created_by = ?').run(userId);
|
||||
db.prepare('UPDATE proposals SET approved_by = NULL WHERE approved_by = ?').run(userId);
|
||||
|
||||
// Projekte
|
||||
db.prepare('UPDATE projects SET created_by = NULL WHERE created_by = ?').run(userId);
|
||||
|
||||
// Anhänge
|
||||
db.prepare('UPDATE attachments SET uploaded_by = NULL WHERE uploaded_by = ?').run(userId);
|
||||
|
||||
// Links
|
||||
db.prepare('UPDATE links SET created_by = NULL WHERE created_by = ?').run(userId);
|
||||
|
||||
// Login-Audit (kann gelöscht werden)
|
||||
db.prepare('DELETE FROM login_audit WHERE user_id = ?').run(userId);
|
||||
|
||||
// Votes des Benutzers löschen
|
||||
db.prepare('DELETE FROM proposal_votes WHERE user_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;
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren