Commit Graph
100 Commits
Author SHA1 Message Date
duffyduckandClaude Opus 4.8 cb21a2c9be Mass-Assignment-Schutz: Bankkarte/Adresse/Ausweis (Pentest R155)
Die create/update-Services reichten rohen req.body an Prisma durch, wodurch
customerId (Owner) und id (PK) per Update mutierbar waren - staff-only, kein
Cross-Tenant-Bruch, aber echte Integritaetsschwaeche; mit dem neuen
cardNumber-Feld liegt zudem Finanz-PII auf dieser Flaeche.

Fix: explizite Feld-Whitelist im Service (create+update) fuer BankCard,
Address und IdentityDocument - nur benannte Spalten gehen an Prisma, kein
...data/req.body-Spread mehr. Controller-Helper pickBankCardFields haelt
zusaetzlich die Audit-Logs sauber (keine Phantom-Eintraege injizierter Keys).

Verifiziert: updateBankCard mit {customerId:99999, id:88888, bogusField, ...}
-> id+customerId unveraendert, nur cardNumber gesetzt, Fremdfelder ignoriert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 16:40:15 +02:00
duffyduckandClaude Opus 4.8 0d42bd89e8 Bankkarte-/Ausweis-Details in Vertrag + neues Feld Kartennummer
Schema: BankCard.cardNumber (String?, optional) + Migration
20260813100000_bank_card_number (ADD COLUMN IF NOT EXISTS), auf Dev
angewandt + prisma generate; Prod via migrate deploy. Eingabefeld
"Kartennummer" im Bankkarten-Modal (Kundenakte).

Vertragsansicht (ContractDetail) und Vertrag bearbeiten (ContractForm)
zeigen jetzt bei Bankkarte zusaetzlich BIC/Bank/Kartennummer/Ablaufdatum
und bei Ausweis Behoerde/Ausstellung/Ablaufdatum sowie Geburtsort +
Geburtsdatum des Kunden - jeweils mit Copy-Button und nur wenn gesetzt.
Im Form je Select in eigenem div gewrappt (Grid-Alignment).

Verifiziert: tsc + vite build gruen, cardNumber Round-Trip (update->read),
Contract-Include liefert alle Felder inkl. customer.birthDate/birthPlace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 15:00:29 +02:00
duffyduckandClaude Opus 4.8 f3668b8a5a Magic-Byte-Seed: aus Prod-Image ausschliessen + Prod-Riegel
Der Pentester-Helfer soll nicht ins Produktions-Image und darf nie gegen
Prod laufen:
- .dockerignore schliesst backend/scripts/seed-magic-byte-test.ts aus dem
  Build-Context aus -> Datei liegt nicht mehr im Prod-Container (bleibt im
  Repo fuer Dev/Staging-Checkout).
- Script bricht bei NODE_ENV=production im "create"-Modus hart ab (exit 1);
  "cleanup" bleibt erlaubt, damit man immer aufraeumen kann.

Verifiziert: create@production -> Abbruch exit 1; cleanup@production laeuft.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 13:49:32 +02:00
duffyduckandClaude Opus 4.8 2ea211cca0 Pentester-Helfer: Seed-Script fuer Magic-Byte-Mismatch-Test (R152)
backend/scripts/seed-magic-byte-test.ts legt eine dedizierte TEST-Bankkarte
(Marker im accountHolder) mit einer getarnten Datei an: .pdf-Endung, aber
SVG-mit-<script>-Inhalt (Non-Whitelist-Magic-Byte, Stored-XSS-Payload).
Damit kann der Pentester den Magic-Byte-Mismatch->attachment-Zweig des
Download-Endpoints live ausloesen (auf Staging fehlte bisher ein
Non-Whitelist-Upload). Echte Kundendaten werden nicht angefasst; cleanup
entfernt Karte + Datei.

Modi: create [--customer <id>] | cleanup.

Verifiziert (create -> Controller-Integrationstest -> cleanup): inline
angefragt -> Content-Disposition attachment + nosniff (nicht inline) + Log;
ohne disposition -> attachment; fremder Portal-User -> 403.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 13:31:08 +02:00
duffyduckandClaude Opus 4.8 06d51cf680 PDF-Viewer-Modal fuer Bankkarte-/Ausweis-Dokument
Neue Komponente PdfViewerModal (Modal + iframe auf viewUrl(documentPath),
inkl. Neuer-Tab/Download-Links). Buttons:
- Vertragsansicht (ContractDetail): im Card-Header von Bankkarte/Ausweis
- Vertrag bearbeiten (ContractForm): neben dem Label Bankkarte/Ausweis

Button erscheint NUR wenn ein documentPath hinterlegt/gewaehlt ist. Nutzt den
bestehenden /api/files/download-Endpoint (Per-File-Ownership-Check) - kein
neuer Zugriffspfad. iframe (nicht embed/object wegen object-src 'none');
same-origin via CSP default-src 'self' erlaubt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 13:15:57 +02:00
duffyduckandClaude Opus 4.8 b2b1cb3387 Vertragssuche: Karteninhaber (cardUser) durchsuchbar (Pentest R150)
getAllContracts durchsucht jetzt auch simCards.cardUser - zusaetzlich zu
Rufnummer/SIM-Nummer/IMEI. R150-Randnotiz, fachlich gewuenscht (Staff sucht
nach Karteninhaber). Portal-Suche bleibt durch das bestehende customerIds-
Scoping begrenzt.

Verifiziert: Suche nach gesetztem cardUser findet den Vertrag, Kontroll-
Suche 0 Treffer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 12:50:36 +02:00
duffyduckandClaude Opus 4.8 2d12221c41 Vertragsliste (Staff-Tabelle): Karteninhaber/Netz/Kuendigung nachgezogen
Der vorige Commit ergaenzte nur die Baum-Render-Pfade (Kundenakte + Portal).
Die Staff-Ansicht unter /contracts rendert aber eine eigene Tabelle aus der
flachen getAll-Liste - dort fehlten die neuen Felder noch. Jetzt zeigt auch
die Staff-Tabelle Karteninhaber + Netz hinter der Rufnummer und die
Kuendigungsbestaetigung in roter Schrift. Backend liefert die Felder via
getAllContracts bereits mit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 12:26:58 +02:00
duffyduckandClaude Opus 4.8 c5cf2c7416 Vertragslisten: Karteninhaber + Mobilfunknetz + Kuendigung (rot)
Beide Vertragslisten (Kundenakte-Baum + Hauptmenue /contracts) zeigen bei
Mobilfunkvertraegen mit Rufnummer jetzt zusaetzlich Karteninhaber
(SimCard.cardUser der angezeigten SIM, nur wenn gesetzt) und Netz
(mobileNetwork -> Telekom/Vodafone/Telefonica (o2)) inline. Vertraege mit
Kuendigungsbestaetigung (cancellationConfirmationDate) bekommen eine eigene
Zeile in roter, fetter Schrift.

Backend: getContractTreeForCustomer + getAllContracts selektieren nun
mobileNetwork + cardUser (Tree zusaetzlich cancellationConfirmationDate).
Shared-Helper getContractTypeInfo um cardUser/network + mobileNetworkLabel
erweitert. Beide Listen rendern ueber den Tree-Endpoint.

Verifiziert: tsc + vite build gruen, Tree-Service liefert die Felder,
Helper-Mapping getestet (Vodafone/Telekom/o2, leerer Karteninhaber ausgelassen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 12:18:24 +02:00
duffyduckandClaude Opus 4.8 8e46dbbfed Globaler /api-Rate-Limit-Backstop (Pentest R148)
Es gab bislang keinen generellen Limiter - nur Login/Passwort-Reset/
Staff-ReAuth/Consent; authentifizierte Endpoints waren gegen Enumeration/
DoS ungedrosselt. Neuer apiBackstopRateLimiter auf alle /api-Requests,
vor den Routern gemountet (ergaenzt die feineren Limiter, ersetzt sie nicht).

Key = nur IPv6-/56-normalisierte IP - bewusst nicht IP+User, da der User-
Claim hier nur unverifiziert lesbar waere (authenticate laeuft erst pro
Route) und ein Angreifer sonst per Fake-userId beliebig Buckets erzeugen
koennte. Limit per Env API_RATE_LIMIT_PER_MIN (Default 1200/min, Floor 60),
/api/health ausgenommen. Kein SecurityEvent pro Block (Flood-Amplification).
Deckt zugleich den offenen IPv6-Rate-Limit-Test ausserhalb der Auth-Pfade ab.

Verifiziert: 60x200 dann 429, health bleibt 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 14:54:12 +02:00
duffyduckandClaude Opus 4.8 955fceb3b8 BLZ-Guard: vollstaendige Formatpruefung aller Eintraege (Pentest R148)
Der Poisoning-Guard pruefte bisher nur die erste current-Zeile - ein Set mit
1 echten + 999 Fake-Eintraegen kaeme durch. Jetzt wird JEDER Eintrag in
current + next.upsert geprueft (8-stellige BLZ, Wert [Name] oder [Name,BIC]),
next.remove auf 8-stellige BLZ, next.valid auf ein gueltiges Datum.

Dabei korrekt beruecksichtigt: Banken ohne BIC haben nur [Name] (Laenge 1) -
lookupBlz liefert dann bic:''. Real gegen den echten Datensatz verifiziert
(3506 Eintraege, inkl. BIC-lose wie BLZ 60050009).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 14:46:56 +02:00
duffyduckandClaude Opus 4.8 d0619141bb Fix: Modal-Formulare nach Abbrechen nicht mehr mit alten Daten vorbefuellt
Die Modals in der Kundenakte (Bankkarte, Adresse, Ausweis, Zaehler,
Zaehlerstand) bleiben dauerhaft gemountet und wurden nur ueber isOpen
umgeschaltet. Der Reset-Effekt haengte an <entity>?.id - bei Neuanlage
immer undefined, also kein Reset beim erneuten Oeffnen: nach "Abbrechen"
standen die vorher getippten Daten noch drin.

Jetzt Reset beim Oeffnen (Guard if(!isOpen), Deps [isOpen, <entity>?.id]),
weiterhin kein Reset bei jedem Tastendruck. Ausserdem den fehlerhaften
useState(()=>{})-Init-Missbrauch im Bankkarten-Modal entfernt.
StressfreiEmail-/AdditionalForwards-Modal waren bereits korrekt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 14:31:03 +02:00
duffyduckandClaude Opus 4.8 a28c8355fd BLZ-Updater: Versionsformat vor URL-Interpolation strikt pruefen
Die von der npm-Registry gelieferte "latest"-Version wird in die jsDelivr-
CDN-URL interpoliert. Nun nur noch rein numerisches semver (^\d.\d.\d$)
zugelassen, damit eine manipulierte Registry-Antwort (../, Slashes, Query)
den Pfad nicht verbiegen kann. Host bleibt ohnehin fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 14:09:24 +02:00
duffyduckandClaude Opus 4.8 91fbe12299 BLZ-Bankdaten: Laufzeit-Auto-Update ins Volume + Einstellungen-Seite
Statt npm-Rebuild-Wartung aktualisiert sich der Bankleitzahlen-Datensatz
jetzt zur Laufzeit. Ein Scheduler (taeglich 03:30 + Catch-up 90s) prueft
gemaess konfigurierbarem Intervall und laedt current.json/next.json von
npm/jsDelivr (Paket bankdata-germany) in das neue Bind-Mount-Volume
BANKDATA_DIR (./data/bankdata -> /app/bankdata).

Lookup bevorzugt den Volume-Datensatz vor den ins Image gebackenen Daten
(Fallback). Es wird kein Fremdcode ausgefuehrt - nur JSON gelesen und die
current+next-Delta-Logik nachgebaut. Validierung (>=1000 Eintraege, Format)
+ atomarer Write (tmp+rename) schuetzen den guten Stand vor Muell.

Datenschutz: Der Updater sendet keine Kundendaten, laedt nur eine
oeffentliche Datendatei; abschaltbar; bei Fehler/ohne Egress greift Builtin.

Neue Einstellungen-Seite /settings/bank-data zeigt Datenstand, Update-
Verfuegbarkeit und bietet "Jetzt aktualisieren" + Auto-Update-Schalter +
Intervall. Endpoints GET /api/settings/blz, POST /api/settings/blz/update-now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 13:15:07 +02:00
duffyduckandClaude Opus 4.8 73bd72bd9f Bankkarte: IBAN pruefen + BIC/Bank offline aus Bundesbank-BLZ ausfuellen
Neuer Button "BIC & Bank aus IBAN abrufen" im Bankkarten-Modal fuellt BIC +
Banknamen automatisch und validiert dabei die IBAN-Pruefziffer (mod-97).
Leeres IBAN-Feld -> OK-Messagebox statt Anfrage.

Datenschutzfreundlich/offline: kein Dritt-Dienst. Nachschlag im eigenen
Backend ueber die Bundesbank-Bankleitzahlendatei (bankdata-germany) +
ibantools fuer die Pruefziffer. Die IBAN verlaesst nie den Server; zurueck
kommen nur oeffentliche Bankverzeichnis-Daten.

Endpoint: POST /api/bank-cards/iban-lookup (nur eingeloggt).
Wartung: bankdata-germany/ibantools ~quartalsweise per npm update ziehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 12:32:53 +02:00
duffyduckandClaude Opus 4.8 aff3f7e84a Update-Banner: klebt oben fest (sticky), Sticky-Header rasten darunter ein
Der Hinweis "Neue Version verfuegbar" scrollte bisher weg. Jetzt sticky an
der Viewport-Oberkante; das Banner meldet seine gemessene Hoehe als CSS-Var
--app-banner-h, an der sich die Sticky-Header von ContractDetail/ContractForm
ausrichten (top-[var(--app-banner-h,0px)]), damit sie nicht dahinter
verschwinden.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 12:20:31 +02:00
duffyduckandClaude Opus 4.8 e1ca56ebfb getCockpit fail-closed (R146) - konsistent zu listAll/getContracts
Pentester R146 (Konsistenz + Defense-in-Depth): getCockpit war der
einzige der drei Geschwister-Endpunkte noch auf dem alten Fail-open-
Muster. Der Cockpit-SERVICE ist bereits fail-closed (customerIds:[] ->
IN () -> 0), aber der CONTROLLER uebergab bei falsy customerId
undefined statt [] -> {} -> alle Kunden. Genau die R4-HIGH-Stelle
'Cockpit leakt alle Vertraege'.

Jetzt: Portal-Token wird IMMER gescoped (ohne customerId -> []),
identischer Einzeiler wie listAll/getContracts. Nicht erreichbar (Token
traegt immer customerId), aber der urspruengliche HIGH-Endpunkt soll
nicht das letzte Fail-open-Muster bleiben.

Verifiziert: Cockpit Staff -> 17 Vertraege; Portal []->0 (contracts +
cancellationConfirmations).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 11:44:52 +02:00
duffyduckandClaude Opus 4.8 8d93c1767b R145-Hygiene: limit-Floor + getContracts fail-closed (Konsistenz)
Pentester R145 (nice-to-have, keine Findings):
1) listAll: limit bekommt einen Floor (Math.max(1, ...)) - limit=-5
   ergab vorher take:-5 an Prisma. page ebenso auf >=1 geklemmt.
