Files
opencrm/backend/src/controllers/user.controller.ts
T
duffyduckandClaude Opus 5 fc6f39eba0 Aufsicht und Eingriff getrennt: neuer Haken "Audit-Betrieb"
Die DSGVO-Rolle trug audit:* komplett, also auch audit:admin. Ein
DSGVO-Beauftragter konnte damit seal-backlog, rehash und cleanup - seine
eigene Beweisgrundlage ersetzen. Wer das Protokoll beaufsichtigt, darf es
nicht umschreiben koennen. Dieselbe Klasse wie R184-02: falsche Domaene,
zu breit gebuendelt.

Der naheliegende Fix waere falsch gewesen. audit:admin einfach aus der
DSGVO-Rolle zu streichen haette es heimatlos gemacht: Die Admin-Rolle ist
ausdruecklich ohne audit/gdpr gebaut, einzige verbleibende Quelle waere
der Entwicklerzugriff - der alles gibt. Prod versiegeln haette dann
Vollzugriff vorausgesetzt.

Deshalb eine eigene versteckte Rolle "Audit-Betrieb" (audit:read +
audit:admin), zugewiesen ueber eine Checkbox wie DSGVO/Entwickler. DSGVO
behaelt audit:read + audit:export + gdpr:*. Fuer kein bestehendes Konto
weitet sich etwas aus; es wird enger, und wer eingreifen koennen soll,
bekommt es ausdruecklich.

Keine zusaetzliche Rechte-Huerde davor, weil das am Henne-Ei-Problem
scheitert: Nach der Aufteilung haelt zunaechst niemand audit:admin,
koennte ihn also auch niemand vergeben. Stattdessen wird die Vergabe
laut - CRITICAL im Protokoll und PERMISSION_CHANGED/CRITICAL im
Alarmkanal, samt Kennzeichen, ob sich jemand den Haken selbst gesetzt hat.

Nebenbei geschlossen: setUserGdprAccess() legte die DSGVO-Rolle im
Notfallpfad mit audit:* komplett an - eine zweite Liste, die dasselbe
bedeuten sollte und die Buendelung stillschweigend zurueckgebracht haette.

ACHTUNG beim Deploy: Bestehende DSGVO-Konten verlieren audit:admin. Wer
Prod versiegeln will, muss sich vorher "Audit-Betrieb" ankreuzen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 17:16:27 +02:00

