Aufraeumrunde vor Etappe 2: Fehlerhygiene, Admin-Kriterium, Portal-Riegel

Drei Nachrangpunkte aus dem Pentest, vor dem Schneiden des Rechtekatalogs.

1. R192-01 projektweit. Das Muster `error instanceof Error ? error.message`
stand 124-mal in 26 Controllern und konnte ueberall Serverpfade,
Spaltennamen und Bibliotheksinterna ausliefern. Zentral geloest statt
124-mal einzeln - das waere die Falle aus R186-01 und R188 gewesen:
utils/fehlerAntwort.ts mit antworteAufFehler(), jetzt 123 Aufrufe in 26
Dateien und eine Regel.

Die Unterscheidung laeuft ueber die Fehlerklasse. Neue Basisklasse
FachlicherFehler fuer alles, dessen Wortlaut fuer den Aufrufer bestimmt
ist; ApiError, RechteEskalationError, RollenSperrError,
UngueltigeEingabeError, FilterFehler und ReferralError stammen davon ab.
Ein blankes Error gilt weiter als absichtlich. Alles andere - TypeError,
Prisma, JWT-Bibliothek - wird 500 mit allgemeiner Auskunft, Einzelheiten
ins Protokoll.

Mit gefunden: Sechs Stellen in cachedEmail.controller interpolierten die
interne Meldung in den Antworttext; das haette kein Filter erwischt, der
nur das Feld ersetzt. Und POST /auth/refresh gab den Wortlaut der
JWT-Bibliothek zurueck ("jwt malformed", "invalid signature") - der sagt
einem Angreifer, woran sein Token gescheitert ist.

2. Admin-Heuristik. Bisher galt "wer users:delete hat, ist Admin" - ein
Zufallsmerkmal. Bewusst NICHT auf den Rollennamen umgestellt, wie
vorgeschlagen: Eine selbst gebaute Rolle mit users:update verwaltet
tatsaechlich, ein Namenskriterium wuerde sie uebersehen, und dann liesse
sich der letzte Admin loeschen, obwohl die Faehigkeit erhalten bliebe.
Geschuetzt wird jetzt die Faehigkeit selbst: users:update und
roles:manage. Wer der letzte Traeger ist, kann sie nicht verlieren - durch
Rollenwechsel, Deaktivierung oder Loeschung. Die Meldung nennt die
Faehigkeit beim Namen. Nebenbei der letzte Cost-10-Rest im
Kennwort-Zuruecksetzen.