2) getContracts nutzte dasselbe 'if (isCustomerPortal && customerId)'-
   Muster und war NICHT fail-closed. Jetzt konsistent zu listAll:
   - Controller: Portal-Token immer gescoped (ohne customerId -> []).
   - Service getAllContracts: 'if (customerIds)' statt '.length > 0',
     damit ein leeres Array strikt auf IN () filtert (0 Treffer) statt
     durchzufallen. Einziger Caller ist der Contract-Controller ->
     keine Regression fuer den Normalfall.

Verifiziert: Staff -> alle; Portal customerIds=[] -> 0 Vertraege/Belege.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 11:32:21 +02:00
duffyduckandClaude Opus 4.8 4c5548ea3c Belegübersicht: fail-closed Portal-Scoping (R144)
Pentester R144 (fail-closed, kein Finding): listAll scopte ueber
'if (isCustomerPortal && customerId)'. Fiele customerId bei einem
Portal-Token mal falsy aus, rutschte er in den Staff-Zweig (alle
Belege).

Jetzt: Portal-Token wird IMMER gescoped; ohne customerId -> leere
Menge (customerIds=[]) statt undefined/Staff. In:[] kann nie matchen.
Aktuell nicht erreichbar (Portal-Token traegt immer customerId), aber
robuster.

Verifiziert: customerIds=[] -> 0 Belege.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 11:18:46 +02:00
duffyduckandClaude Opus 4.8 48e65be91c Hauptmenue: Gutschriften/Lieferscheine-Gesamtuebersicht (portal-scoped)
Neuer Menuepunkt 'Gutschriften' -> Seite /credit-notes mit Tabelle
aller Belege (Beleg-Nr, Art, Kunde, Vertrag, Betrag, Datum, PDF),
Suche + Pagination.

Neuer Endpoint GET /credit-notes (NICHT staff-only wie die uebrigen
Credit-Note-Endpoints): Staff sieht alle Belege aller Kunden, Portal-
Kunden nur eigene + vertretene (Vollmacht via hasAuthorization).
customerIds kommt aus dem JWT, nicht aus Query/Body -> nicht
manipulierbar. Fuer Portal wird receiptPath aus der Response entfernt
(Belege bleiben staff-only). Route requirePermission contracts:read.

Verifiziert: Staff -> alle Belege; Portal-scoped -> nur eigene, korrekt
zugeordnet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 10:23:53 +02:00
duffyduckandClaude Opus 4.8 bbe1aed230 Portal-Passwort: encrypted ohne Hash gilt jetzt als desync (R143)
Pentester R143 (Robustheit): getCustomerPortalPassword lieferte 'ok',
wenn portalPasswordEncrypted gesetzt, aber portalPasswordHash null ist.
Ohne Hash ist ein Login unmoeglich -> ein revealtes Passwort waere
irrefuehrend. Jetzt -> desync (Reveal/Send blocken mit 409). Aktuell
ueber die API nicht erreichbar, nur als DB-Altlast; jetzt sauber
abgefangen.

Verifiziert: encrypted+hash=null -> desync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 10:06:00 +02:00
duffyduckandClaude Opus 4.8 5047bfaab3 Gutschrift: nur mit Empfaengeradresse anlegbar (Rechnung > Liefer)
Eine Gutschrift/ein Lieferschein braucht eine Empfaengeradresse aufs
Dokument. Rechnungsadresse hat Vorrang, sonst Lieferadresse. Ist keine
von beiden hinterlegt -> Anlegen blockiert.

- Frontend: Klick auf 'Gutschrift anlegen' prueft defaults.hasRecipient-
  Address; wenn false -> Modal-OK-Meldung statt Formular.
- Backend Defense-in-Depth: createCreditNote wirft 400, wenn weder
  billingAddressId noch addressId gesetzt. getCreditNoteDefaults liefert
  hasRecipientAddress.

Verifiziert: ohne Adresse -> hasRecipientAddress false + create 400;
mit Adresse -> ok.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 09:39:04 +02:00
duffyduckandClaude Opus 4.8 0078654465 Gutschrift: Beleg-Upload auch fuer Sachwerte + kein Unterschriftsblock bei Geld
- Beleg-Upload jetzt fuer beide Arten: bei Geld die Ueberweisungs-
  bestaetigung, bei Sachwert das unterschriebene Dokument. ReceiptControls
  in der Liste fuer Geld UND Sachwert (Label je nach Typ). Endpoint war
  schon typ-agnostisch.
- PDF-Unterschriftsblock nur noch bei Sachwert - eine Ueberweisung wird
  nicht unterschrieben (Beleg = hochgeladene Ueberweisungsbestaetigung).
  Bei Geld entfaellt der Unterschrift/Ort-Block; im Formular sind Ort +
  'Unterschrift am' bei Geld ausgeblendet.

Verifiziert: beide PDFs erzeugen sauber (je 1 Seite).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 09:26:35 +02:00
duffyduckandClaude Opus 4.8 5e606df49e Gutschrift: separater Lieferschein-Nummernkreis (GoBD, luecken-frei)
Pentester R142: Uebergang Geld->betragsloser Sachwert setzte die schon
vergebene Gutschriftsnummer auf null -> Luecke in der GS-Serie.

Loesung: betragsloser Sachwert = Lieferschein mit eigener
Lieferscheinnummer aus separatem Nummernkreis.

- Schema: CreditNote.deliveryNoteNumber (nullbar, unique) + neues Model
  DeliveryNoteNumberRange (Default-Praefix 'LS-') + Migration.
- deliveryNoteNumberRange.service (mirror, eigener Zaehler, FOR UPDATE).
- Nummern lazy pro Serie, NIE freigeben: Uebergaenge behalten die
  jeweils vergebene Nummer der anderen Serie reserviert -> kein
  Doppelverbrauch, keine Luecke. effectiveNumber() liefert je nach Typ
  die passende (LS/GS) fuer Anzeige/PDF/Audit.
- Endpunkte GET/PUT /credit-notes/delivery-note-number-range; Settings-
  Seite verwaltet jetzt beide Nummernkreise. PDF-Titel 'Sachwert-
  Uebergabe', Dateiname lieferschein-...
- Frontend: Typ + displayNumber in Liste/Modal.

Verifiziert: Sachwert 0 -> LS-Nr, GS-Zaehler unberuehrt; Geld -> GS-Nr;
Uebergaenge behalten beide Nummern (kein Neuverbrauch, keine Luecke).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 08:47:49 +02:00
duffyduckandClaude Opus 4.8 79f6f3e629 Portal-Passwort: Reveal/Send prueft Konsistenz gegen Login-Hash
Pentester-Hinweis: bcrypt-Hash (Login) und verschluesseltes Reveal-Feld
koennen out-of-sync sein -> Support liest ein Passwort vor, das beim
Login scheitert.

Analyse: alle aktuellen Schreibpfade sind konsistent (beide Felder
zusammen, oder encrypted=null, oder Rehash desselben Passworts) - der
Code erzeugt keinen Desync. Ursache = Altlast/manueller DB-Eingriff.

Fix (defensiv, unabhaengig von der Ursache):
- getCustomerPortalPassword liefert {status: ok|none|desync} und prueft
  den entschluesselten Klartext per bcrypt.compare gegen den Login-Hash.
- Bei desync (oder Entschluesselungsfehler) geben WEDER Reveal NOCH
  Send-Credentials das Passwort aus -> 409 'Dateninkonsistenz, bitte
  neu setzen'. Reveal-Read wird mit Status auditiert.
- Neues Diagnose-Script scripts/check-portal-password-sync.ts scannt
  alle Portal-Kunden auf Desync (nur Diagnose, aendert nichts) - fuer
  Prod, da der Pentester keinen FS-Zugriff hat.

Verifiziert: desync -> nicht ausgegeben; konsistent -> ok; kein PW ->
none. Scan laeuft (0 Desync auf Dev).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-11 22:46:53 +02:00
duffyduckandClaude Opus 4.8 15ac003dad Gutschrift: betragsloser Sachwert bekommt keine Gutschriftsnummer
Ein betragsloser Sachwert ist eher ein Lieferschein als eine Gutschrift
-> er soll KEINE Gutschriftsnummer aus dem Nummernkreis verbrauchen.

- Schema: CreditNote.number nullbar (Migration MODIFY ... NULL, UNIQUE
  bleibt - MySQL erlaubt mehrere NULLs).
- createCreditNote: betragsloser Sachwert -> number=null, assignNextNumber
  wird NICHT aufgerufen (Zaehler unangetastet).
