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>
552 lines
23 KiB
TypeScript
552 lines
23 KiB
TypeScript
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);
|
||
}
|
||
}
|