552 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import prisma from '../lib/prisma.js';
import * as userService from '../services/user.service.js';
import { logChange } from '../services/audit.service.js';
import { AUDIT_OPS_ROLLE } from '../services/user.service.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
import { emit as emitSecurityEvent, contextFromRequest } from '../services/securityMonitor.service.js';
import { pickUserCreate, pickUserUpdate, isValidEmail, sanitizePhoneField } from '../utils/sanitize.js';
import { validatePasswordComplexity, STAFF_MIN_PASSWORD_LENGTH } from '../utils/passwordGenerator.js';
// Users
export async function getUsers(req: Request, res: Response): Promise<void> {
try {
const { search, isActive, roleId, page, limit } = req.query;
const result = await userService.getAllUsers({
search: search as string,
isActive: isActive !== undefined ? isActive === 'true' : undefined,
roleId: roleId ? parseInt(roleId as string) : undefined,
page: page ? parseInt(page as string) : undefined,
limit: limit ? parseInt(limit as string) : undefined,
});
res.json({
success: true,
data: result.users,
pagination: result.pagination,
} as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: 'Fehler beim Laden der Benutzer',
} as ApiResponse);
}
}
export async function getUser(req: Request, res: Response): Promise<void> {
try {
const user = await userService.getUserById(parseInt(req.params.id));
if (!user) {
res.status(404).json({
success: false,
error: 'Benutzer nicht gefunden',
} as ApiResponse);
return;
}
res.json({ success: true, data: user } as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: 'Fehler beim Laden des Benutzers',
} as ApiResponse);
}
}
export async function createUser(req: Request, res: Response): Promise<void> {
try {
// Whitelist: nur erlaubte Felder aus req.body übernehmen (Mass-Assignment-Schutz)
const data = pickUserCreate(req.body) as any;
// Email-Format prüfen, sonst landet "x@y\nBcc:..." in der DB
// (Pentest 29.4 SMTP-Header-Injection).
if (!isValidEmail(data?.email) || !data?.email) {
res.status(400).json({ success: false, error: 'Ungültiges E-Mail-Format' } as ApiResponse);
return;
}
if (data?.password) {
const c = validatePasswordComplexity(data.password, { minLength: STAFF_MIN_PASSWORD_LENGTH });
if (!c.ok) {
res.status(400).json({
success: false,
error: 'Passwort erfüllt Mindestanforderungen nicht: ' + c.errors.join(', '),
} as ApiResponse);
return;
}
}
// 60.3: WhatsApp- und Signal-Nummern gegen CRLF/Header-Injection sichern.
try {
if ('whatsappNumber' in data) {
const cleaned = sanitizePhoneField(data.whatsappNumber, 'WhatsApp-Nummer');
data.whatsappNumber = cleaned ?? null;
}
if ('signalNumber' in data) {
const cleaned = sanitizePhoneField(data.signalNumber, 'Signal-Nummer');
data.signalNumber = cleaned ?? null;
}
} catch (err) {
res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Ungültige Nummer' } as ApiResponse);
return;
}
const user = await userService.createUser(data);
await logChange({
req, action: 'CREATE', resourceType: 'User',
resourceId: user.id.toString(),
label: `Benutzer ${user.firstName} ${user.lastName} angelegt`,
});
res.status(201).json({ success: true, data: user } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Benutzers',
} as ApiResponse);
}
}
export async function updateUser(req: AuthRequest, res: Response): Promise<void> {
try {
const userId = parseInt(req.params.id);
// `permissions` und `password` darf der generische Update nicht
// entgegennehmen. Vorher landeten sie auf dem Floor (Whitelist-Drop),
// der Caller bekam aber 200 zurück und glaubte fälschlich, die Werte
// wären übernommen worden. Stattdessen sofort 400, damit Tooling /
// Client den Fehler sieht. (Pentest 2026-05-20)
// - permissions kommen aus Rollen (PUT roleIds bzw. die DSGVO-/
// Developer-Checkboxen) und können nicht direkt am User hängen.
// - password wird über POST /users/:id/password gesetzt
// (eigene Komplexitäts-Validierung + Audit-Trail).
const body = req.body || {};
const forbidden = ['permissions', 'password', 'passwordHash'];
const offending = forbidden.filter((k) => k in body);
if (offending.length > 0) {
res.status(400).json({
success: false,
error: `Felder nicht erlaubt: ${offending.join(', ')}. ` +
(offending.includes('permissions')
? 'Permissions werden über roleIds / hasGdprAccess / hasDeveloperAccess gesteuert. '
: '') +
(offending.includes('password') || offending.includes('passwordHash')
? `Passwort über POST /users/${userId}/password setzen.`
: ''),
} as ApiResponse);
return;
}
// Whitelist: nur erlaubte Felder aus req.body übernehmen (Mass-Assignment-Schutz)
const data = pickUserUpdate(req.body) as Record<string, unknown>;
// Email-Validierung gegen SMTP-Header-Injection (Pentest 29.4).
// null/leer ist OK (Email darf optional sein), nur falsches Format prüfen.
if (data?.email !== undefined && !isValidEmail(data.email)) {
res.status(400).json({ success: false, error: 'Ungültiges E-Mail-Format' } as ApiResponse);
return;
}
// 60.3: WhatsApp- und Signal-Nummern gegen CRLF/Header-Injection sichern.
try {
if ('whatsappNumber' in data) {
const cleaned = sanitizePhoneField(data.whatsappNumber, 'WhatsApp-Nummer');
data.whatsappNumber = cleaned ?? null;
}
if ('signalNumber' in data) {
const cleaned = sanitizePhoneField(data.signalNumber, 'Signal-Nummer');
data.signalNumber = cleaned ?? null;
}
} catch (err) {
res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Ungültige Nummer' } as ApiResponse);
return;
}
// Vorherigen Stand laden für Audit inkl. Rollen, damit hasGdprAccess /
// hasDeveloperAccess (versteckte Rollen) korrekt verglichen werden.
const beforeUser = await prisma.user.findUnique({
where: { id: userId },
include: { roles: { include: { role: true } } },
});
const before = beforeUser
? {
...beforeUser,
hasGdprAccess: beforeUser.roles.some((ur) => ur.role.name === 'DSGVO'),
hasDeveloperAccess: beforeUser.roles.some((ur) => ur.role.name === 'Developer'),
hasAuditOpsAccess: beforeUser.roles.some((ur) => ur.role.name === AUDIT_OPS_ROLLE),
}
: null;
// Der Audit-Betrieb-Haken vergibt die EINGREIFENDEN Rechte am Protokoll
// (versiegeln, neu berechnen, aufraeumen, Aufbewahrung). Ihn zu setzen ist
// kein gewoehnliches Benutzer-Update: Er entscheidet, wer die
// Beweisgrundlage ersetzen darf.
//
// Eine eigene Rechte-Huerde davorzusetzen scheitert am Henne-Ei-Problem -
// nach der Aufteilung haelt zunaechst niemand `audit:admin`, und dann
// koennte ihn auch niemand vergeben. Stattdessen wird die Vergabe LAUT:
// CRITICAL im Protokoll und zusaetzlich in den Alarmkanal, so wie beim
// Dienstkonto-Kennzeichen (R184-01).
const setztAuditBetrieb =
data.hasAuditOpsAccess !== undefined &&
before !== null &&
data.hasAuditOpsAccess !== (before as any).hasAuditOpsAccess;
const aktiviertAuditBetrieb = data.hasAuditOpsAccess === true;
// Das Dienstkonto-Kennzeichen SENKT die Alarmstufe der Anmeldungen dieses
// Kontos (Pentest R184). Damit ist es selbst ein Hebel zur Waesche: Wer sein
// Konto so markiert, laesst die eigenen auffaelligen Anmeldungen als
// Routine erscheinen. Es bekommt deshalb dieselbe Behandlung wie das
// Absenken einer Aufbewahrungsfrist Bestaetigung, laute Protokollierung,
// und kein Selbstbedienen.
const setztDienstkonto =
typeof (data as any).isServiceAccount === 'boolean' &&
(data as any).isServiceAccount !== before?.isServiceAccount;
const aktiviertDienstkonto = setztDienstkonto && (data as any).isServiceAccount === true;
// R184-02: Das Kennzeichen ist ein Audit-Governance-Eingriff, kein
// gewoehnliches Benutzerfeld es ist Geschwister von retention-shorten,
// seal-backlog, rehash und cleanup, die alle `audit:admin` verlangen. Es
// haengt aber an der Route mit `users:update`. Heute deckungsgleich, weil
// nur die Administratorrolle beides hat aber jede kuenftige Rolle mit
// „Benutzer bearbeiten“ wuerde still „Login-Alarme herunterstufen“ erben.
// Deshalb hier zusaetzlich auf die Domaene pruefen, in der der Schaden
// liegt.
if (setztDienstkonto && !(req.user?.permissions || []).includes('audit:admin')) {
res.status(403).json({
success: false,
error:
'Das Dienstkonto-Kennzeichen ändert, wie Anmeldungen im Audit-Log bewertet werden. ' +
'Dafür genügt „Benutzer bearbeiten“ nicht es erfordert die Berechtigung ' +
'audit:admin, wie das Verkürzen von Aufbewahrungsfristen auch.',
} as ApiResponse);
return;
}
if (setztDienstkonto && req.user?.userId === userId) {
res.status(403).json({
success: false,
error:
'Das Dienstkonto-Kennzeichen lässt sich nicht am eigenen Konto setzen oder entfernen. ' +
'Es stuft Anmeldungen dieses Kontos als Routine ein wer das für sich selbst täte, ' +
'könnte die eigenen Anmeldungen unauffällig machen. Bitte von einem anderen ' +
'Administrator vornehmen lassen.',
} as ApiResponse);
return;
}
// Gate in BEIDE Richtungen (Pentest R184-01). Das Entfernen war ungegatet
// und es ist der gefaehrlichere Weg: Ein un-geflaggtes Konto faellt aus der
// Heartbeat-Wache, weil diese am Live-Kennzeichen haengt. Genau das
// „stilllegen und auf Stille setzen“, gegen das der Wachhund gebaut wurde.
if (setztDienstkonto && req.body?.confirm !== 'SERVICE_ACCOUNT') {
res.status(400).json({
success: false,
error: aktiviertDienstkonto
? `Damit werden künftige Anmeldungen von ${before?.email ?? 'diesem Konto'} im ` +
'Audit-Log als Routine geführt statt als kritisches Ereignis. Das ist für ' +
'planmäßig arbeitende Dienste gedacht (etwa das Gegenbuch) für ein Konto, das ' +
'ein Mensch benutzt, wäre es eine Tarnung. ' +
'Zum Bestätigen {"confirm":"SERVICE_ACCOUNT"} mitsenden.'
: `Damit fällt ${before?.email ?? 'dieses Konto'} aus der Überwachung heraus: Es wird ` +
'dann nicht mehr gemeldet, wenn es sich nicht mehr anmeldet. Bei einem laufenden ' +
'Dienst wie dem Gegenbuch hieße das, sein Ausfall bliebe unbemerkt. ' +
'Zum Bestätigen {"confirm":"SERVICE_ACCOUNT"} mitsenden.',
} as ApiResponse);
return;
}
const user = await userService.updateUser(userId, data as any);
if (user) {
// Audit: Geänderte Felder ermitteln und loggen
if (before) {
const changes: Record<string, { von: unknown; nach: unknown }> = {};
const fieldLabels: Record<string, string> = {
email: 'E-Mail', firstName: 'Vorname', lastName: 'Nachname', isActive: 'Aktiv',
hasGdprAccess: 'DSGVO-Zugriff', hasDeveloperAccess: 'Entwicklerzugriff',
hasAuditOpsAccess: 'Audit-Betrieb (versiegeln, aufräumen, Aufbewahrung)',
isServiceAccount: 'Dienstkonto (Anmeldungen als Routine)',
};
for (const [key, newVal] of Object.entries(data)) {
if (['id', 'createdAt', 'updatedAt'].includes(key)) continue;
const oldVal = (before as any)[key];
const norm = (v: unknown) => (v === null || v === undefined || v === '' ? null : v);
if (JSON.stringify(norm(oldVal)) !== JSON.stringify(norm(newVal))) {
const label = fieldLabels[key] || key;
const formatVal = (v: unknown) => {
if (v === null || v === undefined || v === '') return '-';
if (typeof v === 'boolean') return v ? 'Ja' : 'Nein';
return String(v);
};
changes[label] = { von: formatVal(oldVal), nach: formatVal(newVal) };
}
}
const changeList = Object.entries(changes).map(([f, c]) => `${f}: ${c.von}${c.nach}`).join(', ');
await logChange({
req, action: 'UPDATE', resourceType: 'User',
resourceId: user.id.toString(),
label: setztDienstkonto
? `Dienstkonto-Kennzeichen ${aktiviertDienstkonto ? 'GESETZT' : 'entfernt'} für ` +
`${user.email} Anmeldungen werden künftig ` +
`${aktiviertDienstkonto ? 'als Routine' : 'wieder als kritisch'} geführt`
: changeList
? `Benutzer ${user.firstName} ${user.lastName} aktualisiert: ${changeList}`
: `Benutzer ${user.firstName} ${user.lastName} aktualisiert`,
// Die Aenderung dieses Kennzeichens wird wie ihre Wirkung eingestuft:
// Sie beeinflusst, wie kuenftige Anmeldungen bewertet werden.
sensitivity: setztDienstkonto || setztAuditBetrieb ? 'CRITICAL' : undefined,
details: Object.keys(changes).length > 0 ? changes : undefined,
});
// Zusaetzlich in den Alarmkanal (Pentest R184-01). Eine CRITICAL-Zeile
// im Audit-Log muss jemand LESEN das ist die R183-02-Klasse. Das
// Entfernen des Kennzeichens nimmt das Konto aus der Heartbeat-Wache;
// ab dann faellt sein Ausbleiben nicht mehr auf. Diese Aenderung
// gehoert deshalb dorthin, wo automatisch reagiert wird.
if (setztDienstkonto) {
const ctx = contextFromRequest(req);
emitSecurityEvent({
type: 'PERMISSION_CHANGED',
severity: 'CRITICAL',
message: aktiviertDienstkonto
? `Konto ${user.email} als Dienstkonto markiert seine Anmeldungen gelten ab ` +
'jetzt als Routine statt als kritisches Ereignis.'
: `Konto ${user.email} ist KEIN Dienstkonto mehr es fällt damit aus der ` +
'Überwachung heraus: Ein Ausbleiben seiner Anmeldungen wird nicht mehr gemeldet.',
ipAddress: ctx.ipAddress,
userId: req.user?.userId,
userEmail: req.user?.email,
endpoint: ctx.endpoint,
details: { betroffenesKonto: user.email, aktiviert: aktiviertDienstkonto },
});
}
if (setztAuditBetrieb) {
const ctx = contextFromRequest(req);
emitSecurityEvent({
type: 'PERMISSION_CHANGED',
severity: 'CRITICAL',
message: aktiviertAuditBetrieb
? `Konto ${user.email} darf ab jetzt am Audit-Protokoll EINGREIFEN: versiegeln, ` +
'neu berechnen, aufräumen, Aufbewahrung ändern. Damit kann es die Grundlage ' +
'ersetzen, gegen die Manipulation nachgewiesen wird.'
: `Konto ${user.email} darf nicht mehr am Audit-Protokoll eingreifen.`,
ipAddress: ctx.ipAddress,
userId: req.user?.userId,
userEmail: req.user?.email,
endpoint: ctx.endpoint,
details: {
betroffenesKonto: user.email,
aktiviert: aktiviertAuditBetrieb,
selbstvergabe: user.id === req.user?.userId,
},
});
}
} else {
await logChange({
req, action: 'UPDATE', resourceType: 'User',
resourceId: user.id.toString(),
label: `Benutzer ${user.firstName} ${user.lastName} aktualisiert`,
});
}
}
res.json({ success: true, data: user } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Benutzers',
} as ApiResponse);
}
}
// Admin setzt das Passwort eines anderen Users zurück. Separat vom
// generischen Update damit der Vorgang explizit auditiert wird und nicht
// versehentlich über Mass-Assignment passieren kann.
// Pentest Runde 12 (2026-05-18) MITTEL.
export async function setUserPassword(req: Request, res: Response): Promise<void> {
try {
const userId = parseInt(req.params.id);
const { password, currentPassword } = req.body || {};
if (!password || typeof password !== 'string') {
res.status(400).json({ success: false, error: 'Passwort erforderlich' } as ApiResponse);
return;
}
// Pentest 47.3 (MEDIUM, 2026-06-01): Re-Auth verpflichtend.
// Ein gestohlener Admin-JWT reichte bisher, um Staff-Passwörter
// umzuschreiben. Jetzt muss der aufrufende Admin sein eigenes
// Passwort mitsenden CSRF/Token-Klau allein reicht nicht mehr.
const authReq = req as AuthRequest;
const callerId = authReq.user?.userId;
if (!callerId) {
res.status(401).json({ success: false, error: 'Nicht authentifiziert' } as ApiResponse);
return;
}
if (!currentPassword || typeof currentPassword !== 'string') {
res.status(400).json({
success: false,
error: 'Bitte das eigene Passwort zur Bestätigung mitsenden.',
} as ApiResponse);
return;
}
const caller = await prisma.user.findUnique({
where: { id: callerId },
select: { password: true },
});
// Timing-Schutz: immer einen bcrypt.compare laufen lassen
const callerHash = caller?.password ?? '$2a$10$0000000000000000000000000000000000000000000000000000';
const reAuthOk = await bcrypt.compare(currentPassword, callerHash);
if (!caller || !reAuthOk) {
res.status(403).json({
success: false,
error: 'Eigenes Passwort ist falsch.',
} as ApiResponse);
return;
}
const c = validatePasswordComplexity(password, { minLength: STAFF_MIN_PASSWORD_LENGTH });
if (!c.ok) {
res.status(400).json({
success: false,
error: 'Passwort erfüllt Mindestanforderungen nicht: ' + c.errors.join(', '),
} as ApiResponse);
return;
}
const user = await userService.updateUser(userId, { password } as any);
if (!user) {
res.status(404).json({ success: false, error: 'Benutzer nicht gefunden' } as ApiResponse);
return;
}
// Pentest 48.4 (INFO, 2026-06-01): Bestehende JWTs für den Ziel-User
// sofort invalidieren. Greift insbesondere bei Self-Reset (Admin setzt
// sich selbst zurück) der gestohlene Token wird damit ungültig,
// statt noch bis zum natürlichen Ablauf brauchbar zu bleiben.
// Die Middleware-Auth liest tokenInvalidatedAt und vergleicht gegen
// den `iat`-Claim des JWT.
await prisma.user.update({
where: { id: user.id },
data: { tokenInvalidatedAt: new Date() },
});
await logChange({
req, action: 'UPDATE', resourceType: 'User',
resourceId: user.id.toString(),
label: `Passwort für Benutzer ${user.firstName} ${user.lastName} (${user.email}) durch Admin gesetzt (Re-Auth bestätigt, Sessions invalidiert)`,
});
res.json({ success: true, message: 'Passwort gesetzt' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Setzen des Passworts',
} as ApiResponse);
}
}
export async function deleteUser(req: Request, res: Response): Promise<void> {
try {
const userId = parseInt(req.params.id);
const userBefore = await userService.getUserById(userId);
await userService.deleteUser(userId);
await logChange({
req, action: 'DELETE', resourceType: 'User',
resourceId: userId.toString(),
label: `Benutzer ${userBefore?.firstName || ''} ${userBefore?.lastName || ''} gelöscht`,
});
res.json({ success: true, message: 'Benutzer gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Benutzers',
} as ApiResponse);
}
}
// Roles
export async function getRoles(req: Request, res: Response): Promise<void> {
try {
const roles = await userService.getAllRoles();
res.json({ success: true, data: roles } as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: 'Fehler beim Laden der Rollen',
} as ApiResponse);
}
}
export async function getRole(req: Request, res: Response): Promise<void> {
try {
const role = await userService.getRoleById(parseInt(req.params.id));
if (!role) {
res.status(404).json({
success: false,
error: 'Rolle nicht gefunden',
} as ApiResponse);
return;
}
res.json({ success: true, data: role } as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: 'Fehler beim Laden der Rolle',
} as ApiResponse);
}
}
export async function createRole(req: Request, res: Response): Promise<void> {
try {
const role = await userService.createRole(req.body);
await logChange({
req, action: 'CREATE', resourceType: 'Role',
resourceId: role.id.toString(),
label: `Rolle ${role.name} angelegt`,
});
res.status(201).json({ success: true, data: role } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Rolle',
} as ApiResponse);
}
}
export async function updateRole(req: Request, res: Response): Promise<void> {
try {
const role = await userService.updateRole(parseInt(req.params.id), req.body);
if (role) {
await logChange({
req, action: 'UPDATE', resourceType: 'Role',
resourceId: role.id.toString(),
label: `Rolle ${role.name} aktualisiert`,
});
}
res.json({ success: true, data: role } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Rolle',
} as ApiResponse);
}
}
export async function deleteRole(req: Request, res: Response): Promise<void> {
try {
const roleId = parseInt(req.params.id);
const role = await userService.getRoleById(roleId);
await userService.deleteRole(roleId);
await logChange({
req, action: 'DELETE', resourceType: 'Role',
resourceId: roleId.toString(),
label: `Rolle ${role?.name || roleId} gelöscht`,
});
res.json({ success: true, message: 'Rolle gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Rolle',
} as ApiResponse);
}
}
// Permissions
export async function getPermissions(req: Request, res: Response): Promise<void> {
try {
const permissions = await userService.getAllPermissions();
res.json({ success: true, data: permissions } as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: 'Fehler beim Laden der Berechtigungen',
} as ApiResponse);
}
}