- updateCreditNote: Uebergaenge - wird betragslos -> Nummer entfernen;
  bekommt nachtraeglich Betrag & hatte keine -> jetzt Nummer vergeben.
- PDF/Liste/Modal/Audit: Fallback 'Sachwert-Uebergabe'/'Beleg #id' wenn
  keine Nummer; PDF-Titel 'Sachwert-Uebergabe', kein ZUGFeRD (schon vorher).

Verifiziert: Sachwert 0 -> number null + Zaehler bleibt; Geld -> Nummer
+ Zaehler +1; Sachwert nachtraeglich mit Betrag -> Nummer vergeben.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-09 17:40:15 +02:00
duffyduckandClaude Opus 4.8 6f95005530 Gutschrift: Sachwert ohne Betrag = keine Rechnung (kein ZUGFeRD)
Eine Sachwert-Gutschrift darf betragslos sein (Betrag leer/0): dann
findet keine Rechnungsstellung statt - der Kunde hat den Gegenstand
einfach als Subvention erhalten.

- Backend: leeres amount -> 0; Sachwert erlaubt 0, Geld verlangt > 0
  (400 sonst). vatRelevant bei 0 erzwungen-false.
- PDF: betragsloser Sachwert -> Titel 'Sachwert-Uebergabe', kein
  Betrags-/USt-Block (Hinweis keine Rechnungsstellung), KEIN ZUGFeRD-
  Embedding. Mit Betrag -> unveraendert ZUGFeRD.
- Frontend: Wert-Feld bei Sachwert optional; USt-Block ausgeblendet ohne
  Betrag; Liste zeigt 'Sachwert ohne Betrag (keine Rechnung)'.

Verifiziert: Sachwert 0 -> kein factur-x.xml; Geld 0 -> 400;
Sachwert 200 -> ZUGFeRD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-09 13:22:13 +02:00
duffyduckandClaude Opus 4.8 4d14390b2a Cockpit-Filter Kuendigungsbestaetigung: Status EXPIRED mitzaehlen
Verträge mit hinterlegter Kuendigungsbestaetigung im Status EXPIRED
(Abgelaufen) werden jetzt auch im Cockpit-Filter gelistet, zusaetzlich
zu ACTIVE/DRAFT/CANCELLED. Die Cockpit-Query laedt EXPIRED ohnehin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 23:42:31 +02:00
duffyduckandClaude Opus 4.8 3e2d9395a7 Hygiene R140: Datei-Loesch-Helfer konsolidieren + DB-vor-Datei
Pentester-Hygiene zu a6b1dac:

1) Konsolidierung: neuer utils/fileCleanup.ts mit deleteFileAbsolute
   (absoluter Pfad, z.B. Multer-Temp) + deleteUploadByRelativePath
   (in DB gespeicherter /uploads/-Pfad). Ersetzt die 3x kopierten
   deleteFileIfExists/cleanupFile in creditNote-, upload- und
   customer-Service.

2) Reihenfolge: In deleteCreditNote/updateCreditNote erst die DB-
   Operation, DANN die Datei loeschen. Schlaegt der DB-Schritt fehl,
   bleibt die Datei erhalten (kein ins-Leere-zeigender Zustand).

Verifiziert: Update -> pdfPath null + alte Datei weg; Delete -> gibt
geloeschte Row zurueck (Audit) + Datei weg. Kein Regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 22:57:40 +02:00
duffyduckandClaude Opus 4.8 c29ffd7bea Vertragsformular: Link-Icon am Zaehler-Label zur Kundenakte (Strom/Gas)
Wie bei Bankkarte/Ausweis/Adresse: das 'Zaehler'-Label im Energie-
Vertragsformular bekommt ein LabelWithLink zum Zaehler-Tab der
Kundenakte (/customers/:id?tab=meters), oeffnet im neuen Tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 22:51:38 +02:00
duffyduckandClaude Opus 4.8 5f06ed9830 Fix Folgevertrag aus deaktiviertem Vertrag + Kundendaten-Modal erweitern
1) Folgevertrag-Bug: Backend legt den Folgevertrag korrekt mit
   previousContractId an. Der Verlust passierte im Frontend - das
   Vorgaenger-Dropdown holt Vertraege ueber getAll, das DEACTIVATED
   standardmaessig ausblendet. Beim Bearbeiten des Folgevertrags war der
   deaktivierte Vorgaenger nicht als Option da -> Verknuepfung ging beim
   Speichern verloren.
   Fix: getAllContracts + Controller + contractApi.getAll um
   includeDeactivated erweitert; Vorgaenger-Dropdown nutzt es und markiert
   deaktivierte Vertraege mit '· deaktiviert'. Verifiziert (Flag inkludiert
   deaktivierte; Folgevertrag setzt previousContractId).

2) Kundendaten-Modal: zeigt jetzt zusaetzlich Lieferadresse + (falls
   abweichend) Rechnungsadresse des geoeffneten Vertrags, die Stressfrei-
   Adresse des Vertrags einzeln und darunter alle weiteren Stressfrei-
   Adressen des Kunden. CustomerInfoModal nimmt optionale Vertragskontext-
   Props; ContractDetail + ContractForm uebergeben sie.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 22:49:41 +02:00
duffyduckandClaude Opus 4.8 a6b1dac606 Gutschriften: Dateien beim Loeschen/Bearbeiten mit aufraeumen (R138)
Pentester-Hinweis: generierte Gutschrift-PDFs blieben nach dem Loeschen
der Gutschrift als verwaiste Files im Upload-Ordner liegen (harmlos, da
ohne DB-Referenz nicht mehr abrufbar - aber unsauber).

deleteCreditNote entfernt jetzt PDF (pdfPath) + Ueberweisungsbeleg
(receiptPath) von der Platte. updateCreditNote loescht das alte PDF
beim Leeren von pdfPath. Kein verwaister Ordner-Muell mehr.

Verifiziert: PDF nach Erzeugung vorhanden, nach Loeschen der Gutschrift
weg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 22:31:33 +02:00
duffyduckandClaude Opus 4.8 5fb01627b0 Vertrag-Update: Kuendigungs-Datumsfelder robust normalisieren (R138)
cancellationConfirmationDate / cancellationConfirmationOptionsDate im
PUT /contracts/:id ueber validateOptionalIsoDate normalisieren: nimmt
Datum-only (YYYY-MM-DD) UND volles ISO, liefert einen sauberen Date an
Prisma. Vorher lehnte Prisma ein Datum-only ab (400 statt Verarbeitung).
Ungueltige Formate -> sauberes 400. Konsistent zum Upload-Pfad, der
confirmationDate bereits so validiert.

Verifiziert: '2027-09-13' akzeptiert + als DATETIME geschrieben;
deutsches Format abgelehnt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 21:40:31 +02:00
duffyduckandClaude Opus 4.8 0f51a1cc3b Gutschriften/Kuendigung: DRAFT-endDate-Schutz + Belege staff-only
Pentest R138 Hygiene + neue Vorgaben:

1) endDate wird bei DRAFT-Vertraegen NICHT mehr gesetzt (Entwurf = nur
   Vorlage). Nur Status wurde vorher geschont, endDate zog trotzdem mit.

2) Ueberweisungsbelege (credit-note-receipts) sind jetzt reine
   Mitarbeiter/Admin-Downloads: neuer FileOwner-kind 'contract-staff'
   blockt Portal-Kunden im fileDownload-Controller. Das generierte
   Gutschrift-PDF (credit-notes) bleibt vertragsbasiert -> der
   besitzende Kunde darf seine eigene Gutschrift laden.

Bereits vorher abgesichert (bestaetigt): Kunden koennen keine
Gutschriften anlegen (blockPortal) und keine Belege hochladen
(Portal-403 im Upload).

Verifiziert: DRAFT haelt endDate; Beleg-Owner=contract-staff,
PDF-Owner=contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 21:29:43 +02:00
duffyduckandClaude Opus 4.8 8169c74d8e Vertrag: Auto-CANCELLED bei Kuendigungsbestaetigung + Cockpit-Filter
1) Auto-Status: Wird eine Kuendigungsbestaetigung hinzugefuegt (Datum
   ueber Formular und/oder Dokument-Upload), wird der Vertrag von ACTIVE
   auf CANCELLED gesetzt und das Vertragsende = Kuendigungsdatum.
   Zentrale Funktion maybeCancelOnCancellationConfirmation (idempotent,
   nur aus ACTIVE). Upload-Route ersetzt die alte Inline-Logik (setzt
   jetzt auch endDate); Update-Controller triggert nur bei neu/geaendertem
   Bestaetigungsdatum (manuelle Status-Korrekturen bleiben unangetastet).

2) Cockpit-Filter 'Kuendigungsbestaetigung': neue Liste
   cancellationConfirmations (Vertraege mit Bestaetigung in Status
   ACTIVE/DRAFT/CANCELLED) + Filter-Option im Cockpit-Dropdown. Eigene
   Liste, weil bereits CANCELLED-Vertraege mangels Issue sonst nicht in
   der Cockpit-Liste auftauchen.

Beides lokal verifiziert (Helper: ACTIVE->CANCELLED + endDate; Cockpit:
Vertrag erscheint in der Liste mit korrektem Status).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 15:13:05 +02:00
duffyduckandClaude Opus 4.8 5423c83009 Gutschriften Phase 3b/2: ZUGFeRD/Factur-X (hybrides PDF/A-3)
- zugferd.service.ts: CII-XML (EN 16931, urn:cen.eu:en16931:2017,
  Dokumenttyp 381 Gutschrift). Steuerkategorie S bei USt, sonst E mit
  Befreiungsgrund. Verkaeufer=Firma (CompanyProfile), Kaeufer=Kunde.
- zugferdPdf.service.ts: bettet factur-x.xml als AF /Data ein, setzt
  sRGB-OutputIntent + XMP (PDF/A-3B pdfaid + Factur-X-Extension-Schema)
  via pdf-lib.
- creditNotePdf: eingebettete DejaVuSans-Fonts (Pflicht fuer PDF/A)
  statt Standard-Helvetica; nach PDF-Erzeugung ZUGFeRD-Embedding.
- assets/fonts (DejaVuSans + Bold) + assets/icc (sRGB) ins Repo;
  Dockerfile kopiert backend/assets ins Runtime-Image.

Lokal strukturell verifiziert: 1 Seite, /AF, /Metadata, /OutputIntents,
EmbeddedFiles, Font eingebettet, XML wohlgeformt (xmllint), TypeCode
381, GrandTotal korrekt.

WICHTIG: vor Prod gegen einen ZUGFeRD-/Factur-X-Validator pruefen
(Staging + echte Firmendaten). Feinheiten (Trailer-ID, XMP, XML-MIME)
ggf. nach erstem Validator-Lauf nachziehen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-07 12:56:12 +02:00
duffyduckandClaude Opus 4.8 043bc173b7 Gutschrift-PDF: Fusszeile etwas tiefer positioniert
Unteren Rand fuer die Fusszeile temporaer auf 22 verkleinert, damit sie
naeher am Seitenende sitzt - weiterhin nur 1 Seite (verifiziert).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-06 19:01:34 +02:00
duffyduckandClaude Opus 4.8 32236706db Gutschrift-PDF: Fusszeile auf Seite 1 (keine zweite Seite mehr)
Footer wurde mit fester y=790 gesetzt und lief dadurch ueber den
unteren Seitenrand -> pdfkit legte eine zweite Seite an. Position jetzt
aus Seitengeometrie berechnet (heightOfString + page.height/margins),
sodass die Fusszeile am unteren Rand der ersten Seite endet, auch bei
zweizeiligem Umbruch. Verifiziert: PDF hat nur noch 1 Seite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-06 18:39:41 +02:00
duffyduckandClaude Opus 4.8 f24c4308aa Gutschriften: Auszahlungskonto (Bankkonto des Kunden) fuer Ueberweisungen
Bei Geld-Gutschriften kann die Auszahlung auf ein anderes Konto gehen
als das Vertrags-Abbuchkonto:

- CreditNote.payoutBankCardId + Migration (FK ON DELETE SET NULL, damit
  Loeschen einer Bankkarte die Gutschrift nicht mitreisst).
- Formular: Dropdown mit allen Bankkonten des Kunden (Default =
  Vertrags-Abbuchkonto). getCreditNoteDefaults liefert bankCards +
  contractBankCardId.
- Server prueft, dass die gewaehlte Bankkarte dem Kunden des Vertrags
  gehoert (kein Fremdkonto unterschieben).
- PDF: bei Ueberweisung 'Unsere Bankverbindung' (Absender) + darunter
  'an Bankkonto: <Kunden-IBAN> (<Inhaber>)'. Section-Zeile zeigt das
  Auszahlungskonto.

Lokal verifiziert (Anlage + PDF).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-06 13:49:35 +02:00
duffyduckandClaude Opus 4.8 642fb05c76 Gutschriften Phase 3b/1: Gutschrift-PDF (pdfkit)
PDF-Erzeugung fuer Gutschriften mit Firmenstammdaten als Absender,
Empfaenger (Kunde+Adresse), Netto/USt/Brutto (bzw. ohne USt-Ausweis),
Sachwert/'Ware erhalten' bzw. Ueberweisungs-Bankverbindung,
Unterschriftsblock, Firmen-Fusszeile.

Endpoint POST /credit-notes/:id/pdf; 'PDF'-Button in CreditNotesSection
(erzeugen + im Tab oeffnen bzw. vorhandenes ansehen). PDF-Pfad wird bei
inhaltlicher Aenderung geleert -> Neu-Erzeugung. Download ueber
fileDownload (credit-notes subDir, Vertrags-Ownership).

Lokal verifiziert (valides %PDF, korrekte Betraege).

ZUGFeRD-XML-Embedding (PDF/A-3, EN 16931) folgt als Teil 2 mit
Validator-Pruefung.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-06 13:32:34 +02:00
duffyduckandClaude Opus 4.8 2c705272ea Gutschriften Phase 3a: Firmenstammdaten (Absender) fuer PDF/ZUGFeRD
Neues CompanyProfile-Modell (Einzel-Zeile) + Migration (IF NOT EXISTS):
Firmenname, Anschrift, USt-IdNr, Steuernummer, Handelsregister,
Geschaeftsfuehrer, Kontakt, IBAN/BIC/Bank. Fliesst spaeter in
Gutschrift-PDF + ZUGFeRD-Verkaeuferdaten ein.

Backend: Service (getOrCreate/update mit Feld-Whitelist), Controller,
Routes GET/PUT /api/company-profile (settings:read/update), auditiert.

Frontend: Settings-Seite 'Firmenstammdaten (Absender)'
(/settings/company-profile) mit Firma/Anschrift, Steuer/Register,
Kontakt/Bank. Typ + companyProfileApi + Menue-Eintrag.

Phase 3b (PDF + ZUGFeRD-Erzeugung) folgt darauf auf.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-06 13:19:36 +02:00
duffyduckandClaude Opus 4.8 9e38cb32f0 Gutschriften Phase 2b: Vertrags-UI + Beleg-Upload + Nummernkreis-Seite
Frontend:
- CreditNotesSection im Vertragsdetail (eigene Section wie Rechnungen):
  Liste + Anlegen/Bearbeiten (Modal). Geld/Sachwert mit passenden
  Feldern, USt-Block mit Live-Vorschau Netto/USt/Brutto, Datum, Ort,
  Unterschrift, 'Ware erhalten'. Loeschen mit Bestaetigung.
- Ueberweisungsbeleg-Upload/-Anzeige/-Entfernen (nur Geld-Gutschriften).
- Nummernkreis-Verwaltung als Settings-Seite mit Live-Vorschau der
  naechsten Nummer.
- Frontend-Typen (CreditNote etc.) + creditNoteApi.

Backend:
- Ueberweisungsbeleg-Upload-Route /upload/credit-notes/:id/receipt
  (+ DELETE), Portal geblockt, Ownership ueber Vertrag.
- fileDownload: Ownership-Aufloesung fuer credit-note-receipts +
  credit-notes (Vertrag) ergaenzt.

Als Section statt Tab-Balken, weil die Vertragsansicht kartenbasiert
ist (kein bestehender Tab-Balken); spaeter umbaubar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-06 13:15:02 +02:00
duffyduckandClaude Opus 4.8 3580c51cb1 Gutschriften Phase 2a: Kleinunternehmer-Flag am Kunden + USt-Default
Kunden-Feld vatExempt (Kleinunternehmer/USt-befreit §19) + Migration
(ADD COLUMN IF NOT EXISTS). Checkbox im Kundenformular nur fuer
Firmenkunden.

Gutschrift-Vorbelegung vatRelevant wird aus dem Kunden abgeleitet:
Firmenkunde ohne USt-Befreiung -> USt-relevant an (Netto), sonst aus
(wie Privat). Jede Gutschrift speichert ihren eigenen Snapshot, ein
spaeterer Statuswechsel des Kunden aendert bestehende Gutschriften
nicht.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-06 10:10:13 +02:00
duffyduckandClaude Opus 4.8 f1c37a7d25 Gutschriften Phase 1: Backend (Modell, Nummernkreis, CRUD, USt-Rechnung)
Neues Feature Gutschriftsverwaltung (Subventionen am Vertrag):

- Modelle CreditNote + CreditNoteNumberRange + Enums (GELD/SACHWERT,
  PRIVAT/FIRMA, NETTO/BRUTTO) + Migration (IF NOT EXISTS, auf Dev
  angewandt).
- USt pro Gutschrift waehlbar (vatRelevant + Basis Netto/Brutto +
  Satz); Netto/USt/Brutto werden berechnet und getrennt gespeichert
  (ZUGFeRD-tauglich). Kundentyp Privat/Firma aus Kunde vorbelegt.
- Nummernkreis in Settings verwaltbar; Nummernvergabe transaktional
  mit SELECT ... FOR UPDATE (keine Doppelvergabe). Bsp GS-2026-0001.
- Service/Controller/Routes: GET/POST /contracts/:id/credit-notes,
  GET .../defaults, GET/PUT/DELETE /credit-notes/:id,
  GET/PUT /credit-notes/number-range. Portal-Token geblockt (interner
  Bereich), CREATE/UPDATE/DELETE auditiert.

Verifiziert: USt-Rechnung (200 netto->238, 200 brutto->168,07+31,93)
und fortlaufende Nummernvergabe.

Phase 2 (Vertrag-UI + Beleg-Upload + Nummernkreis-UI) und Phase 3
(PDF + ZUGFeRD) folgen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-06 09:03:42 +02:00
duffyduckandClaude Opus 4.8 3bbe262039 Vertrag kopieren: Legacy-phoneNumber/simCardNumber auch leeren (R135/136)
Pentester-Fund: die Legacy-Scalar-Felder mobileDetails.phoneNumber und
simCardNumber (kein sichtbares Formularfeld mehr) wurden beim Kopieren
noch uebernommen - anders als die SIM-Karten-Liste und anders als
'Rufnummern werden nicht uebernommen'. Zwei getrennte Vertraege
desselben Kunden haetten so unbemerkt dieselbe Rufnummer getragen.

Datenintegritaet, kein Security-Loch. Beide Felder jetzt im Kopier-
Leer-Block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 13:26:42 +02:00
duffyduckandClaude Opus 4.8 2aee1f1114 Vertrag kopieren: Portal-Zugangsdaten mitkopieren
Auf Wunsch: Portal-Benutzername + Stressfrei-Verknuepfung/'nicht
benoetigt'-Flag werden im Kopier-Modus behalten (bei gleichem Anbieter
oft identisch). Nur das Passwort bleibt leer, weil es verschluesselt
gespeichert und - wie beim Bearbeiten - nicht im Klartext ins Feld
geladen wird. Banner-Hinweis ergaenzt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 12:58:41 +02:00
duffyduckandClaude Opus 4.8 a2ed0e94f1 Vertrag kopieren: neuen eigenstaendigen Vertrag aus Vorlage anlegen
Kopieren-Button in der Vertragsansicht oeffnet das Neu-Formular unter
/contracts/new?copyFrom=<id> mit allen Daten des Quellvertrags
vorbefuellt. Beim Speichern entsteht ein frischer, unabhaengiger
Vertrag (kein previousContractId-Link, kein VVL).

Use-Case: neuer Mobilfunk-/o.ae. Vertrag ist meist fast identisch;
nur Preis, Laufzeit, Kunden-/Vertragsnummer, Anbieter/Tarif aendern
sich - der Rest bleibt gleich.

Im Kopier-Modus geleert (Integritaet/Eindeutigkeit): Status->DRAFT,
Vorgaenger-Link, alle Datumsfelder, Kunden-/Vertragsnummer beim
Anbieter + Plattform-Nummern, Portal-Zugang. SIM-Karten & Rufnummern
werden nicht uebernommen. Rest (Anbieter/Tarif/Preise/Detailfelder/
Notizen) bleibt als Vorlage.

Rein Frontend (ContractForm copy-Mode + Button in ContractDetail),
kein neuer Endpoint - nutzt bestehendes Create.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 12:44:49 +02:00
duffyduckandClaude Opus 4.8 5bbb7dfffe PdfDragButton komplett entfernt (Plattformgrenze)
Test-Ergebnis: Browser kann einer fremden Desktop-App keine echte
lokale Datei zum Anhaengen uebergeben. URL-Drag -> Thunderbird nur Link
(0 Bytes, Fehler beim Senden); Datei-Drop in den Text -> nur Dateiname
als Text; auf die Anhang-Leiste (Thunderbird/Linux) -> ebenfalls kein
echter Anhang; Webmail -> gar nicht moeglich.

Entscheidung: Feature raus. Download- und Anzeigen-Button decken den
Bedarf zuverlaessig ab. PdfDragButton geloescht, fileUrl()-Token-Param
zurueckgebaut. Pentest R131 damit gegenstandslos (kein Drag mehr).

Die separaten Copy-Buttons fuer IBAN + Ausweisnummer im Vertrags-
formular bleiben erhalten.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 14:51:52 +02:00
duffyduckandClaude Opus 4.8 3073acb4b0 PdfDragButton: echten Datei-Inhalt (Blob) ziehen statt Link
Thunderbird & Co. hatten beim URL-basierten Drag nur einen LINK als
Anhang gespeichert (0 Bytes) und die Datei erst beim Senden nachgeladen
-> Fehler, weil kurzlebiger Token abgelaufen. Der User erwartet, dass
die Datei direkt angehaengt wird, nicht der Link.

Loesung: Datei wird vorab per Bearer-Auth (Axios-Instanz) als Blob
geladen und beim dragstart als echter Datei-Inhalt uebergeben:
dataTransfer.items.add(File) + DownloadURL mit lokaler blob:-URL.
-> Datei-INHALT wird uebertragen, kein Link.

Blob wird beim Mount vorgeladen (Ladezustand 'laedt ...'); der
Endpoint /files/download schreibt kein Audit-Log, daher kein Spam.