3. Portal-Kunden. Korrektur meiner eigenen Einordnung: Das war kein
Migrationsrueckstand, sondern eine gewollte Trennung. Kunden bekommen
niemals operative Rechte; die Portalansicht ist dafuer nicht gebaut und
prueft es nicht. Die zwei Kopien des festen Arrays sind jetzt eine
Konstante PORTAL_RECHTE mit einem Kommentar, der die Absicht benennt.
Dazu ein harter Riegel im Gate: requirePermission schneidet die Rechte
eines Portal-Zugangs auf PORTAL_RECHTE zu, unabhaengig davon, was sein
Token behauptet. Heute wirkungslos, morgen die Sicherung - bisher haette
eine unbedachte Zeile in der Token-Erzeugung gereicht. Und eine Startwache,
die jedes Nicht-Lese-Recht in dieser Liste meldet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 14:23:50 +02:00
co-authored by Claude Opus 5
parent f964610af7
commit 0ef28c7a87
37 changed files with 513 additions and 670 deletions
+50
View File
@@ -291,3 +291,53 @@ export function istSystemrollenName(name: string): boolean {
const normalisiert = name.trim().toLocaleLowerCase('de-DE');
return SYSTEMROLLEN_NAMEN.some((n) => n.toLocaleLowerCase('de-DE') === normalisiert);
}
// ---------------------------------------------------------------------------
// Kundenportal - bewusst NICHT Teil des Rollensystems
// ---------------------------------------------------------------------------
/**
* Die Rechte eines angemeldeten Portal-Kunden. Fest, kurz, abschliessend.
*
* DIESE TRENNUNG IST ABSICHT UND SOLL BLEIBEN.
*
* Kunden im Portal ziehen ihre Rechte NICHT aus Rollen, sondern aus dieser
* Liste. Das ist kein Altbestand, den man noch migrieren muesste - es ist
* die Sicherung dagegen, dass eine Aenderung am Rollenmodell versehentlich
* auf die Kundenseite durchschlaegt. Ein Kunde, der ueber eine Rolle
* ploetzlich `contracts:create` haelt, koennte in fremdem Namen Vertraege
* anlegen; die Portalansicht ist fuer so etwas nicht gebaut und pruefte es
* nicht.
*
* Wer hier etwas hinzufuegen will, aendert nicht eine Liste, sondern eine
* Vertrauensgrenze. `pruefePortalRechte()` weist beim Start alles ab, was
* nicht Lesen ist - absichtlich als Alarm und nicht als stiller Default.
*
* Die Durchsetzung liegt zusaetzlich im Gate (`requirePermission`): Ein
* Portal-Zugang bekommt dort nur, was auch hier steht, unabhaengig davon,
* was in seinem Token behauptet wird.
*/
export const PORTAL_RECHTE: readonly string[] = [
'contracts:read', // eigene Vertraege lesen
'customers:read', // eigene Kundendaten lesen
] as const;
const PORTAL_ERLAUBTE_AKTIONEN = ['read'];
/**
* Wacht darueber, dass die Portal-Liste eine Leseliste bleibt.
*
* Wird beim Start aufgerufen. Ein schreibendes Recht hier waere kein
* Schoenheitsfehler, sondern eine stille Ausweitung der Kundenrechte - und
* genau die soll nicht durch eine unbedachte Zeile passieren koennen.
*/
export function pruefePortalRechte(): string[] {
const beanstandet: string[] = [];
for (const recht of PORTAL_RECHTE) {
const [, aktion] = recht.split(':');
if (!PORTAL_ERLAUBTE_AKTIONEN.includes(aktion)) {
beanstandet.push(recht);
}
}
return beanstandet;
}
@@ -1,4 +1,5 @@
import { Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import prisma from '../lib/prisma.js';
import * as appSettingService from '../services/appSetting.service.js';
import { logChange } from '../services/audit.service.js';
@@ -81,10 +82,7 @@ export async function updateSetting(req: AuthRequest, res: Response): Promise<vo
});
res.json({ success: true, message: 'Einstellung gespeichert' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Speichern der Einstellung',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Speichern der Einstellung');
}
}
@@ -146,9 +144,6 @@ export async function updateSettings(req: AuthRequest, res: Response): Promise<v
res.json({ success: true, message: 'Einstellungen gespeichert' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Speichern der Einstellungen',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Speichern der Einstellungen');
}
}
@@ -1,4 +1,6 @@
import { Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import { FachlicherFehler } from '../utils/apiError.js';
import { AuthRequest } from '../types/index.js';
import * as auditService from '../services/audit.service.js';
import { logChange } from '../services/audit.service.js';
@@ -18,7 +20,11 @@ import { emit as emitSecurityEvent, contextFromRequest } from '../services/secur
* sprengt die Query erst in der Datenbank) und fuer Zahlen (`parseInt('x')`
* ergibt NaN).
*/
class FilterFehler extends Error {}
class FilterFehler extends FachlicherFehler {
constructor(nachricht: string) {
super(nachricht, 400);
}
}
const AUDIT_ACTIONS = Object.values(AuditAction) as string[];
const AUDIT_SENSITIVITIES = Object.values(AuditSensitivity) as string[];
@@ -537,10 +543,7 @@ export async function sealBacklog(req: AuthRequest, res: Response) {
});
} catch (error) {
console.error('Fehler beim Versiegeln des Altbestands:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Versiegeln',
});
antworteAufFehler(res, error, 'Fehler beim Versiegeln');
}
}
+9 -12
View File
@@ -1,4 +1,5 @@
import { Request, Response, CookieOptions } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import * as authService from '../services/auth.service.js';
import { AuthRequest, ApiResponse } from '../types/index.js';
import prisma from '../lib/prisma.js';
@@ -281,10 +282,7 @@ export async function confirmPasswordReset(req: Request, res: Response): Promise
ipAddress: ctx.ipAddress,
endpoint: ctx.endpoint,
});
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Passwort-Reset fehlgeschlagen',
} as ApiResponse);
antworteAufFehler(res, error, 'Passwort-Reset fehlgeschlagen');
}
}
@@ -400,10 +398,12 @@ export async function refresh(req: Request, res: Response): Promise<void> {
endpoint: ctx.endpoint,
});
}
res.status(401).json({
success: false,
error: msg,
} as ApiResponse);
// `msg` geht weiterhin in den Alarmkanal (oben), aber nicht mehr an den
// Aufrufer: Bei einem abgelehnten Refresh stammt der Wortlaut aus der
// JWT-Bibliothek ("jwt malformed", "invalid signature") und sagt einem
// Angreifer, WORAN sein Token gescheitert ist. Fuer den berechtigten
// Nutzer aendert das nichts - er muss sich so oder so neu anmelden.
antworteAufFehler(res, error, 'Refresh fehlgeschlagen', 401);
}
}
@@ -525,9 +525,6 @@ export async function changeInitialPortalPassword(req: AuthRequest, res: Respons
clearRefreshCookie(res);
res.json({ success: true, message: 'Passwort geändert' } as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Passwort konnte nicht geändert werden',
} as ApiResponse);
antworteAufFehler(res, error, 'Passwort konnte nicht geändert werden', 500);
}
}
@@ -1,4 +1,5 @@
import { Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import { isValidIBAN } from 'ibantools';
import * as blzData from '../services/blzData.service.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
@@ -47,9 +48,6 @@ export async function lookupIban(req: AuthRequest, res: Response): Promise<void>
},
} as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim IBAN-Nachschlagen',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim IBAN-Nachschlagen', 500);
}
}
@@ -1,4 +1,5 @@
import { Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import { AuthRequest } from '../types/index.js';
import * as birthdayService from '../services/birthday.service.js';
import { sendEmail, SmtpCredentials } from '../services/smtpService.js';
@@ -89,10 +90,7 @@ export async function resetBirthdayGreeting(req: AuthRequest, res: Response) {
res.json({ success: true });
} catch (error) {
console.error('Fehler beim Zurücksetzen:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Zurücksetzen',
});
antworteAufFehler(res, error, 'Fehler beim Zurücksetzen', 500);
}
}
@@ -180,9 +178,6 @@ export async function sendBirthdayGreeting(req: AuthRequest, res: Response) {
});
} catch (error) {
console.error('Fehler beim Senden des Geburtstagsgrußes:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Senden',
});
antworteAufFehler(res, error, 'Fehler beim Senden', 500);
}
}
@@ -1,4 +1,5 @@
import { Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
import { logChange } from '../services/audit.service.js';
import * as blzData from '../services/blzData.service.js';
@@ -9,10 +10,7 @@ export async function getStatus(req: AuthRequest, res: Response): Promise<void>
const status = await blzData.getStatus();
res.json({ success: true, data: status } as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Laden des BLZ-Status',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Laden des BLZ-Status', 500);
}
}
@@ -32,9 +30,6 @@ export async function updateNow(req: AuthRequest, res: Response): Promise<void>
const status = await blzData.getStatus();
res.json({ success: true, data: { result, status } } as ApiResponse);
} catch (error) {
res.status(502).json({
success: false,
error: error instanceof Error ? error.message : 'BLZ-Aktualisierung fehlgeschlagen',
} as ApiResponse);
antworteAufFehler(res, error, 'BLZ-Aktualisierung fehlgeschlagen', 502);
}
}
@@ -1,6 +1,7 @@
// ==================== CACHED EMAIL CONTROLLER ====================
import { Request, Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import * as cachedEmailService from '../services/cachedEmail.service.js';
import * as stressfreiEmailService from '../services/stressfreiEmail.service.js';
import * as invoiceService from '../services/invoice.service.js';
@@ -272,10 +273,7 @@ export async function assignToContract(req: AuthRequest, res: Response): Promise
res.json({ success: true, data: email } as ApiResponse);
} catch (error) {
console.error('assignToContract error:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Zuordnen der E-Mail',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Zuordnen der E-Mail');
}
}
@@ -290,10 +288,7 @@ export async function unassignFromContract(req: AuthRequest, res: Response): Pro
res.json({ success: true, data: email } as ApiResponse);
} catch (error) {
console.error('unassignFromContract error:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aufheben der Zuordnung',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aufheben der Zuordnung');
}
}
@@ -1660,12 +1655,7 @@ export async function saveAttachmentTo(req: AuthRequest, res: Response): Promise
} catch (error) {
console.error('saveAttachmentTo error:', error);
// Detailliertere Fehlermeldung für Debugging
const status = error instanceof ApiError ? error.statusCode : 500;
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
res.status(status).json({
success: false,
error: `Fehler beim Speichern des Anhangs: ${errorMessage}`,
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Speichern des Anhangs', 500);
}
}
@@ -1909,11 +1899,7 @@ export async function saveEmailAsPdf(req: AuthRequest, res: Response): Promise<v
} as ApiResponse);
} catch (error) {
console.error('saveEmailAsPdf error:', error);
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
res.status(500).json({
success: false,
error: `Fehler beim Erstellen der PDF: ${errorMessage}`,
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen der PDF', 500);
}
}
@@ -2036,11 +2022,7 @@ export async function saveEmailAsInvoice(req: AuthRequest, res: Response): Promi
} as ApiResponse);
} catch (error) {
console.error('saveEmailAsInvoice error:', error);
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
res.status(500).json({
success: false,
error: `Fehler beim Erstellen der Rechnung: ${errorMessage}`,
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen der Rechnung', 500);
}
}
@@ -2145,9 +2127,7 @@ export async function saveEmailAsContractDocument(req: AuthRequest, res: Respons
console.error('saveEmailAsContractDocument error:', error);
// Pentest 64.1: ApiError mit eigenem statusCode (z.B. 400 vom Race-
// Lock) statt pauschal 500.
const status = error instanceof ApiError ? error.statusCode : 500;
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
res.status(status).json({ success: false, error: `Fehler beim Speichern: ${errorMessage}` } as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Speichern', 500);
}
}
@@ -2332,12 +2312,7 @@ export async function saveAttachmentAsInvoice(req: AuthRequest, res: Response):
} as ApiResponse);
} catch (error) {
console.error('saveAttachmentAsInvoice error:', error);
const status = error instanceof ApiError ? error.statusCode : 500;
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
res.status(status).json({
success: false,
error: `Fehler beim Erstellen der Rechnung: ${errorMessage}`,
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen der Rechnung', 500);
}
}
@@ -2512,11 +2487,6 @@ export async function saveAttachmentAsContractDocument(req: AuthRequest, res: Re
} catch (error) {
console.error('saveAttachmentAsContractDocument error:', error);
// Pentest 64.1: ApiError mit eigenem statusCode statt pauschal 500.
const status = error instanceof ApiError ? error.statusCode : 500;
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
res.status(status).json({
success: false,
error: `Fehler beim Speichern: ${errorMessage}`,
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Speichern', 500);
}
}
@@ -1,4 +1,5 @@
import { Request, Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import * as cancellationPeriodService from '../services/cancellation-period.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse } from '../types/index.js';
@@ -46,10 +47,7 @@ export async function createCancellationPeriod(req: Request, res: Response): Pro
});
res.status(201).json({ success: true, data: period } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Kündigungsfrist',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen der Kündigungsfrist');
}
}
@@ -64,10 +62,7 @@ export async function updateCancellationPeriod(req: Request, res: Response): Pro
});
res.json({ success: true, data: period } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Kündigungsfrist',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren der Kündigungsfrist');
}
}
@@ -83,9 +78,6 @@ export async function deleteCancellationPeriod(req: Request, res: Response): Pro
});
res.json({ success: true, message: 'Kündigungsfrist gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Kündigungsfrist',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen der Kündigungsfrist');
}
}
@@ -1,4 +1,5 @@
import { Request, Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import * as contractDurationService from '../services/contract-duration.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse } from '../types/index.js';
@@ -46,10 +47,7 @@ export async function createContractDuration(req: Request, res: Response): Promi
});
res.status(201).json({ success: true, data: duration } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Laufzeit',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen der Laufzeit');
}
}
@@ -64,10 +62,7 @@ export async function updateContractDuration(req: Request, res: Response): Promi
});
res.json({ success: true, data: duration } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Laufzeit',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren der Laufzeit');
}
}
@@ -83,9 +78,6 @@ export async function deleteContractDuration(req: Request, res: Response): Promi
});
res.json({ success: true, message: 'Laufzeit gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Laufzeit',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen der Laufzeit');
}
}
+10 -37
View File
@@ -1,4 +1,5 @@
import { Request, Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import fs from 'fs';
import prisma from '../lib/prisma.js';
import * as contractService from '../services/contract.service.js';
@@ -211,10 +212,7 @@ export async function createContract(req: AuthRequest, res: Response): Promise<v
const sanitized = isPortal ? sanitizeContractStrict(contract as any) : sanitizeContract(contract as any);
res.status(201).json({ success: true, data: sanitized } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Vertrags',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen des Vertrags');
}
}
@@ -374,10 +372,7 @@ export async function updateContract(req: AuthRequest, res: Response): Promise<v
: sanitizeContract(contract as any);
res.json({ success: true, data: sanitized } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Vertrags',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren des Vertrags');
}
}
@@ -396,10 +391,7 @@ export async function deleteContract(req: Request, res: Response): Promise<void>
});
res.json({ success: true, message: 'Vertrag gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Vertrags',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen des Vertrags');
}
}
@@ -447,10 +439,7 @@ export async function createFollowUp(req: AuthRequest, res: Response): Promise<v
const sanitized = isPortal ? sanitizeContractStrict(contract as any) : sanitizeContract(contract as any);
res.status(201).json({ success: true, data: sanitized } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Folgevertrags',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen des Folgevertrags');
}
}
@@ -502,10 +491,7 @@ export async function createRenewal(req: AuthRequest, res: Response): Promise<vo
const sanitized = isPortal ? sanitizeContractStrict(contract as any) : sanitizeContract(contract as any);
res.status(201).json({ success: true, data: sanitized } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der VVL',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen der VVL');
}
}
@@ -793,10 +779,7 @@ export async function addSuccessorMeter(req: AuthRequest, res: Response): Promis
res.json({ success: true, data: contractMeter } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen des Folgezählers',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Hinzufügen des Folgezählers');
}
}
@@ -813,10 +796,7 @@ export async function removeContractMeter(req: AuthRequest, res: Response): Prom
});
res.json({ success: true, data: null } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Entfernen',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Entfernen');
}
}
@@ -905,13 +885,9 @@ export async function uploadContractDocument(req: AuthRequest, res: Response): P
// Pentest 64.1: ApiError mit eigenem statusCode honorieren (z.B. 400
// vom Race-Lock); fallback bleibt 400 für sonstige ContractDocument-
// Schreibfehler.
const status = error instanceof ApiError ? error.statusCode : 400;
// Multer hat die Datei schon geschrieben bei Reject räumen.
if (req.file?.path) try { fs.unlinkSync(req.file.path); } catch { /* ignore */ }
res.status(status).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Hochladen',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Hochladen');
}
}
@@ -948,10 +924,7 @@ export async function deleteContractDocument(req: AuthRequest, res: Response): P
res.json({ success: true, message: 'Dokument gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen');
}
}
@@ -1,4 +1,5 @@
import { Request, Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import * as contractCategoryService from '../services/contractCategory.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse } from '../types/index.js';
@@ -46,10 +47,7 @@ export async function createContractCategory(req: Request, res: Response): Promi
});
res.status(201).json({ success: true, data: category } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Vertragskategorie',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen der Vertragskategorie');
}
}
@@ -64,10 +62,7 @@ export async function updateContractCategory(req: Request, res: Response): Promi
});
res.json({ success: true, data: category } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Vertragskategorie',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren der Vertragskategorie');
}
}
@@ -83,9 +78,6 @@ export async function deleteContractCategory(req: Request, res: Response): Promi
});
res.json({ success: true, message: 'Vertragskategorie gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Vertragskategorie',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen der Vertragskategorie');
}
}
@@ -1,4 +1,5 @@
import { Request, Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import * as contractHistoryService from '../services/contractHistory.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
@@ -47,10 +48,7 @@ export async function createHistoryEntry(req: AuthRequest, res: Response): Promi
res.status(201).json({ success: true, data: entry } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Eintrags',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen des Eintrags');
}
}
@@ -74,10 +72,7 @@ export async function updateHistoryEntry(req: AuthRequest, res: Response): Promi
res.json({ success: true, data: entry } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Eintrags',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren des Eintrags');
}
}
@@ -97,9 +92,6 @@ export async function deleteHistoryEntry(req: AuthRequest, res: Response): Promi
res.json({ success: true, message: 'Eintrag gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Eintrags',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen des Eintrags');
}
}
@@ -1,4 +1,5 @@
import { Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import * as contractTaskService from '../services/contractTask.service.js';
import * as contractService from '../services/contract.service.js';
import * as customerService from '../services/customer.service.js';
@@ -146,10 +147,7 @@ export async function createGeneralTask(req: AuthRequest, res: Response): Promis
});
res.status(201).json({ success: true, data: task } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Aufgabe',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen der Aufgabe');
}
}
@@ -187,10 +185,7 @@ export async function createTask(req: AuthRequest, res: Response): Promise<void>
res.status(201).json({ success: true, data: task } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Aufgabe',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen der Aufgabe');
}
}
@@ -239,10 +234,7 @@ export async function createSupportTicket(req: AuthRequest, res: Response): Prom
res.status(201).json({ success: true, data: task } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Support-Anfrage',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen der Support-Anfrage');
}
}
@@ -265,10 +257,7 @@ export async function updateTask(req: AuthRequest, res: Response): Promise<void>
res.json({ success: true, data: task } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Aufgabe',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren der Aufgabe');
}
}
@@ -283,10 +272,7 @@ export async function completeTask(req: AuthRequest, res: Response): Promise<voi
});
res.json({ success: true, data: task } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Abschließen der Aufgabe',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Abschließen der Aufgabe');
}
}
@@ -301,10 +287,7 @@ export async function reopenTask(req: AuthRequest, res: Response): Promise<void>
});
res.json({ success: true, data: task } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Wiedereröffnen der Aufgabe',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Wiedereröffnen der Aufgabe');
}
}
@@ -319,10 +302,7 @@ export async function deleteTask(req: AuthRequest, res: Response): Promise<void>
});
res.json({ success: true, message: 'Aufgabe gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Aufgabe',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen der Aufgabe');
}
}
@@ -357,10 +337,7 @@ export async function createSubtask(req: AuthRequest, res: Response): Promise<vo
res.status(201).json({ success: true, data: subtask } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Unteraufgabe',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen der Unteraufgabe');
}
}
@@ -439,10 +416,7 @@ export async function createCustomerReply(req: AuthRequest, res: Response): Prom
res.status(201).json({ success: true, data: subtask } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Antwort',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen der Antwort');
}
}
@@ -467,10 +441,7 @@ export async function updateSubtask(req: AuthRequest, res: Response): Promise<vo
});
res.json({ success: true, data: subtask } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Unteraufgabe',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren der Unteraufgabe');
}
}
@@ -485,10 +456,7 @@ export async function completeSubtask(req: AuthRequest, res: Response): Promise<
});
res.json({ success: true, data: subtask } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Abschließen der Unteraufgabe',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Abschließen der Unteraufgabe');
}
}
@@ -503,10 +471,7 @@ export async function reopenSubtask(req: AuthRequest, res: Response): Promise<vo
});
res.json({ success: true, data: subtask } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Wiedereröffnen der Unteraufgabe',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Wiedereröffnen der Unteraufgabe');
}
}
@@ -521,9 +486,6 @@ export async function deleteSubtask(req: AuthRequest, res: Response): Promise<vo
});
res.json({ success: true, message: 'Unteraufgabe gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Unteraufgabe',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen der Unteraufgabe');
}
}
@@ -1,4 +1,5 @@
import { Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
import { logChange } from '../services/audit.service.js';
import { ApiError } from '../utils/apiError.js';
@@ -36,11 +37,9 @@ function idParam(req: AuthRequest, res: Response, name: string): number | null {
}
function handleError(res: Response, error: unknown, fallback: string) {
const status = error instanceof ApiError ? error.statusCode : 500;
res.status(status).json({
success: false,
error: error instanceof Error ? error.message : fallback,
} as ApiResponse);
// Eigene Huelle beibehalten, damit die Aufrufer unveraendert bleiben - die
// Entscheidung, was nach draussen geht, faellt jetzt zentral.
antworteAufFehler(res, error, fallback, 500);
}
// ---- Gesamtübersicht (Hauptmenü) ----
+30 -119
View File
@@ -1,4 +1,5 @@
import { Request, Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import prisma from '../lib/prisma.js';
import * as customerService from '../services/customer.service.js';
import * as authService from '../services/auth.service.js';
@@ -155,10 +156,7 @@ export async function createCustomer(req: Request, res: Response): Promise<void>
: sanitizeCustomerStrict(customer as any);
res.status(201).json({ success: true, data: sanitized } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Kunden',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen des Kunden');
}
}
@@ -276,10 +274,7 @@ export async function updateCustomer(req: Request, res: Response): Promise<void>
res.json({ success: true, data: sanitized } as ApiResponse);
} catch (error) {
console.error('Update customer error:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Kunden',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren des Kunden');
}
}
@@ -296,10 +291,7 @@ export async function deleteCustomer(req: Request, res: Response): Promise<void>
});
res.json({ success: true, message: 'Kunde gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Kunden',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen des Kunden');
}
}
@@ -328,10 +320,7 @@ export async function createAddress(req: AuthRequest, res: Response): Promise<vo
});
res.status(201).json({ success: true, data: address } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Adresse',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen der Adresse');
}
}
@@ -392,10 +381,7 @@ export async function updateAddress(req: AuthRequest, res: Response): Promise<vo
res.json({ success: true, data: address } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Adresse',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren der Adresse');
}
}
@@ -414,10 +400,7 @@ export async function deleteAddress(req: AuthRequest, res: Response): Promise<vo
});
res.json({ success: true, message: 'Adresse gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Adresse',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen der Adresse');
}
}
@@ -448,10 +431,7 @@ export async function createBankCard(req: AuthRequest, res: Response): Promise<v
});
res.status(201).json({ success: true, data: card } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Bankkarte',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen der Bankkarte');
}
}
@@ -510,10 +490,7 @@ export async function updateBankCard(req: AuthRequest, res: Response): Promise<v
res.json({ success: true, data: card } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Bankkarte',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren der Bankkarte');
}
}
@@ -532,10 +509,7 @@ export async function deleteBankCard(req: AuthRequest, res: Response): Promise<v
});
res.json({ success: true, message: 'Bankkarte gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Bankkarte',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen der Bankkarte');
}
}
@@ -565,10 +539,7 @@ export async function createDocument(req: AuthRequest, res: Response): Promise<v
});
res.status(201).json({ success: true, data: doc } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Ausweises',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen des Ausweises');
}
}
@@ -630,10 +601,7 @@ export async function updateDocument(req: AuthRequest, res: Response): Promise<v
res.json({ success: true, data: doc } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Ausweises',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren des Ausweises');
}
}
@@ -652,10 +620,7 @@ export async function deleteDocument(req: AuthRequest, res: Response): Promise<v
});
res.json({ success: true, message: 'Ausweis gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Ausweises',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen des Ausweises');
}
}
@@ -688,10 +653,7 @@ export async function createMeter(req: AuthRequest, res: Response): Promise<void
});
res.status(201).json({ success: true, data: meter } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Zählers',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen des Zählers');
}
}
@@ -746,10 +708,7 @@ export async function updateMeter(req: AuthRequest, res: Response): Promise<void
res.json({ success: true, data: meter } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Zählers',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren des Zählers');
}
}
@@ -765,10 +724,7 @@ export async function deleteMeter(req: AuthRequest, res: Response): Promise<void
});
res.json({ success: true, message: 'Zähler gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Zählers',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen des Zählers');
}
}
@@ -814,10 +770,7 @@ export async function addMeterReading(req: AuthRequest, res: Response): Promise<
res.status(201).json({ success: true, data: reading } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen des Zählerstands',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Hinzufügen des Zählerstands');
}
}
@@ -845,10 +798,7 @@ export async function updateMeterReading(req: AuthRequest, res: Response): Promi
});
res.json({ success: true, data: reading } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Zählerstands',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren des Zählerstands');
}
}
@@ -865,10 +815,7 @@ export async function deleteMeterReading(req: AuthRequest, res: Response): Promi
});
res.json({ success: true, data: null } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Zählerstands',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen des Zählerstands');
}
}
@@ -923,10 +870,7 @@ export async function reportMeterReading(req: AuthRequest, res: Response): Promi
res.status(201).json({ success: true, data: reading } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Melden des Zählerstands',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Melden des Zählerstands');
}
}
@@ -978,10 +922,7 @@ export async function markReadingTransferred(req: AuthRequest, res: Response): P
res.json({ success: true, data: reading } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren');
}
}
@@ -1091,10 +1032,7 @@ export async function updatePortalSettings(req: Request, res: Response): Promise
res.json({ success: true, data: settings } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Portal-Einstellungen',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren der Portal-Einstellungen');
}
}
@@ -1108,10 +1046,7 @@ export async function generatePortalPassword(req: Request, res: Response): Promi
const password = generateSecurePassword({ length: 16 });
res.json({ success: true, data: { password } } as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Generieren des Passworts',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Generieren des Passworts', 500);
}
}
@@ -1199,10 +1134,7 @@ export async function sendPortalCredentials(req: AuthRequest, res: Response): Pr
res.json({ success: true, message: `Zugangsdaten an ${targetEmail} versendet (Einmalpasswort)` } as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Versenden der Zugangsdaten',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Versenden der Zugangsdaten', 500);
}
}
@@ -1228,10 +1160,7 @@ export async function setPortalPassword(req: Request, res: Response): Promise<vo
});
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);
antworteAufFehler(res, error, 'Fehler beim Setzen des Passworts');
}
}
@@ -1304,10 +1233,7 @@ export async function addRepresentative(req: AuthRequest, res: Response): Promis
});
res.status(201).json({ success: true, data: representative } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen des Vertreters',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Hinzufügen des Vertreters');
}
}
@@ -1326,10 +1252,7 @@ export async function removeRepresentative(req: AuthRequest, res: Response): Pro
});
res.json({ success: true, message: 'Vertreter entfernt' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Entfernen des Vertreters',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Entfernen des Vertreters');
}
}
@@ -1354,11 +1277,7 @@ export async function getSalutationPreference(req: AuthRequest, res: Response):
} catch (error) {
// Pentest R104.1: ApiError-Statuscodes durchreichen (404 statt 500
// bei nicht-existierendem Kunden).
const status = error instanceof ApiError ? error.statusCode : 500;
res.status(status).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Laden der Anrede-Präferenz',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Laden der Anrede-Präferenz', 500);
}
}
@@ -1387,11 +1306,7 @@ export async function setSalutationPreference(req: AuthRequest, res: Response):
const result = await customerService.getSalutationPreference(userId, customerId);
res.json({ success: true, data: result } as ApiResponse);
} catch (error) {
const status = error instanceof ApiError ? error.statusCode : 500;
res.status(status).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Speichern der Anrede-Präferenz',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Speichern der Anrede-Präferenz', 500);
}
}
@@ -1412,11 +1327,7 @@ export async function clearSalutationPreference(req: AuthRequest, res: Response)
const result = await customerService.getSalutationPreference(userId, customerId);
res.json({ success: true, data: result } as ApiResponse);
} catch (error) {
const status = error instanceof ApiError ? error.statusCode : 500;
res.status(status).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Zurücksetzen der Anrede-Präferenz',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Zurücksetzen der Anrede-Präferenz', 500);
}
}
@@ -1,4 +1,5 @@
import { Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
import { logChange } from '../services/audit.service.js';
import * as referralService from '../services/customerReferral.service.js';
@@ -98,11 +99,7 @@ export async function createReferral(req: AuthRequest, res: Response): Promise<v
res.status(201).json({ success: true, data: created } as ApiResponse);
} catch (error) {
const status = (error as referralService.ReferralError)?.status ?? 500;
res.status(status).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Anlegen der Werbe-Beziehung',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Anlegen der Werbe-Beziehung', 500);
}
}
@@ -153,11 +150,7 @@ export async function updateReferral(req: AuthRequest, res: Response): Promise<v
res.json({ success: true, data: updated } as ApiResponse);
} catch (error) {
const status = (error as referralService.ReferralError)?.status ?? 500;
res.status(status).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Ändern der Werbe-Beziehung',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Ändern der Werbe-Beziehung', 500);
}
}
@@ -1,6 +1,7 @@
// ==================== EMAIL PROVIDER CONTROLLER ====================
import { Request, Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import * as emailProviderService from '../services/emailProvider/emailProviderService.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse } from '../types/index.js';
@@ -60,10 +61,7 @@ export async function createProviderConfig(req: Request, res: Response): Promise
});
res.status(201).json({ success: true, data: config } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Email-Providers',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen des Email-Providers');
}
}
@@ -80,10 +78,7 @@ export async function updateProviderConfig(req: Request, res: Response): Promise
});
res.json({ success: true, data: config } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Email-Providers',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren des Email-Providers');
}
}
@@ -99,10 +94,7 @@ export async function deleteProviderConfig(req: Request, res: Response): Promise
});
res.json({ success: true, message: 'Email-Provider gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Email-Providers',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen des Email-Providers');
}
}
@@ -158,10 +150,7 @@ export async function testConnection(req: Request, res: Response): Promise<void>
const result = await emailProviderService.testProviderConnection({ id, testData });
res.json({ success: result.success, data: result } as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Verbindungstest fehlgeschlagen',
} as ApiResponse);
antworteAufFehler(res, error, 'Verbindungstest fehlgeschlagen', 500);
}
}
@@ -342,10 +331,7 @@ export async function testMailAccess(req: Request, res: Response): Promise<void>
} as ApiResponse);
} catch (error) {
console.error('testMailAccess error:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Test',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Test', 500);
}
}
@@ -355,10 +341,7 @@ export async function checkEmailExists(req: Request, res: Response): Promise<voi
const result = await emailProviderService.checkEmailExists(localPart);
res.json({ success: true, data: result } as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler bei der E-Mail-Prüfung',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler bei der E-Mail-Prüfung', 500);
}
}
@@ -377,10 +360,7 @@ export async function provisionEmail(req: Request, res: Response): Promise<void>
const result = await emailProviderService.provisionEmail(localPart, customerEmail);
res.json({ success: result.success, data: result } as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler bei der E-Mail-Provisionierung',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler bei der E-Mail-Provisionierung', 500);
}
}
@@ -390,10 +370,7 @@ export async function deprovisionEmail(req: Request, res: Response): Promise<voi
const result = await emailProviderService.deprovisionEmail(localPart);
res.json({ success: result.success, data: result } as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen der E-Mail',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen der E-Mail', 500);
}
}
@@ -1,4 +1,5 @@
import { Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import { AuthRequest } from '../types/index.js';
import * as factoryDefaultsService from '../services/factoryDefaults.service.js';
import { createAuditLog } from '../services/audit.service.js';
@@ -31,10 +32,7 @@ export async function exportFactoryDefaults(req: AuthRequest, res: Response) {
res.send(buffer);
} catch (error) {
console.error('Fehler beim Factory-Defaults-Export:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Export',
});
antworteAufFehler(res, error, 'Fehler beim Export', 500);
}
}
@@ -93,9 +91,6 @@ export async function importFactoryDefaults(req: AuthRequest, res: Response) {
res.json({ success: true, data: result });
} catch (error) {
console.error('Fehler beim Factory-Defaults-Import:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Import',
});
antworteAufFehler(res, error, 'Fehler beim Import');
}
}
+11 -40
View File
@@ -1,4 +1,5 @@
import { Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import { AuthRequest } from '../types/index.js';
import * as gdprService from '../services/gdpr.service.js';
import * as consentService from '../services/consent.service.js';
@@ -54,10 +55,7 @@ export async function exportCustomerData(req: AuthRequest, res: Response) {
}
} catch (error) {
console.error('Fehler beim Datenexport:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Datenexport',
});
antworteAufFehler(res, error, 'Fehler beim Datenexport', 500);
}
}
@@ -98,10 +96,7 @@ export async function createDeletionRequest(req: AuthRequest, res: Response) {
res.status(201).json({ success: true, data: request });
} catch (error) {
console.error('Fehler beim Erstellen der Löschanfrage:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen',
});
antworteAufFehler(res, error, 'Fehler beim Erstellen');
}
}
@@ -183,10 +178,7 @@ export async function processDeletionRequest(req: AuthRequest, res: Response) {
res.json({ success: true, data: result });
} catch (error) {
console.error('Fehler beim Bearbeiten der Löschanfrage:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Bearbeiten',
});
antworteAufFehler(res, error, 'Fehler beim Bearbeiten');
}
}
@@ -346,10 +338,7 @@ export async function updateCustomerConsent(req: AuthRequest, res: Response) {
res.json({ success: true, data: consent });
} catch (error) {
console.error('Fehler beim Aktualisieren der Einwilligung:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren',
});
antworteAufFehler(res, error, 'Fehler beim Aktualisieren');
}
}
@@ -697,10 +686,7 @@ export async function sendConsentLink(req: AuthRequest, res: Response) {
});
} catch (error) {
console.error('Fehler beim Senden des Consent-Links:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Senden',
});
antworteAufFehler(res, error, 'Fehler beim Senden', 500);
}
}
@@ -819,10 +805,7 @@ export async function sendAuthorizationRequest(req: AuthRequest, res: Response)
});
} catch (error) {
console.error('Fehler beim Senden der Vollmacht-Anfrage:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Senden',
});
antworteAufFehler(res, error, 'Fehler beim Senden', 500);
}
}
@@ -875,10 +858,7 @@ export async function grantAuthorization(req: AuthRequest, res: Response) {
res.json({ success: true, data: auth });
} catch (error) {
console.error('Fehler beim Erteilen der Vollmacht:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erteilen der Vollmacht',
});
antworteAufFehler(res, error, 'Fehler beim Erteilen der Vollmacht');
}
}
@@ -904,10 +884,7 @@ export async function withdrawAuthorization(req: AuthRequest, res: Response) {
res.json({ success: true, data: auth });
} catch (error) {
console.error('Fehler beim Widerrufen der Vollmacht:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Widerrufen',
});
antworteAufFehler(res, error, 'Fehler beim Widerrufen');
}
}
@@ -1012,10 +989,7 @@ export async function uploadAuthorizationDocument(req: AuthRequest, res: Respons
res.json({ success: true, data: auth });
} catch (error) {
console.error('Fehler beim Upload des Vollmacht-Dokuments:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Upload',
});
antworteAufFehler(res, error, 'Fehler beim Upload');
}
}
@@ -1039,10 +1013,7 @@ export async function deleteAuthorizationDocument(req: AuthRequest, res: Respons
res.json({ success: true, data: auth });
} catch (error) {
console.error('Fehler beim Löschen des Vollmacht-Dokuments:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen',
});
antworteAufFehler(res, error, 'Fehler beim Löschen');
}
}
+5 -16
View File
@@ -1,4 +1,5 @@
import { Request, Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import * as invoiceService from '../services/invoice.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
@@ -101,10 +102,7 @@ export async function addInvoice(req: AuthRequest, res: Response): Promise<void>
res.status(201).json({ success: true, data: invoice } as ApiResponse);
} catch (error) {
console.error('addInvoice error:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen der Rechnung',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Hinzufügen der Rechnung');
}
}
@@ -136,10 +134,7 @@ export async function updateInvoice(req: AuthRequest, res: Response): Promise<vo
res.json({ success: true, data: invoice } as ApiResponse);
} catch (error) {
console.error('updateInvoice error:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Rechnung',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren der Rechnung');
}
}
@@ -163,10 +158,7 @@ export async function deleteInvoice(req: AuthRequest, res: Response): Promise<vo
res.json({ success: true, data: null } as ApiResponse);
} catch (error) {
console.error('deleteInvoice error:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Rechnung',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen der Rechnung');
}
}
@@ -208,9 +200,6 @@ export async function addInvoiceByContract(req: AuthRequest, res: Response): Pro
});
res.status(201).json({ success: true, data: invoice } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Hinzufügen');
}
}
@@ -1,4 +1,5 @@
import { Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import prisma from '../lib/prisma.js';
import { AuthRequest, ApiResponse } from '../types/index.js';
import * as appSettingService from '../services/appSetting.service.js';
@@ -141,10 +142,7 @@ export async function testAlert(_req: AuthRequest, res: Response): Promise<void>
res.status(500).json({ success: false, error: result.error || 'Versand fehlgeschlagen' } as ApiResponse);
}
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Test-Alert fehlgeschlagen',
} as ApiResponse);
antworteAufFehler(res, error, 'Test-Alert fehlgeschlagen', 500);
}
}
@@ -201,9 +199,6 @@ export async function runDigestNow(_req: AuthRequest, res: Response): Promise<vo
const result = await sendDigest({ force: true });
res.json({ success: true, data: result } as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Digest fehlgeschlagen',
} as ApiResponse);
antworteAufFehler(res, error, 'Digest fehlgeschlagen', 500);
}
}
@@ -1,4 +1,5 @@
import { Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import { AuthRequest } from '../types/index.js';
import * as pdfTemplateService from '../services/pdfTemplate.service.js';
import { logChange } from '../services/audit.service.js';
@@ -57,10 +58,7 @@ export async function createTemplate(req: AuthRequest, res: Response) {
res.status(201).json({ success: true, data: { ...template, pdfFields } });
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen',
});
antworteAufFehler(res, error, 'Fehler beim Erstellen');
}
}
@@ -87,10 +85,7 @@ export async function updateTemplate(req: AuthRequest, res: Response) {
res.json({ success: true, data: template });
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren',
});
antworteAufFehler(res, error, 'Fehler beim Aktualisieren');
}
}
@@ -119,10 +114,7 @@ export async function extractFields(req: AuthRequest, res: Response) {
const result = await pdfTemplateService.extractPdfFields(template.templatePath);
res.json({ success: true, data: result.fields, totalPages: result.totalPages });
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Auslesen der PDF-Felder',
});
antworteAufFehler(res, error, 'Fehler beim Auslesen der PDF-Felder');
}
}
@@ -137,7 +129,7 @@ export async function getAnnotatedPreview(req: AuthRequest, res: Response) {
res.setHeader('Content-Disposition', 'inline; filename="preview.pdf"');
res.send(pdfBuffer);
} catch (error) {
res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler' });
antworteAufFehler(res, error, 'Fehler', 400);
}
}
@@ -154,7 +146,7 @@ export async function getRequiredInputs(req: AuthRequest, res: Response) {
const inputs = await pdfTemplateService.getRequiredInputs(templateId, contractId);
res.json({ success: true, data: inputs });
} catch (error) {
res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler' });
antworteAufFehler(res, error, 'Fehler', 400);
}
}
@@ -195,9 +187,6 @@ export async function generatePdf(req: AuthRequest, res: Response) {
res.send(pdfBuffer);
} catch (error) {
console.error('PDF generate error:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Generieren',
});
antworteAufFehler(res, error, 'Fehler beim Generieren');
}
}
+4 -12
View File
@@ -1,4 +1,5 @@
import { Request, Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import * as platformService from '../services/platform.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse } from '../types/index.js';
@@ -46,10 +47,7 @@ export async function createPlatform(req: Request, res: Response): Promise<void>
});
res.status(201).json({ success: true, data: platform } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Vertriebsplattform',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen der Vertriebsplattform');
}
}
@@ -64,10 +62,7 @@ export async function updatePlatform(req: Request, res: Response): Promise<void>
});
res.json({ success: true, data: platform } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Vertriebsplattform',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren der Vertriebsplattform');
}
}
@@ -83,9 +78,6 @@ export async function deletePlatform(req: Request, res: Response): Promise<void>
});
res.json({ success: true, message: 'Vertriebsplattform gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Vertriebsplattform',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen der Vertriebsplattform');
}
}
+4 -12
View File
@@ -1,4 +1,5 @@
import { Request, Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import bcrypt from 'bcryptjs';
import prisma from '../lib/prisma.js';
import * as providerService from '../services/provider.service.js';
@@ -99,10 +100,7 @@ export async function createProvider(req: Request, res: Response): Promise<void>
});
res.status(201).json({ success: true, data: provider } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Anbieters',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen des Anbieters');
}
}
@@ -136,10 +134,7 @@ export async function updateProvider(req: Request, res: Response): Promise<void>
});
res.json({ success: true, data: provider } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Anbieters',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren des Anbieters');
}
}
@@ -155,9 +150,6 @@ export async function deleteProvider(req: Request, res: Response): Promise<void>
});
res.json({ success: true, message: 'Anbieter gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Anbieters',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen des Anbieters');
}
}
@@ -1,4 +1,5 @@
import { Request, Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import * as stressfreiEmailService from '../services/stressfreiEmail.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
@@ -93,11 +94,7 @@ export async function createEmail(req: Request, res: Response): Promise<void> {
});
res.status(201).json({ success: true, data: email } as ApiResponse);
} catch (error) {
const status = error instanceof ApiError ? error.statusCode : 400;
res.status(status).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Stressfrei-Wechseln Adresse',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen der Stressfrei-Wechseln Adresse', 400);
}
}
@@ -119,11 +116,7 @@ export async function updateEmail(req: AuthRequest, res: Response): Promise<void
});
res.json({ success: true, data: email } as ApiResponse);
} catch (error) {
const status = error instanceof ApiError ? error.statusCode : 400;
res.status(status).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Stressfrei-Wechseln Adresse',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren der Stressfrei-Wechseln Adresse', 400);
}
}
@@ -140,10 +133,7 @@ export async function deleteEmail(req: AuthRequest, res: Response): Promise<void
});
res.json({ success: true, message: 'Stressfrei-Wechseln Adresse gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Stressfrei-Wechseln Adresse',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen der Stressfrei-Wechseln Adresse');
}
}
@@ -180,10 +170,7 @@ export async function syncForwarding(req: AuthRequest, res: Response): Promise<v
message: 'Weiterleitungen aktualisiert',
} as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Synchronisieren der Weiterleitungen',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Synchronisieren der Weiterleitungen', 500);
}
}
@@ -228,11 +215,7 @@ export async function updateAdditionalForwards(req: AuthRequest, res: Response):
message: 'Weiterleitungen aktualisiert',
} as ApiResponse);
} catch (error) {
const status = error instanceof ApiError ? error.statusCode : 500;
res.status(status).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Weiterleitungen',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren der Weiterleitungen', 500);
}
}
@@ -255,9 +238,6 @@ export async function resetPassword(req: AuthRequest, res: Response): Promise<vo
message: 'Passwort wurde zurückgesetzt',
} as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Zurücksetzen des Passworts',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Zurücksetzen des Passworts', 500);
}
}
+4 -12
View File
@@ -1,4 +1,5 @@
import { Request, Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import * as tariffService from '../services/tariff.service.js';
import { logChange } from '../services/audit.service.js';
import { ApiResponse } from '../types/index.js';
@@ -48,10 +49,7 @@ export async function createTariff(req: Request, res: Response): Promise<void> {
});
res.status(201).json({ success: true, data: tariff } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Tarifs',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Erstellen des Tarifs');
}
}
@@ -66,10 +64,7 @@ export async function updateTariff(req: Request, res: Response): Promise<void> {
});
res.json({ success: true, data: tariff } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Tarifs',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Aktualisieren des Tarifs');
}
}
@@ -85,9 +80,6 @@ export async function deleteTariff(req: Request, res: Response): Promise<void> {
});
res.json({ success: true, message: 'Tarif gelöscht' } as ApiResponse);
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Tarifs',
} as ApiResponse);
antworteAufFehler(res, error, 'Fehler beim Löschen des Tarifs');
}
}
+3 -8
View File
@@ -1,4 +1,5 @@
import { Request, Response } from 'express';
import { antworteAufFehler } from '../utils/fehlerAntwort.js';
import bcrypt from 'bcryptjs';
import prisma from '../lib/prisma.js';
import * as userService from '../services/user.service.js';
@@ -520,10 +521,7 @@ export async function setUserPassword(req: Request, res: Response): Promise<void
});
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);
antworteAufFehler(res, error, 'Fehler beim Setzen des Passworts');
}
}
@@ -539,10 +537,7 @@ export async function deleteUser(req: Request, res: Response): Promise<void> {
});
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);
antworteAufFehler(res, error, 'Fehler beim Löschen des Benutzers');
}
}
+13 -1
View File
@@ -1,4 +1,5 @@
import { Response, NextFunction } from 'express';
import { PORTAL_RECHTE } from '../config/rechte-katalog.js';
import jwt from 'jsonwebtoken';
import prisma from '../lib/prisma.js';
import { AuthRequest, JwtPayload } from '../types/index.js';
@@ -128,7 +129,18 @@ export function requirePermission(...requiredPermissions: string[]) {
return;
}
const userPermissions = req.user.permissions || [];
// Portal-Zugaenge bekommen hier nur, was in PORTAL_RECHTE steht -
// unabhaengig davon, was ihr Token behauptet.
//
// Das ist ein Riegel, kein Filter: Die Trennung zwischen Kundenportal und
// Rollensystem ist Absicht (siehe config/rechte-katalog.ts). Ein Kunde
// darf niemals operative Rechte bekommen - die Portalansicht ist dafuer
// nicht gebaut und prueft es nicht. Bisher haette eine einzige unbedachte
// Zeile in der Token-Erzeugung gereicht, um das zu kippen; ab hier
// braeuchte es zwei, an zwei Stellen, in Kenntnis dieses Kommentars.
const userPermissions = req.user.isCustomerPortal
? (req.user.permissions || []).filter((p) => PORTAL_RECHTE.includes(p))
: req.user.permissions || [];
// Check if user has any of the required permissions
const hasPermission = requiredPermissions.some((perm) =>
+5 -8
View File
@@ -1,4 +1,5 @@
import prisma from '../lib/prisma.js';
import { PORTAL_RECHTE } from '../config/rechte-katalog.js';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
@@ -420,10 +421,9 @@ export async function customerLogin(email: string, password: string) {
const representedCustomerIds = grantedRepresentingFor.map((rep) => rep.customer.id);
// Kundenportal-Berechtigungen (eingeschränkt)
const customerPermissions = [
'contracts:read', // Eigene Verträge lesen
'customers:read', // Eigene Kundendaten lesen
];
// Feste Portal-Rechte aus config/rechte-katalog.ts. Bewusst getrennt vom
// Rollensystem - siehe den Kommentar dort.
const customerPermissions = [...PORTAL_RECHTE];
const payload: JwtPayload = {
email: customer.portalEmail!,
@@ -791,10 +791,7 @@ export async function getCustomerPortalUser(customerId: number) {
if (!customer || !customer.portalEnabled) return null;
const customerPermissions = [
'contracts:read',
'customers:read',
];
const customerPermissions = [...PORTAL_RECHTE];
// Selbe Live-Vollmacht-Filterung wie in customerLogin (Pentest Runde 10):
// ohne sie zeigt /me dem Vertreter weiterhin widerrufene Beziehungen.
@@ -2,6 +2,7 @@
// "Geworben / angeworben": wer hat wen an Board geholt.
import prisma from '../lib/prisma.js';
import { FachlicherFehler } from '../utils/apiError.js';
// Erlaubte Beziehungsarten (Whitelist). Server vertraut dem Frontend-Dropdown
// nicht ein handgebauter Request mit beliebigem String wird abgewiesen.
@@ -64,10 +65,11 @@ export async function getReferralsForCustomer(customerId: number) {
};
}
export class ReferralError extends Error {
status: number;
export class ReferralError extends FachlicherFehler {
/** Beibehalten, weil bestehende Aufrufer `.status` lesen. */
readonly status: number;
constructor(message: string, status = 400) {
super(message);
super(message, status);
this.status = status;
}
}
+22 -1
View File
@@ -1,5 +1,5 @@
import prisma from '../lib/prisma.js';
import { SYSTEMROLLEN } from '../config/rechte-katalog.js';
import { SYSTEMROLLEN, pruefePortalRechte } from '../config/rechte-katalog.js';
/**
* Prueft beim Start, ob die gesetzlich gebundenen Rechte ueberhaupt jemand
@@ -59,6 +59,27 @@ export async function pruefePflichtrechte(): Promise<void> {
}
}
// Wache ueber die Portal-Trennung.
//
// Ein schreibendes Recht in PORTAL_RECHTE waere kein Schoenheitsfehler,
// sondern eine stille Ausweitung der Kundenrechte - jeder Portal-Kunde
// haette es sofort. Diese Meldung soll den Tag ueberleben, an dem jemand
// "nur kurz" etwas hinzufuegt.
const portalBeanstandet = pruefePortalRechte();
if (portalBeanstandet.length > 0) {
console.warn(
'\n' +
'========================================================================\n' +
' ACHTUNG: Das Kundenportal traegt Rechte, die nicht Lesen sind:\n' +
portalBeanstandet.map((r) => ` ${r}\n`).join('') +
'\n' +
' Kunden sollen im Portal NIEMALS operative Rechte haben. Die Ansicht\n' +
' ist dafuer nicht gebaut und prueft es nicht. Bitte PORTAL_RECHTE in\n' +
' config/rechte-katalog.ts pruefen.\n' +
'========================================================================\n',
);
}
// Zweite Wache: Stimmen die Systemrollen noch?
//
// Die versteckten Rollen werden ueber ihren NAMEN gefunden
+7 -8
View File
@@ -24,6 +24,7 @@
*/
import prisma from '../lib/prisma.js';
import { FachlicherFehler } from '../utils/apiError.js';
/** Groesste Zahl, die in eine INT-Spalte passt. Darueber gibt es keine ID. */
const INT_MAX = 2147483647;
@@ -35,13 +36,13 @@ import {
} from '../config/rechte-katalog.js';
/** Der Handelnde wollte Rechte vergeben, die er selbst nicht besitzt. */
export class RechteEskalationError extends Error {
export class RechteEskalationError extends FachlicherFehler {
constructor(public readonly fehlend: string[]) {
super(
'Sie können nur Rechte vergeben, die Sie selbst besitzen. ' +
`Nicht vergeben werden können: ${fehlend.join(', ')}`,
403,
);
this.name = 'RechteEskalationError';
}
}
@@ -50,18 +51,16 @@ export class RechteEskalationError extends Error {
* Rollen-ID. Eigene Klasse, damit daraus ein 400 wird und nicht ein 500 aus
* einem Fremdschluesselfehler tief in Prisma.
*/
export class UngueltigeEingabeError extends Error {
export class UngueltigeEingabeError extends FachlicherFehler {
constructor(nachricht: string) {
super(nachricht);
this.name = 'UngueltigeEingabeError';
super(nachricht, 400);
}
}
/** Der Vorgang zielte auf eine Systemrolle, die von der Anwendung gepflegt wird. */
export class RollenSperrError extends Error {
export class RollenSperrError extends FachlicherFehler {
constructor(nachricht: string) {
super(nachricht);
this.name = 'RollenSperrError';
super(nachricht, 403);
}
}
+95 -98
View File
@@ -169,6 +169,79 @@ export async function getUserById(id: number) {
};
}
/**
* Faehigkeiten, ohne die sich das System nicht mehr verwalten laesst.
*
* Bis 09/2026 galt hier "wer `users:delete` hat, ist Admin". Das war ein
* Zufallsmerkmal: Wer Konten anlegen und bearbeiten darf, aber nicht loeschen,
* verwaltet genauso - zaehlte aber nicht. Umgekehrt zaehlte jemand mit nur
* `users:delete` als Admin, obwohl er kein Konto anlegen kann. Ein
* Sicherheitsnetz an einem Merkmal, das mit der geschuetzten Eigenschaft nur
* lose zusammenhing (Pentest R193, Nachrangpunkt 2).
*
* Bewusst NICHT auf die Rolle "Admin" umgestellt, wie zunaechst vorgeschlagen:
* Eine selbst gebaute Rolle mit `users:update` verwaltet tatsaechlich, und ein
* Namenskriterium wuerde sie uebersehen - man koennte dann den letzten
* Admin loeschen, obwohl die Verwaltungsfaehigkeit erhalten bliebe. Geschuetzt
* wird die FAEHIGKEIT, nicht ihr ueblicher Traeger.
*
* `users:update` ist das Minimum, um Konten wieder in Ordnung zu bringen
* (Rollen zuweisen). `roles:manage` ist das Minimum, um Rollen zu definieren.
* Faellt eines davon ersatzlos weg, hilft nur noch die Kommandozeile.
*/
const UNVERZICHTBARE_FAEHIGKEITEN = [
{ recht: 'users:update', wofuer: 'Benutzer verwalten' },
{ recht: 'roles:manage', wofuer: 'Rollen und Rechte pflegen' },
] as const;
/** Die Rechte eines Kontos aus seinen geladenen Rollen. */
function rechteAusRollen(
rollen: Array<{ role: { permissions: Array<{ permission: { resource: string; action: string } }> } }>,
): Set<string> {
const rechte = new Set<string>();
for (const ur of rollen) {
for (const rp of ur.role.permissions) {
rechte.add(`${rp.permission.resource}:${rp.permission.action}`);
}
}
return rechte;
}
/**
* Verhindert, dass die letzte Person mit einer unverzichtbaren Faehigkeit
* sie verliert - durch Rollenwechsel, Deaktivierung oder Loeschung.
*
* `kuenftigeRechte === null` heisst: Das Konto faellt ganz weg.
*/
async function pruefeLetzterVerwalter(
zielId: number,
bisherigeRechte: Set<string>,
kuenftigeRechte: Set<string> | null,
): Promise<void> {
for (const { recht, wofuer } of UNVERZICHTBARE_FAEHIGKEITEN) {
if (!bisherigeRechte.has(recht)) continue; // hatte es nie
if (kuenftigeRechte?.has(recht)) continue; // behaelt es
const [resource, action] = recht.split(':');
const andere = await prisma.user.count({
where: {
id: { not: zielId },
isActive: true,
roles: { some: { role: { permissions: { some: { permission: { resource, action } } } } } },
},
});
if (andere === 0) {
throw new Error(
`Dieses Konto ist das letzte, das „${wofuer}" kann (${recht}). ` +
'Ohne es liesse sich das System nur noch über die Kommandozeile ' +
'wieder in Ordnung bringen. Bitte zuerst einem anderen aktiven Konto ' +
'die entsprechende Rolle geben.',
);
}
}
}
export async function createUser(data: {
email: string;
password: string;
@@ -324,74 +397,31 @@ export async function updateUser(
},
});
const isCurrentlyAdmin = currentUser?.roles.some((ur) =>
ur.role.permissions.some(
(rp) => rp.permission.resource === 'users' && rp.permission.action === 'delete'
)
);
// Geschuetzt wird die FAEHIGKEIT, nicht ihr ueblicher Traeger -
// siehe UNVERZICHTBARE_FAEHIGKEITEN.
const bisherigeRechte = rechteAusRollen(currentUser?.roles ?? []);
if (isCurrentlyAdmin) {
// Check if user will still be admin after role change
let willStillBeAdmin = false;
if (rolesAreBeingChanged) {
const newRoles = await prisma.role.findMany({
where: { id: { in: gepruefteRollenIds! } },
include: {
permissions: {
include: { permission: true },
},
},
});
willStillBeAdmin = newRoles.some((role) =>
role.permissions.some(
(rp) => rp.permission.resource === 'users' && rp.permission.action === 'delete'
)
);
} else {
willStillBeAdmin = true; // Roles not being changed
}
// If user is losing admin status or being deactivated, check for other admins
if (!willStillBeAdmin || isBeingDeactivated) {
const otherAdminCount = await prisma.user.count({
where: {
id: { not: id },
isActive: true,
roles: {
some: {
role: {
permissions: {
some: {
permission: {
resource: 'users',
action: 'delete',
},
},
},
},
},
},
},
});
if (otherAdminCount === 0) {
if (isBeingDeactivated) {
throw new Error(
'Dieser Benutzer ist der letzte Administrator und kann nicht deaktiviert werden'
);
} else {
throw new Error(
'Die Admin-Rolle kann nicht entfernt werden, da dies der letzte Administrator ist'
);
}
}
}
let kuenftigeRechte: Set<string> | null;
if (isBeingDeactivated) {
// Ein deaktiviertes Konto kann nichts mehr, egal welche Rollen formal
// drankleben.
kuenftigeRechte = null;
} else if (rolesAreBeingChanged) {
const neueRollen = await prisma.role.findMany({
where: { id: { in: gepruefteRollenIds! } },
include: { permissions: { include: { permission: true } } },
});
kuenftigeRechte = rechteAusRollen(neueRollen.map((role) => ({ role })));
} else {
kuenftigeRechte = bisherigeRechte;
}
await pruefeLetzterVerwalter(id, bisherigeRechte, kuenftigeRechte);
}
// Hash password if provided
if (password) {
(userData as Record<string, unknown>).password = await bcrypt.hash(password, 10);
(userData as Record<string, unknown>).password = await bcrypt.hash(password, 12);
}
// Prüfen ob Rollen geändert werden (für Zwangslogout)
@@ -549,42 +579,9 @@ export async function deleteUser(id: number) {
throw new Error('Benutzer nicht gefunden');
}
// Check if user has admin permissions (users:delete means admin)
const isAdmin = user.roles.some((ur) =>
ur.role.permissions.some(
(rp) => rp.permission.resource === 'users' && rp.permission.action === 'delete'
)
);
if (isAdmin) {
// Count other admins (users with users:delete permission)
const adminCount = await prisma.user.count({
where: {
id: { not: id },
isActive: true,
roles: {
some: {
role: {
permissions: {
some: {
permission: {
resource: 'users',
action: 'delete',
},
},
},
},
},
},
},
});
if (adminCount === 0) {
throw new Error(
'Dieser Benutzer ist der letzte Administrator und kann nicht gelöscht werden'
);
}
}
// Dasselbe Kriterium wie beim Bearbeiten: Geschuetzt wird die Faehigkeit,
// das System zu verwalten - nicht die Rolle, die sie ueblicherweise traegt.
await pruefeLetzterVerwalter(id, rechteAusRollen(user.roles), null);
return prisma.user.delete({ where: { id } });
}
+25 -3
View File
@@ -8,11 +8,33 @@
* obwohl die Fehlermeldung "Dokument vor wenigen Sekunden bereits
* angelegt" eindeutig eine 400-Class-Situation ist.
*/
export class ApiError extends Error {
/**
* Basisklasse fuer Fehler, deren Wortlaut fuer den AUFRUFER bestimmt ist.
*
* Der Unterschied ist der Kern von R190-01 und R192-01: Bis 09/2026 reichten
* die Controller jede `error.message` durch. Damit landete erst der
* vollstaendige Prisma-Aufruf samt Serverpfad beim Client und dann der
* Wortlaut eines TypeError. Beide Male dieselbe Ursache - eine Meldung, die
* fuer Entwickler geschrieben ist, an einen Empfaenger, fuer den sie nicht
* gedacht war.
*
* Ab hier gilt: Was von dieser Klasse abstammt (oder ein blankes `Error` ist,
* das wir absichtlich geworfen haben), geht nach draussen. Alles andere -
* TypeError, Prisma, was auch immer - wird protokolliert und durch eine
* allgemeine Auskunft ersetzt.
*/
export class FachlicherFehler extends Error {
readonly statusCode: number;
constructor(statusCode: number, message: string) {
constructor(message: string, statusCode = 400) {
super(message);
this.name = 'ApiError';
this.name = new.target.name;
this.statusCode = statusCode;
}
}
export class ApiError extends FachlicherFehler {
constructor(statusCode: number, message: string) {
super(message, statusCode);
this.name = 'ApiError';
}
}
+54
View File
@@ -0,0 +1,54 @@
/**
* Eine Stelle, an der entschieden wird, was ein Fehler dem Aufrufer sagt.
*
* Vorher stand in 26 Controllern 124-mal dieselbe Zeile:
*
* error: error instanceof Error ? error.message : 'Irgendein Fallback'
*
* Das reicht JEDE Fehlermeldung durch - auch die, die fuer Entwickler
* geschrieben sind. Konkret gefunden wurden ein vollstaendiger Prisma-Aufruf
* samt Serverpfad (R190-01) und der Wortlaut eines TypeError (R192-01). Kein
* Loch fuer sich genommen, aber eine Landkarte: Pfade, Spaltennamen,
* eingesetzte Bibliotheken.
*
* 124 Einzelkorrekturen waeren die falsche Antwort gewesen - das ist die
* Falle aus R186-01 und R188, wo dieselbe Regel in mehreren Kopien lebte und
* auseinanderlief. Deshalb eine Funktion.
*
* Die Unterscheidung laeuft ueber die Fehlerklasse, nicht ueber eine
* Heuristik auf dem Text:
*
* - `FachlicherFehler` (und Abkoemmlinge wie `ApiError`) - absichtlich
* geworfen, Wortlaut fuer den Aufrufer, eigener Statuscode.
* - blankes `Error` - ebenfalls absichtlich geworfen, historisch die
* ueblichste Form ("Der letzte Admin kann nicht..."). Geht mit dem
* uebergebenen Status hinaus.
* - alles andere (TypeError, RangeError, Prisma...) - Programmierfehler oder
* Fremdfehler. Geht als 500 mit der allgemeinen Auskunft hinaus, die
* Einzelheiten ins Protokoll. 500 und nicht 400, weil ein
* Eingabefehler-Code hier die naechste Meldung waere, die sich falsch
* ausgibt.
*/
import { Response } from 'express';
import { FachlicherFehler } from './apiError.js';
export function antworteAufFehler(
res: Response,
error: unknown,
fallback: string,
status = 400,
): void {
if (error instanceof FachlicherFehler) {
res.status(error.statusCode).json({ success: false, error: error.message });
return;
}
if (error instanceof Error && error.constructor === Error) {
res.status(status).json({ success: false, error: error.message });
return;
}
console.error(`[${fallback}] unerwarteter Fehler:`, error);
res.status(500).json({ success: false, error: fallback });
}
+63
View File
@@ -102,6 +102,69 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
- [x] **🧹 Aufräumrunde vor Etappe 2: drei Nachrangpunkte** (2026-09-09)
**1. R192-01 projektweit — interne Fehlermeldungen an den Client**
- Das Muster `error instanceof Error ? error.message` stand **124-mal in 26
Controllern**. Jede dieser Stellen konnte Serverpfade, Spaltennamen und
Bibliotheksinterna ausliefern — kein Loch für sich, aber eine Landkarte.
- Zentral gelöst statt 124-mal einzeln (die Falle aus R186-01/R188):
`src/utils/fehlerAntwort.ts` mit `antworteAufFehler()`. Jetzt **123
Aufrufe in 26 Dateien**, eine Regel.
- Die Unterscheidung läuft über die **Fehlerklasse**: Neue Basisklasse
`FachlicherFehler` (in `utils/apiError.ts`) für alles, dessen Wortlaut
für den Aufrufer bestimmt ist — `ApiError`, `RechteEskalationError` (403),
`RollenSperrError` (403), `UngueltigeEingabeError` (400), `FilterFehler`,
`ReferralError` stammen jetzt davon ab. Ein blankes `Error` gilt weiter
als absichtlich. Alles andere (TypeError, Prisma, JWT-Bibliothek) → **500**
mit allgemeiner Auskunft, Einzelheiten ins Protokoll.
- Mit gefunden: Sechs Stellen in `cachedEmail.controller.ts` **interpolierten**
die interne Meldung in den Antworttext (`Fehler beim Speichern: ${msg}`) —
die hätte kein Klassenfilter erwischt, der nur das Feld ersetzt.
Und `POST /auth/refresh` gab den Wortlaut der JWT-Bibliothek zurück
(„jwt malformed", „invalid signature") — der sagt einem Angreifer, **woran**
sein Token gescheitert ist. Geht jetzt nur noch in den Alarmkanal.
**2. Admin-Heuristik — geschützt wird jetzt die Fähigkeit, nicht ihr Träger**
- Bisher galt „wer `users:delete` hat, ist Admin". Ein Zufallsmerkmal: Wer
Konten anlegen und bearbeiten darf, aber nicht löschen, verwaltet genauso —
zählte aber nicht.
- **Bewusst nicht auf die Rolle „Admin" umgestellt**, wie zunächst
vorgeschlagen: Eine selbst gebaute Rolle mit `users:update` verwaltet
tatsächlich, ein Namenskriterium würde sie übersehen — man könnte dann den
letzten Admin löschen, obwohl die Verwaltungsfähigkeit erhalten bliebe.
- Neu: `UNVERZICHTBARE_FAEHIGKEITEN` = `users:update` („Benutzer verwalten")
und `roles:manage` („Rollen und Rechte pflegen"). Wer die letzte Person mit
einer davon ist, kann sie nicht verlieren — durch Rollenwechsel,
Deaktivierung oder Löschung. Die Meldung nennt die Fähigkeit beim Namen
statt „letzter Administrator".
- Nachgeprüft: Mit drei Trägern ist alles erlaubt; ist einer der letzte,
→ 400 mit Klartext. Deaktivierung zählt als Verlust (ein inaktives Konto
kann nichts, egal welche Rollen drankleben).
- Nebenbei: `updateUser` hashte beim Passwort-Zurücksetzen noch mit Cost 10 —
der letzte Rest der Inkonsistenz aus R191-01.
**3. Portal-Kunden — Trennung festgenagelt, nicht aufgelöst**
- Korrektur meiner eigenen Einordnung: Ich hatte das als „noch zu migrieren"
geführt. Falsch. Kunden bekommen **niemals** operative Rechte; die
Portalansicht ist dafür nicht gebaut und prüft es nicht.
- Die zwei Kopien des festen Arrays in `auth.service.ts` sind jetzt eine
Konstante `PORTAL_RECHTE` mit einem Kommentar, der die Trennung als
Absicht benennt.
- **Harter Riegel im Gate:** `requirePermission` schneidet die Rechte eines
Portal-Zugangs auf `PORTAL_RECHTE` zu — unabhängig davon, was sein Token
behauptet. Heute wirkungslos (sie halten ohnehin nur diese zwei), morgen
die Sicherung: Bisher hätte eine unbedachte Zeile in der Token-Erzeugung
gereicht, um die Trennung zu kippen.
- **Startwache:** `pruefePortalRechte()` meldet beim Start jedes Recht in
`PORTAL_RECHTE`, das nicht Lesen ist. Damit überlebt die Regel den Tag, an
dem jemand „nur kurz" etwas hinzufügt.
- Dateien: `utils/fehlerAntwort.ts` (neu), `utils/apiError.ts`,
`config/rechte-katalog.ts`, `middleware/auth.ts`, `services/auth.service.ts`,
`services/user.service.ts`, `services/rechte.service.ts`,
`services/pflichtrechte.service.ts`, 26 Controller
- [x] **🚨 R193-02: Truthiness-Bypass bei den drei Haken (HIGH)** (2026-09-09)
- Befund der Pentesterin, **Prod-Blocker**: Der Eskalations-Guard fragte
`=== true`, die Zuweisung fragte auf Truthiness. Zwischen diesen beiden