Security R131 damit vollstaendig erledigt: im Drag steckt weder eine
Server-URL noch ein Token -> ein Fehl-Drop kann gar nichts mehr leaken
(kein Access-, kein Download-Token). Token-Cache/getDownloadToken im
Drag-Pfad entfernt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 14:11:17 +02:00
duffyduckandClaude Opus 4.8 645d9a2624 PdfDragButton: text/x-moz-url fuer Thunderbird/Firefox ergaenzen
Zusaetzliches Mozilla-Drag-Format (URL\nTitel), damit Thunderbird und
Firefox als Drop-Ziel zuverlaessiger auf den Drag reagieren. Weiterhin
mit dem 60s-Download-Token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 13:59:37 +02:00
duffyduckandClaude Opus 4.8 31350d3225 PdfDragButton: Text-Formate wieder setzen, damit Ziel-Fenster reagiert
Das Weglassen von text/uri-list + text/plain (optionale R131-Haertung
#2) hatte den Drop ins Mail-Fenster gebrochen: web-basierte Ziele
verstehen DownloadURL (Nativ-Format) nicht und reagierten gar nicht.

Jetzt werden im Normalfall wieder alle drei Formate gesetzt - aber mit
dem kurzlebigen 60s-Download-Token (R131-Empfehlung #1, der eigentliche
Fix des Token-SCOPE). Ein Fehl-Drop zeigt damit hoechstens eine 60s
gueltige Downloads-only-URL statt des 15-Min-Voll-Access-Tokens.

Nur im seltenen Warm-up-Rennen wird ausschliesslich DownloadURL mit
Access-Token-Fallback gesetzt (kein Klartext-Leak).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 13:57:32 +02:00
duffyduckandClaude Opus 4.8 c344de8bfa PdfDragButton: Drag nie abbrechen (Regression nach R131-Fix)
Der R131-Fix hatte den Drag per preventDefault() abgebrochen, wenn der
60s-Download-Token beim Griff noch nicht vorgewaermt war -> im Mail-
Fenster kam gar keine Datei mehr an.

Jetzt wird der Drag nie abgebrochen: bevorzugt der vorgewaermte
Download-Token, im seltenen Rennen Fallback auf den Access-Token, aber
weiterhin NUR in DownloadURL (kein text/plain) -> kein Klartext-Leak.
DownloadURL ist ein Nativ-Format und wird bei Fehl-Drop in Web-Text-
felder nicht als lesbarer Text ausgegeben, daher bleibt der R131-Fix
(kein Token-Leak als Text) erhalten.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 12:39:45 +02:00
duffyduckandClaude Opus 4.8 7d062bf9b2 PdfDragButton R131: 60s-Download-Token statt Access-Token, nur DownloadURL
Pentest R131 (LOW-MEDIUM): Der Drag-Button haengte den 15-Min-Access-
Token an die URL und schrieb ihn per text/plain + text/uri-list beim
Drag mit. Ein Fehl-Drop in ein Text-/Chat-/URL-Feld haette den vollen
Access-Token (alle Berechtigungen, 15 Min) als lesbaren Text geleakt.

Fix:
- fileUrl() akzeptiert jetzt optionalen expliziten Token.
- PdfDragButton nutzt den kurzlebigen 60s-Download-Token
  (authApi.getDownloadToken(), type:download, nur ?token=) statt des
  Access-Tokens. Modul-weiter Cache mit Dedup, auf mount + hover
  vorgewaermt (dragstart ist synchron, kann nicht awaiten).
- Es wird NUR noch DownloadURL im DataTransfer gesetzt, keine
  text/plain- oder text/uri-list-Repraesentation -> Fehl-Drop in ein
  Textfeld erzeugt gar keinen sichtbaren Text.
- Klick-Vorschau nutzt ebenfalls bevorzugt den Download-Token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 12:12:24 +02:00
duffyduckandClaude Opus 4.8 8e74f7733a PdfDragButton: Info-Icon mit Chrome/Edge-Hinweis als Tooltip
Kleines Info-Icon hinter dem Drag-Element mit Tooltip, dass das Ziehen
nur in Chrome/Edge funktioniert und man in Firefox stattdessen klicken
soll. Gilt automatisch an allen Einbaustellen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 11:55:06 +02:00
duffyduckandClaude Opus 4.8 a509bb7826 PdfDragButton: PDF per Drag-and-Drop aus Bankkarte/Ausweis ziehen
Neue wiederverwendbare Komponente PdfDragButton: ziehbares Element,
mit dem der hinterlegte Scan direkt aus dem Browser in ein Mail-Fenster
(Anhang) oder den Datei-Explorer gezogen werden kann (Chromium-
DownloadURL, Format <mime>:<name>:<absolute-url>).

Bewusste Plattform-Grenze: PDF per Strg+V als Datei einfuegen geht im
Browser nicht (Web-Clipboard darf keine OS-Datei-Zwischenablage
befuellen) -> Drag-and-Drop. DownloadURL nur Chrome/Edge, nicht
Firefox (dort Klick-Fallback: Datei im Tab oeffnen).

Eingebaut an: ContractForm (neben IBAN/Ausweisnummer-Copy), Vertrags-
ansicht (Bankkarte/Ausweis-Card), Kundenakte-Tabs Bankkarten+Ausweise.
Nur bei vorhandenem documentPath. Nutzt bestehende fileUrl()-Download-
URL (Token als Query, Per-File-Ownership-Check unveraendert) - kein
neuer Endpoint, keine neue Angriffsflaeche.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 11:50:01 +02:00
duffyduckandClaude Opus 4.8 1018412afb Mailbox-Fix R130: enableMailboxForExisting statt updateMailboxPassword
Pentester R130: der erste Fix (c39d252) setzte bei Plesk nur -passwd,
liess die Adresse aber auf -mailbox false stehen -> Mailbox wurde nie
aktiviert, IMAP/SMTP-Login scheiterte trotz korrektem Passwort.

Jetzt enableMailboxForExistingEmail (-mailbox true -passwd ...), das
sowohl den existierte-als-Forward-Fall als auch den Neu-Anlage-Fall
idempotent abdeckt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 11:29:34 +02:00
duffyduckandClaude Opus 4.8 f0480ab432 Vertragsformular: Copy-Buttons fuer Bankkarte (IBAN) + Ausweis
In Kundendaten verknuepfen je ein Kopieren-Button neben dem Label.
Kopiert nur den reinen Wert der aktuell gewaehlten Option: bei
Bankkarte die IBAN ohne Namen, bei Ausweis die Ausweisnummer ohne
(TYP). Button erscheint nur bei ausgewaehltem Eintrag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 11:24:09 +02:00
duffyduckandClaude Opus 4.8 c39d252f5f Stressfrei-Mailbox: Postfach-Passwort beim Anlegen verbindlich setzen
Beim Anlegen einer Adresse mit echter Mailbox (IMAP/SMTP) wird das
frisch generierte Passwort jetzt immer explizit am Provider gesetzt
(updateMailboxPassword direkt nach dem Provisioning).

Behebt den Fall, dass die Adresse beim Provider bereits als reine
Weiterleitung existierte: dann kehrte provisionEmailWithMailbox frueh
mit success zurueck, ohne je ein Postfach-Passwort zu setzen. Im CRM
lag dann ein verschluesseltes Passwort, das der Provider nicht kannte
-> IMAP/SMTP-Login schlug fehl. Jetzt stimmen CRM und Provider ueberein.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 11:13:08 +02:00
duffyduckandClaude Opus 4.8 c0bd8a9f5c Tabs: umbrechen statt aus dem Rahmen laufen
Tab-Leiste ist mit dem zusaetzlichen Geworben/angeworben-Tab zu breit
geworden; der letzte Tab lief aus dem Karten-Rahmen. nav auf flex-wrap
umgestellt (gap-x-6 gap-y-1), damit ueberzaehlige Tabs in eine zweite
Zeile umbrechen und innerhalb der Karte bleiben.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 10:58:15 +02:00
duffyduckandClaude Opus 4.8 81cd284cc4 Referrals: 4 neue Beziehungen + Bearbeiten-Stift pro Eintrag
Beziehungs-Dropdown erweitert um Schwiegertochter/Schwiegersohn,
Schwiegermutter/Schwiegervater, Oma/Opa, Uroma/Uropa (Whitelist
front- und backend synchron).

Bearbeiten-Stift pro Zeile (vor der Muelltonne): Beziehung und/oder
Gegen-Kunde aenderbar. Neuer PUT /:customerId/referrals/:referralId
mit Whitelist-Pruefung, Doppel-Werber-409 (Self-Ausschluss),
Portal-Block und UPDATE-Auditeintrag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 10:57:04 +02:00
duffyduckandClaude Opus 4.7 2d8ee7f569 Kundenportal: Deaktivierte-Toggle in "Meine Verträge"-Header
Der Toggle "Deaktivierte anzeigen" saß im obersten Seiten-Header.
Verschoben in den Gruppen-Header "Meine Verträge" (rechtsbündig,
nur beim eigenen Kunden-Block), damit er direkt bei den Verträgen
sitzt statt losgelöst ganz oben.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 02:51:31 +02:00
duffyduckandClaude Opus 4.7 cda6d2814e Kundenakte: Tab "Geworben / angeworben" (Kundenempfehlungen)
Neuer Tab vor "Datenschutz", nur Mitarbeiter/Admin (nicht Portal),
ohne Consent-Pflicht. Zwei Abschnitte:
1. "<Kunde> wurde an Board geholt durch:" – max. 1 Werber
   (DB-Unique auf recruitedId).
2. "<Kunde> hat folgende Kunden an Board geholt:" – beliebig viele.

Jede Zeile: Kunde per Lupe-Such-Modal (breite Suche über Name/
Kundennr./Firma/E-Mail/Telefon) + Beziehungs-Dropdown. Löschen +
Externtab-Link zur Kundenakte pro Zeile.

Bidirektional aus EINEM Datensatz: "A geworben durch B" erscheint
automatisch bei B unter "hat geworben"; von beiden Akten
hinzufügbar/löschbar.

Backend: neues Model CustomerReferral (recruiter/recruited FKs,
recruitedId @unique, relationship) + Migration. Beziehungs-Whitelist
serverseitig; Self-Werbung + Doppel-Werber (409) abgefangen.
Portal-Token wird explizit geblockt (Defense-in-Depth, nicht nur
UI-Ausblendung). CREATE/DELETE auditiert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 17:50:15 +02:00
duffyduckandClaude Opus 4.7 6b08762ff9 Frontend: "Neue Version verfügbar"-Banner nach Deploy
Die Cache-Header waren schon optimal (index.html no-store, Assets
immutable) – aber eine bereits laufende SPA holt sich nach einem
Deploy keinen neuen Code, bis der Nutzer neu lädt. Das fängt kein
Cache-Header ab.

useVersionCheck holt die (server-seitig no-store) index.html
periodisch (5 min) + beim Zurückkehren zum Tab, extrahiert die Menge
der referenzierten Vite-Asset-Hashes als Signatur und vergleicht sie
mit dem Startstand. Ändert sie sich (= neuer Build deployt), zeigt
UpdateBanner oben im Layout einen dezenten Hinweis mit "Jetzt neu
laden".

Kein Backend-/Build-Change; im Dev-Modus No-op (Vite-Dev liefert
keine /assets/<hash>-Dateien → leere Signatur → kein Alarm).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 16:56:35 +02:00
duffyduckandClaude Opus 4.7 fd8be1d5c4 Kundenportal: Toggle "Deaktivierte Verträge anzeigen"
Der Admin-/Mitarbeiter-Blick (CustomerDetail) hatte den Toggle schon;
jetzt auch in der Kundenportal-Vertragsübersicht (ContractList,
Portal-Zweig). Button nur bei isCustomerPortal, reicht
includeDeactivated an die getTreeForCustomer-Queries durch
(showDeactivated im Query-Key → frischer Fetch beim Umschalten).

Security: derselbe Endpoint hinter canAccessCustomer (R120) – der
Portal-Kunde bekommt nur eigene/vertretene Bäume, das Flag weitet
nur den Status-Filter innerhalb der erlaubten Daten. Keine neue
Exposition.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 16:48:24 +02:00
duffyduckandClaude Opus 4.7 e8d996d07e Mobilfunknetz: server-seitige Whitelist (Pentest-INFO)
mobileNetwork akzeptierte serverseitig jeden stripHtml-bereinigten
String – das Frontend-Dropdown beschränkte nur clientseitig. Jetzt
Whitelist TELEKOM|VODAFONE|TELEFONICA (normalizeMobileNetwork),
angewandt in Create- UND Update-Pfad (der Update-Spread reichte den
Wert vorher ungefiltert an Prisma durch). Unbekannte/leere Werte
werden zu null normalisiert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 16:41:19 +02:00
duffyduckandClaude Opus 4.7 b4dfede494 Rate-Limiting: IPv6-Bypass-Härtung via ipKeyGenerator
loginRateLimiter und staffPasswordReAuthLimiter keyten auf die volle
req.ip. Bei IPv6 kann ein Angreifer aus seinem zugeteilten Block
(/56–/64) pro Request eine neue Adresse nehmen und so das Per-IP-Limit
umgehen – betrifft Login-Bruteforce-Schutz und den Passwort-Set-
Reauth-Limiter.

Fix: req.ip in beiden keyGenerator durch ipKeyGenerator() ersetzt
(express-rate-limit v7). IPv6 wird auf das Subnetz normalisiert
(Library-Default /56), IPv4 bleibt unverändert. Verifiziert: zwei
verschiedene IPv6 im selben /56 ergeben denselben Key.

Die Limiter ohne eigenen keyGenerator (Passwort-Reset, Consent)
normalisieren IPv6 bereits über den Library-Default – damit jetzt
konsistent. Ersetzt den verworfenen aria-WIP, neu auf aktuellem
Stand gebaut.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 16:38:54 +02:00
duffyduckandClaude Opus 4.7 627cae9b3b Spam-Anhänge: klarer Fehler statt Junk-Ordner-Raterei (Pentest R124)
Nachtrag zum R124-Fix: statt bei nicht auffindbarem Junk-Ordner still
auf INBOX bzw. den geratenen String 'Junk' zurückzufallen, wird jetzt
ein klarer Fehler zurückgegeben – sonst könnte im Randfall (Junk-Ordner
serverseitig entfernt/umbenannt) doch wieder die falsche UID aus dem
falschen Ordner gezogen werden.

- 4 Controller-Funktionen (downloadAttachment, saveAttachmentTo,
  saveAttachmentAsInvoice, saveAttachmentAsContractDocument):
  404 "Postfach hat keinen Spam-/Junk-Ordner (mehr)".
- 2 Service-Funktionen (moveEmailToTrash, restoreEmailFromTrash):
  klare error-Message im TrashOperationResult.

Kein undefined mehr Richtung IMAP-Lib.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 16:30:49 +02:00
duffyduckandClaude Opus 4.7 0a0cbe0e53 Spam-Tab: Anhänge aus dem echten Junk-Ordner holen (Pentest R124)
Beim Spam-Feature wurden moveEmailToTrash/restoreEmailFromTrash auf
den echten Junk-Pfad umgestellt, vier Attachment-Funktionen im
Controller aber nicht: downloadAttachment, saveAttachmentTo,
saveAttachmentAsInvoice, saveAttachmentAsContractDocument bestimmten
den IMAP-Ordner weiter hart als
email.folder === 'SENT' ? 'Sent' : 'INBOX'.

Für SPAM-Mails landete das fälschlich auf INBOX. Da IMAP-UIDs pro
Ordner vergeben sind: bestenfalls 404, schlimmstenfalls (UID-Kollision
INBOX vs. Junk) der FALSCHE Anhang aus INBOX – der dann z.B. als
Rechnung/Vertragsdokument abgelegt würde. Kein Cross-Customer-Leak
(gleicher Kunde/Postfach), aber Datenintegritätsproblem.

Fix: an allen vier Stellen dieselbe Junk-Pfad-Logik wie in
moveEmailToTrash (email.folder === 'SPAM' → findJunkFolderPath).
findJunkFolderPath war in dem Controller noch nicht importiert.

Vom Pentester (R124) gefunden – beim ursprünglichen Feature übersehen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 16:26:21 +02:00
duffyduckandClaude Opus 4.7 61c33a993b E-Mail-Client: unbekannter folder-Param defaultet auf INBOX
Kleine Nachhärtung zum Spam-Tab: in getCachedEmails blieb where.folder
bei einem unbekannten folder-Wert ungesetzt und mischte alle Ordner
des Postfachs. Jetzt defaulten unbekannte/fehlende Werte klar auf
INBOX. Ownership-Scope (customerId/stressfreiEmailId) war nie
betroffen – rein Ordner-Filter.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 15:59:24 +02:00
duffyduckandClaude Opus 4.7 dfb4dadae1 E-Mail-Client: Spam-Ordner als eigener Tab
Neuer Tab "Spam" (zwischen Gesendet und Papierkorb), zeigt den Junk-/
Spam-Ordner des gewählten Postfachs – damit fälschlich als Spam
einsortierte Mails auffindbar sind.

Backend:
- EmailFolder-Enum um SPAM erweitert (Migration, Wert angehängt →
  kein Rewrite bestehender Zeilen).
- imapService.findJunkFolderPath: ermittelt den Junk-Ordner per
  Special-Use-Flag \Junk + üblicher Namensliste (Junk/Spam/…).
- syncAllFoldersForAccount synct den Junk-Ordner zusätzlich als
  dbFolder=SPAM (syncEmailsForAccount bekommt dbFolder-Option).
- getCachedEmails + getFolderCountsForAccount um SPAM erweitert.
- Papierkorb-Move/Restore für Spam-Mails nutzt den echten Junk-Pfad
  als Quell-/Zielordner.

Frontend: Tab + Badge (ungelesen/gesamt), gleicher List-/Detail-Pfad
wie INBOX; Zuordnen-zu-Vertrag auch aus Spam möglich.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 15:52:24 +02:00
duffyduckandClaude Opus 4.7 8eb3790e7b Mobilfunk: Feld "Mobilfunknetz" unter Anbieter & Tarif
Neues Dropdown "Mobilfunknetz" in der Anbieter-&-Tarif-Karte, nur
sichtbar bei Vertragstyp Mobilfunk. Optionen: Bitte auswählen (leer),
Telekom, Vodafone, Telefónica.

Neues Feld MobileContractDetails.mobileNetwork (String nullable,
speichert TELEKOM/VODAFONE/TELEFONICA) + idempotente Migration
(ADD COLUMN IF NOT EXISTS). String statt Enum, damit weitere Netze
ohne Migration ergänzbar sind. Anzeige in der Vertragsansicht mit
lesbarem Netz-Namen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 13:07:56 +02:00
duffyduckandClaude Opus 4.7 8508bdc38e Lieferbestätigung: eingegebenes Datum überschreibt Vertragsbeginn
Beim Upload einer Lieferbestätigung mit Datum wurde startDate nur
gesetzt, wenn es noch leer war (if (!contract.startDate)). Hatte der
Vertrag schon ein geschätztes Beginndatum, blieb es trotz
eingetragenem Lieferdatum stehen.

Fix in maybeActivateOnDeliveryConfirmation: ein explizit eingegebenes
Lieferdatum übernimmt jetzt IMMER den Vertragsbeginn (die Liefer-
bestätigung ist das maßgebliche tatsächliche Startdatum), auch
überschreibend. Der Fallback "heute" (kein Datum angegeben) füllt
weiterhin nur ein leeres Feld, damit ein echtes Datum nicht
versehentlich mit heute überschrieben wird. No-op-Guard + Audit-Log
unverändert.

Frontend schickte das Datum bereits mit und lädt den Vertrag nach
dem Upload neu – kein Frontend-Change nötig.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 12:49:55 +02:00
duffyduckandClaude Opus 4.7 bc5d639703 Dashboard: anstehende Geburtstage anzeigen (wie im Cockpit)
Die Geburtstags-Sektion gab es bisher nur im Vertrags-Cockpit.
Jetzt zusätzlich auf dem Dashboard für Mitarbeiter/Admins – gleiche
birthdayApi.getUpcoming(7, 30)-Query und identische Karten-
Darstellung (Heute/vergangen/kommend, Link zur Kundenakte). Kein
Backend-Change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 11:33:58 +02:00
duffyduckandClaude Opus 4.7 e775b0e439 Pentest R121: Audit-Verify-Fehlalarm bei leerem resourceId beheben
POST /api/audit-logs/verify meldete ~73% der Einträge als
"manipuliert" – kein echtes Tampering, sondern ein Serialisierungs-
Bug in generateHash: resourceId war beim Schreiben oft undefined
(Middleware-Einträge ohne Route-ID). JSON.stringify lässt einen
undefined-Wert weg → Hash ohne resourceId-Key. In der DB steht der
Wert aber als NULL; verifyIntegrity/rehashAll lasen null zurück und
JSON.stringify({resourceId:null}) schrieb ihn rein → anderer Hash
→ Fehlalarm für jede Zeile mit leerem resourceId. Die vom Pentester
gefundene durationMs-Korrelation war nur ein Proxy (Middleware-
Einträge haben durationMs UND oft kein resourceId).

Fix: generateHash lässt nullish resourceId weg – reproduziert exakt
das historische Schreibverhalten (undefined → Key weg). Kein Caller
übergibt je null explizit (verifiziert), daher matchen alle
Bestands-Hashes ohne Rehash; Feldreihenfolge unverändert.

Empirisch gegen echte DB bewiesen: aktuelle Hash-Ära (id>4141)
482/482 valide (vorher wurden alle 289 leeren-resourceId-Zeilen
falsch geflaggt).

Separat/vorbestehend (NICHT dieser Fix): Einträge vor Commit
fd55742 ("complete new audit system") nutzen ein altes Hash-Schema
und brauchen einmalig POST /api/audit-logs/rehash zum Re-Baselinen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-21 09:13:18 +02:00
duffyduckandClaude Opus 4.7 094887f1d3 Pentest R120: Audit-Log /:id mit nicht-numerischer ID → 400 statt 500
Beim Suchen eines verify-integrity-Endpoints stiess der Pentester
auf einen 500er: GET /api/audit-logs/verify matcht GET /:id (das
echte Verify ist POST /verify), parseInt("verify") = NaN → Prisma
findUnique({ where:{ id: NaN }}) wirft. Gleiche 400-statt-500-
Klasse wie R64.1/R104.1.

Fix: Number.isNaN-Guard in getAuditLogById, getAuditLogsByCustomer
und updateRetentionPolicy → sauberer 400 „Ungültige ID".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 23:00:28 +02:00
duffyduckandClaude Opus 4.7 42705145dd Pentest R120: canAccess-403 zusätzlich tamper-evident auditieren
Der Retest deckte auf, dass abgewehrte Cross-Customer-Zugriffe
(canAccess*-403) nur im SecurityEvent-Monitoring-Stream landeten
(ACCESS_DENIED, /api/monitoring/events) – nicht im AuditLog, wo
der Pentester suchte. Der Monitoring-Stream ist zudem löschbar
(DELETE /api/monitoring/events) und nicht hash-verkettet.

emitAccessDenied schreibt jetzt zusätzlich einen tamper-evidenten
AuditLog-Eintrag (action READ, resourceType 'AccessDenied',
Sensitivity HIGH), der ein Clearen des Monitoring-Streams
überlebt und über die Hash-Kette manipulationssicher ist. Gilt
für alle canAccess*-403 (Contract + Customer, inkl. Vollmacht-
fehlt-Fall). Kein Enum-/Schema-Change: READ + distinktiver
resourceType, filterbar via searchAuditLogs.

Korrigiert damit auch meine frühere ungenaue Aussage „landet im
Audit" – vorher war das die falsche Tabelle.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 22:51:40 +02:00
duffyduckandClaude Opus 4.7 23c530be46 Pentest R120 CRITICAL: IDOR auf Vertragsbaum-Endpunkt schließen
GET /api/contracts?tree=true&customerId=<fremd> gab Portal-Usern
den vollständigen Vertragsbaum beliebiger Fremdkunden zurück
(Name, Kundennummer, Vertragsnummern, Tarife – HTTP 200). Der
tree=true-Zweig returnte früh, bevor die Portal-User-customerIds-
Filterung griff, die für die flache Liste im selben Handler läuft.
Vorbestehender Bug; der includeDeactivated-Toggle machte ihn nur
sichtbarer (auch archivierte Fremdverträge kamen mit). Live vom
Pentester bestätigt, Gegentest ohne den Param = identisches Leck.

Fix: canAccessCustomer(req, res, customerId) vor dem frühen
Return. Prüft eigene ID + vertretene MIT Live-Vollmacht, sendet
selbst die 403. Staff passiert unverändert. Gleiches Muster wie
Pentest 56.3 bei update/delete.

Flacher Listenpfad war nie betroffen: getAllContracts bevorzugt
das serverseitig gesetzte customerIds-Scope gegenüber dem rohen
customerId-Query-Param.

Docs: SECURITY-HARDENING.md § Runde 120 + docs/todo.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 22:31:13 +02:00
duffyduckandClaude Opus 4.7 d059912c1e Kundenansicht: Toggle „Deaktivierte Verträge anzeigen"
Der Vertragsbaum beim Kunden blendete DEACTIVATED-Verträge
komplett aus. Da der aktuellste Vertrag die Baumwurzel ist und
Vorgänger als Children hängen, verschwand eine ganze Kette aus
der Ansicht, sobald die Wurzel deaktiviert wurde – so „verschwand"
ein Vertrag scheinbar, als ein aktiver Folgevertrag gelöscht und
der Vorgänger vorher deaktiviert worden war.

getContractTreeForCustomer bekommt ein optionales
includeDeactivated-Flag (Default false = bisheriges Verhalten),
durchgereicht per Query-Param includeDeactivated=true. Toggle-
Button (Eye/EyeOff) im Kunden-Vertragstab; showDeactivated ist
Teil des Query-Keys → frischer Fetch beim Umschalten. Deaktivierte
behalten ihr graues DEACTIVATED-Badge.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 21:45:58 +02:00
duffyduckandClaude Opus 4.7 d0611b740a Vertrag-Löschen: Kette bleibt intakt, Liste refresht
Zwei zusammenspielende Bugs:

1) Backend: deleteContract-Reihenfolge scheiterte am UNIQUE-
   Constraint auf Contract.previousContractId. Beim Middle-Delete
   (A → B → C, B löschen) hielt B im UPDATE-Moment noch selbst
   previousContractId=A – der Versuch, C ebenfalls auf A
   umzubiegen, warf MariaDB Duplicate-Entry, das UPDATE brach ab.

   Fix: Transaction, umgedrehte Reihenfolge – erst B löschen
   (DB-Cascade ON DELETE SET NULL räumt C.previousContractId ab,
   gibt A-Slot frei), dann C sauber auf A umbiegen.

2) Frontend: deleteMutation invalidierte weder Contract-Listen
   noch Kunden-Vertragsbaum und hatte keinen onError-Handler.
   Nach dem Delete wurde nach /contracts navigiert, dort zeigte
   der stale Cache noch den gelöschten Vertrag – Eindruck: „ganze
   Historie weg". Bei Bug 1 fehlgeschlagen brach das schweigend
   ab.

   Fix: queryClient.invalidateQueries für ['contracts'],
   ['contract-tree', customerId], ['customer', customerId] nach
   Erfolg. onError-Toast bei Fehler.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 20:52:02 +02:00
duffyduckandClaude Opus 4.7 66fb5092fe Audit: Portaldaten-Opt-out als eigenes CRITICAL-Event loggen
Auf Wunsch des Pentesters (R117-Nachtrag). Das Umschalten des
portalCredentialsNotRequired-Flags emittiert jetzt zusätzlich zum
generischen Contract-Update-Diff ein dediziertes UPDATE-Event
unter resourceType 'ContractPassword' – landet damit in derselben
CRITICAL-Sensitivity-Reihe wie Klartext-Password-Reads.

Motivation: das Setzen des Flags räumt server-seitig
portalUsername, portalPasswordEncrypted und stressfreiEmailId auf
NULL. Diese Löschung soll unabhängig vom generischen Diff sofort
sichtbar sein. Rücknahme wird ebenfalls geloggt.

Details enthält alte + neue Flag-Werte plus Bool-Marker, welche
Anmeldedaten vor dem Opt-out belegt waren – keine Klartext-
Leckage im Log.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 14:58:51 +02:00
duffyduckandClaude Opus 4.7 dcdb3cb057 Vertrag: „Portaldaten nicht möglich / nicht benötigt"-Opt-out
Dritter Radio-Button in der Zugangsdaten-Card des Vertragsformulars.
Wenn gesetzt, unterdrückt das Cockpit die Warnung „Portal-Zugangs-
daten fehlen" für diesen Vertrag – für Anbieter ohne Portal oder
Kunden, die bewusst keine Zugangsdaten pflegen. Verstopft das
Cockpit sonst dauerhaft.

Neues Feld Contract.portalCredentialsNotRequired (Boolean, default
false) + idempotente Migration (ADD COLUMN IF NOT EXISTS). Bestand
bleibt auf false, Warnung greift wie bisher.

Beim Umschalten auf Opt-out werden portalUsername,
portalPasswordEncrypted und stressfreiEmailId server-seitig
explizit auf NULL gesetzt – Datenhygiene, damit keine verwaisten
Anmeldedaten in der DB stehen bleiben.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 14:45:32 +02:00
duffyduckandClaude Opus 4.7 aeecc3327f SIM-Karten: PIN/PUK-Fehler statt stummer Alert-Meldung
Der bisherige alert() maskierte die eigentliche Ursache (Vertrag
nicht gefunden / Kein Zugriff / Decrypt-Fehler). Ersetzt durch
Toast mit Backend-Message + console.error mit Stacktrace, plus
Info-Toast wenn die SIM in der DB gar keine PIN/PUK hinterlegt hat.

Endpoint (GET /contracts/simcard/:id/credentials) und Auditing
unverändert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 14:45:20 +02:00
duffyduckandClaude Opus 4.7 e743e6795b Vertrag-Zugangsdaten: Externtab-Link zu Kunden-Adressen
Icon neben "Portal Benutzername" öffnet die Stressfrei-/Corporate-
Adressverwaltung des Kunden in neuem Tab, ohne das Vertragsformular
zu verlassen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 13:54:35 +02:00
duffyduckandClaude Opus 4.7 9425cb2ab6 E-Mail-Detail: Links sichtbar + Header kompakter
Bug 1 (Header gestaucht): kompakteres Vertical-Spacing im
Header (px-4 pt-3 pb-2 statt p-4, space-y-2 statt -3),
Subject auf max 2 Zeilen begrenzt mit vollem Titel im
tooltip. min-h-0 auf der Flex-Container, damit der Body
tatsächlich flex-1 claimt.

Bug 2 (Link unsichtbar): Corporate-CTAs stylen ihre Buttons
gerne mit inline style="color:white;background:green"; der
zugehörige <style>-Block wird von DOMPurify entfernt, das
inline-style am <a> überlebt aber – Ergebnis: weißer Link
auf weißem CRM-Hintergrund, unsichtbar. Fix: von <a>-Tags
werden style und class gestrippt, dann greift Browser-
Default (blau/violett + unterstrichen) wieder.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 12:47:59 +02:00
duffyduckandClaude Opus 4.7 b099297e11 Cockpit: DEACTIVATED + CANCELLED skippt Fristen-Warnungen
Bug: Ein DEACTIVATED-Vertrag (manuell archiviert) zeigte im
Cockpit weiter "Vertragsende: Vertrag seit 79 Tagen abgelaufen!"
– sinnlos, weil der Mitarbeiter den Vertrag ja aktiv beendet
hatte.

Fix: skipDeadlines-Guard, der bisher nur ONGOING abfing, greift
jetzt auch bei DEACTIVATED und CANCELLED. Beide sind terminale
Status – laufende Fristen ergeben da semantisch keinen Sinn.

Bewusst weiterhin AKTIV:
- Block 13a (Schlussrechnung fehlt): feuert wegen dieser Status,
  nicht trotzdem.
- Block 8 (DSGVO/Consent): läuft pro Kunde, nicht pro Vertrag.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-11 17:08:54 +02:00
duffyduckandClaude Opus 4.7 217aa82b2a Pentest R111: contract-categories – Permission-String angeglichen
Pentester meldete 403 auf POST/PUT/DELETE /api/contract-categories/*
für Admin trotz vorhandener contract-categories:*-Perms. Vermutete
Regression aus R110, war aber schon seit dem allerersten Commit
so: Route verlangte developer:access, während der Permission-Seed
den Admins contract-categories:* gibt.

Fix: Route auf contract-categories:create/update/delete umgestellt,
konsistent mit den sechs Peer-Katalog-Endpunkten (platforms/
tariffs/cancellation-periods/… nutzen alle ressourcen-spezifische
Perms). Kommentar aktualisiert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-11 16:35:47 +02:00
duffyduckandClaude Opus 4.7 da3ae282fc Pentest R110: Mass-Assignment-Whitelist auf 7 Katalog-Endpunkten
MEDIUM: PUT /api/stressfrei-emails/:id und 6 weitere Update-
Endpunkte (platform, tariff, contractCategory, cancellationPeriod,
contractDuration, email-providers) reichten req.body ungefiltert
an Prisma. Gleiche Bug-Klasse wie das gefixte M1-Finding, sieben
Stellen mehr. Nachgewiesen via provisionError-Feld ausserhalb des
TS-Types.

Fix: sieben Whitelists + pickXxxUpdate()-Helper in sanitize.ts,
in den jeweiligen Controllern eingehängt. Reuse der bewährten
pick()-Infrastruktur (Customer/User seit Runde 7).

EmailProvider bewusst OHNE stripHtmlFromStrings, weil Passwörter
und API-Keys legitim Sonderzeichen enthalten dürfen.

Doku: SECURITY-HARDENING.md § Runde 110 + docs/todo.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-11 16:16:49 +02:00
duffyduckandClaude Opus 4.7 dc7dcfb5f3 Docs: neue Fixes in todo + TESTING nachpflegen
todo.md: drei letzte Bug/Feature-Einträge
- AutosaveDateInput (Kündigungsdatum-Fokus-Sprung)
- IMAP-Humanizer (Anhang + Sync)
- Bankkarte-Beschreibung in Vertrags-Ansicht + Select

TESTING.md: neuer Abschnitt "E-Mail-Client (IMAP/Anhänge)"
mit Testfällen für Sync-Fehler, Anhang-Download-Fehler und
Autosave-Datumsfelder.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-11 15:42:18 +02:00
duffyduckandClaude Opus 4.7 70724e35dc Kündigungsdatum: Cursor bleibt beim Tippen im Feld
Bug: bei Eingabe der Jahreszahl im Kündigungsbestätigungs-Datum
sprang der Fokus raus und die Seite scrollte hoch. Ursache:
<input type=date> onChange feuert bei jedem Tastendruck. Sobald
"18.08.0002" ein gültiges Datum ergab, feuerte der Mutation-Aufruf
eine PUT-Request, die Server-Antwort re-renderte den Input und
der Cursor landete am Feldanfang – der Rest der Jahreszahl kam
im nächsten Feld an.

Fix: neue kleine AutosaveDateInput-Komponente – hält den Wert
während der Eingabe lokal, feuert onSave erst bei onBlur oder
Enter. Escape verwirft die Änderung. Kein Save bei unverändertem
Wert.

Angewendet auf beide betroffenen Stellen in ContractDetail
(Bestätigung erhalten am – Kündigungsschreiben + Options-Variante).

Andere Datumsfelder der App laufen entweder über react-hook-form
(batched, save auf Submit) oder halten den Wert lokal – die sind
vom Bug nicht betroffen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-11 15:32:25 +02:00
duffyduckandClaude Opus 4.7 f2703ed6b7 IMAP-Fehler: zentraler Humanizer + Auth-Fehler explizit
Der User sah beim Postfach-Sync stumpf "Command failed" – realer
Grund war ein abgelaufenes Postfach-Passwort. Der Bug betraf
Anhang-Download UND Sync und war der gleiche wie 2 Commits zuvor:
imapflow wirft `new Error('Command failed')` und legt Details in
`.responseText`/`.responseStatus` ab, wir haben sie nirgends
gelesen.

Fix: humanizeImapError() als zentraler Helper in imapService:
- Extrahiert responseText/responseStatus aus imapflow-Errors
- Erkennt Auth-Fehler ("authentication failed", "invalid
  credentials", NO+auth) und liefert klare Meldung mit
  Handlungsanweisung: "Passwort stimmt nicht mehr, bitte
  Zugangsdaten synchronisieren".
- Deckt zusätzlich Netzwerk/TLS/Timeout/UID-Stale ab.

Angewendet auf:
- fetchAttachmentInner (Anhang-Fetch)
- downloadAttachment-Controller (Response an UI)
- syncEmailsForAccount (Sync-Ergebnis + Toast)

Damit sieht der User im Toast künftig statt "Command failed" die
tatsächliche Ursache – gleicher Mechanismus für Sync und
Anhang-Download.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-11 15:28:43 +02:00
duffyduckandClaude Opus 4.7 be19e60a1b Email-Anhang: echte IMAP-Fehlermeldung durchreichen
Bug: Anhang-Download schlug mit "Fehler beim Herunterladen des
Anhangs: Command failed" fehl – ohne jeden Kontext, warum.

Ursache: imapflow wirft bei jedem IMAP-NO/BAD-Response nur
`new Error('Command failed')` und legt den echten Server-Grund
in `.responseText` / `.responseStatus` ab. Unser Code las nur
`.message` und verlor damit alle Information.

Fix:
- fetchAttachmentInner extrahiert responseText/responseStatus
  aus dem imapflow-Error und packt sie in die geworfene Meldung.
- downloadAttachment-Catch macht das gleiche, damit auch andere
  IMAP-Fehler (mailboxOpen, connect) mit sinnvollem Text
  durchkommen.
- Zusätzlicher Friendly-Mapping-Fall für "no longer exist" /
  "no such message" → klare Meldung, dass die E-Mail-Liste
  neu synchronisiert werden sollte.

Damit sieht der User im UI jetzt z.B. "NO Some of the requested
messages no longer exist" statt "Command failed" und weiß, dass
ein Resync hilft.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-11 14:32:42 +02:00
duffyduckandClaude Opus 4.7 96f8715e13 Bankkarte: Beschreibung in Vertrags-Ansicht + Select anzeigen
ContractDetail: Bankkarten-Card zeigt jetzt die description
kursiv unter Bankname (whitespace-pre-line für mehrzeilige
Notizen).

ContractForm: Bankkarten-Select hängt die Beschreibung kompakt
an das Label an (Zeilenumbrüche flach, auf 60 Zeichen gecapped)
– hilft beim Unterscheiden mehrerer Karten desselben Kunden.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-10 02:09:58 +02:00
duffyduckandClaude Opus 4.7 eeb89e09db Vertrag-Zugangsdaten: Externtab-Links neben Stressfrei-Select
Rechts neben dem Stressfrei-Adressen-Dropdown sitzen jetzt zwei
kleine Icon-Links (nur bei gewähltem Kunden sichtbar):
- ExternalLink → öffnet den Stressfrei-Tab des Kunden im neuen Tab
  (dort kann man die Adressen anlegen/pflegen)
- Mail → öffnet den E-Mail-Postfach-Tab des Kunden im neuen Tab

Damit muss man beim Vertragserstellen nicht mehr die Seite
wechseln, um schnell eine Stressfrei-Adresse anzulegen oder ins
Postfach zu springen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-09 13:34:55 +02:00
duffyduckandClaude Opus 4.7 e7f9aa65d4 Stressfrei-Email: Mailbox+Passwort per Default aktiv
Beim Anlegen einer neuen Stressfrei-E-Mail-Adresse: sobald die
Checkbox "Beim Provider anlegen" aktiv ist, wird jetzt automatisch
auch die zweite Checkbox "Echte Mailbox erstellen" mitgeschaltet.
Damit läuft `createEmail` mit `createMailbox: true` durch –
Backend generiert per generateSecurePassword ein Kennwort,
schickt es via -passwd an Plesk und speichert es verschlüsselt.

Vorher musste man beide Häkchen manuell setzen und übersah leicht
die zweite Checkbox → Adresse landete nur mit Weiterleitung ohne
IMAP/SMTP-Zugang bei Plesk.

Falls jemand nur eine reine Weiterleitung ohne Postfach will,
kann er die zweite Checkbox weiterhin abwählen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-09 12:34:24 +02:00
duffyduckandClaude Opus 4.7 616cb77c0d Bankkarte: optionales Beschreibung-Feld
Neues optionales Freitext-Feld `description` (TEXT NULL) auf
BankCard – z.B. "Geschäftskonto", "gemeinsames Konto mit Partner".
Migration mit IF NOT EXISTS.

Backend service create/update nimmt description entgegen.
Frontend:
- BankCard-Type um description ergänzt.
- BankCardModal: neue Textarea zwischen Ablaufdatum und Aktiv-
  Checkbox mit Placeholder-Hilfetext.
- Bank-Kartenübersicht zeigt die Beschreibung kursiv unter den
  bestehenden Feldern, falls gesetzt (whitespace-pre-line für
  mehrzeilige Notizen).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-09 12:22:17 +02:00
duffyduckandClaude Opus 4.7 e49133fd64 Pentest R104.1: Salutation-Endpunkte 404 statt 500
INFO-Finding: GET/PUT /api/customers/:id/salutation-preference
warfen bei nicht-existierendem Customer 500 statt 404.

canAccessCustomer prüft für Staff-User nur den Portal-Flag, kein
Existenz-Check. Der Service warf `new Error('Kunde nicht
gefunden')`, was der Controller-Catch generisch auf 500 mappte.
set/clear kamen als Prisma-P2003 (FK-Constraint) durch.

Fix:
- Neuer Helper assertCustomerExists() im customer.service, wirft
  ApiError(404, ...). Wird von get/set/clearSalutationPreference
  aufgerufen.
- Die drei Controller-Catches respektieren jetzt
  ApiError.statusCode → 404 kommt sauber durch, alle anderen
  Fehler bleiben bei 500.

Doku: docs/todo.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-06 09:19:42 +02:00
duffyduckandClaude Opus 4.7 3c190e43fb Pentest R102: contractNumber-Whitelist nachgezogen
INFO-Finding: die interne CRM-Vertragsnummer war im Update-Path
nicht durch validateContractIdentifier abgedeckt. <script>… →
stripHtml → leerer String → Vertragsnummer silent überschrieben
(R87.1-Pattern auf einem sechsten Feld).

Fix: contractNumber zu CONTRACT_IDENTIFIER_FIELDS hinzugefügt.
Bestehende Whitelist (^[A-Za-z0-9_\-/. ]{0,100}$) und Raw-Input-
Validierung greifen automatisch. Legitime generierte Nummern
(GAS-MPVJ4P10FE8) bleiben OK, <script>/Overflow/CRLF/Tags → 400.

Kein Migration- oder Frontend-Change – der Fix nutzt den
bestehenden R86-Mechanismus.

Doku in SECURITY-HARDENING.md § Runde 102 und docs/todo.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-06 00:40:08 +02:00
duffyduckandClaude Opus 4.7 57959cd782 Kunde: persönliche Du/Sie-Präferenz pro Mitarbeiter
Neue Tabelle UserCustomerSalutation (PK userId+customerId,
preference 'DU'|'SIE'). Fehlender Eintrag → Fallback auf
Customer.useInformalAddress (Kunden-Default).

Backend:
- customerService: get/set/clearSalutationPreference mit Fallback-
  Logik. Response enthält immer effektive Präferenz + `source`
  ('user' | 'customer-default'), damit die UI den Standard-Text
  anzeigen kann.
- customerController: 3 Endpunkte GET/PUT/DELETE
  /:customerId/salutation-preference. userId aus dem JWT, canAccessCustomer
  greift.

Frontend:
- customerApi: 3 neue Methoden.
- CustomerDetail: neues Feld "Anrede für mich" mit Du/Sie-Toggle
  und Zurücksetzen-Link. Wird nur Mitarbeitern angezeigt, nicht
  Portal-Usern.
- Bestehendes Feld "Anrede per" bekommt Hinweis "Standard für alle
  Mitarbeiter", damit die Semantik der beiden Felder klar ist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-06 00:34:51 +02:00
duffyduckandClaude Opus 4.7 d14db3de2f Vertragsauswahl: Kundennr./Zählernr./Telefonnr. in allen Pickern
Zentraler Helper contractLabel.ts liefert je nach Vertragstyp die
sinnvollen Zusatzinfos (nur wenn Daten vorhanden):
- Kdnr. <customer.customerNumber> (immer)
- Anbieter-Vtr. <contractNumberAtProvider>
- Zähler <meterNumber> (Strom/Gas)
- Mobil <main-SIM-Nummer> (Mobilfunk)
- Festnetz <main-phone> (Internet)

Backend contractApi.getAll include um energyDetails.meter und
internetDetails.phoneNumbers erweitert – vorher fehlten die im
Response.

Angewendet auf 5 Call-Sites:
- AssignToContractModal (Email-zu-Vertrag zuordnen)
- TaskList (Mitarbeiter- und Kundenportal-"Neue Aufgabe/Anfrage")
- Dashboard (Support-Anfrage-Modal)
- PdfTemplates (Test-Vorlage-Kontext-Auswahl)
- ContractForm (Vorgänger-Vertrag-Select)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-06 00:26:42 +02:00
duffyduckandClaude Opus 4.7 0b78323c7b Vertrag: neuer Status ONGOING (Läuft weiter auf unbestimmte Zeit)
Enum-Wert ONGOING neu, MariaDB-Migration (ans Ende gehängt, damit
keine Rows umgeschrieben werden). Prisma Client neu generiert.

Cockpit-Logik: ONGOING wird in die Query mit reingeholt (damit
DSGVO-/Consent-Warnungen weiter greifen), aber alle Fristen-
Blöcke pro Vertrag geskippt:
- Kündigungsfrist
- Vertragsende
- Zwischenrechnung-Frist

Kein Skip für Daten-Qualität (fehlende Adresse, Bank, Portal-
Zugang etc.) – die sind auch für unbefristete Verträge relevant.

Frontend: statusLabels/statusVariants/statusDescriptions in
ContractDetail, ContractList, CustomerDetail und
ContractDetailModal um ONGOING ergänzt. Status-Select im
ContractForm bekommt die neue Option zwischen ACTIVE und
CANCELLED.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-06 00:22:03 +02:00
duffyduckandClaude Opus 4.7 cc6fa9be47 Aufgaben: erledigte trotzdem bearbeiten + Nachträge anlegen
Drei Frontend-Guards raus, die verhindert haben, dass man an
erledigten Aufgaben noch etwas machen kann:

- TaskList: der Reply-/Subtask-Input war für erledigte Aufgaben
  komplett ausgeblendet. Jetzt sichtbar mit angepasstem Placeholder
  ("Nachtrag zu erledigter Aufgabe…" / "Antwort zu erledigter
  Anfrage…").
- ContractDetail: der Subtask-Input war für erledigte Aufgaben aus.
  Jetzt sichtbar – man muss die Aufgabe nicht mehr als offen
  markieren, nur um einen Nachtrag anzuhängen.
- ContractDetail: der Edit-Button war für erledigte Aufgaben aus.
  Jetzt immer verfügbar (Löschen war eh schon da).

Backend hatte keine Restriktion, es waren rein UI-Gate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-04 00:55:58 +02:00