Schliesst den nach R166/R167 verbliebenen Grenzfall: Bis Version 2 war die
Kette selbsttragend - wer die DB schreiben kann, konnte jede Zeile aendern und
alle Folgehashes konsistent nachziehen, die Pruefung meldete "gueltig".
Version 3 signiert denselben Inhalt per HMAC-SHA256 mit AUDIT_HMAC_KEY, einem
Schluessel ausserhalb der Datenbank. Ohne ihn laesst sich keine gueltige
Signatur erzeugen; reiner DB-Schreibzugriff genuegt nicht mehr.
Fail-safe: Ohne Schluessel wird weiter Version 2 geschrieben, es faellt nichts
aus. Signierte Zeilen gelten dann als nicht pruefbar (unverifiableEntries) und
ausdruecklich nicht als manipuliert. AUDIT_HMAC_KEY_OLD erlaubt einen
Schluesselwechsel ohne Rehash.
Restluecke der Versionsgrenze geschlossen: Wird die FRUEHESTE Zeile einer Stufe
herabgestuft, wandert MIN(id) mit - die Grenze allein haette den Downgrade
durchgewunken (der erste Testlauf fiel genau darauf durch). Der Nachfolger ist
jedoch HMAC-signiert und sein previousHash ohne Schluessel nicht faelschbar;
eine unerklaerte Luecke vor einer signierten Zeile gilt deshalb als Befund.
Verifiziert: Inhalt geaendert -> erkannt; Downgrade 3->2 auf der fruehesten
V3-Zeile -> erkannt; dasselbe auf der letzten V3-Zeile (kein Nachfolger) ->
erkannt; ohne Schluessel 0 manipuliert / 2 nicht pruefbar; 40 parallele
Schreiber -> 40/40, 0 Forks, alle V3. tsc + vite build gruen.
AUDIT_HMAC_KEY in .env.example dokumentiert. Der Schluessel selbst liegt nur
lokal in .env (gitignored).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
verifyIntegrity waehlte die Pruefstaerke nach der von der Zeile selbst
deklarierten hashVersion - und die ist nicht gehasht. Angriff: hashVersion 2->1
setzen, die nur von V2 abgedeckten Felder aendern (success false->true,
errorMessage leeren, resourceLabel umschreiben) und den schwachen V1-Hash ueber
die 7 unveraenderten Felder nachziehen. Ergebnis: Fehl-Login als Erfolg
getarnt, Pruefung meldet "gueltig". An der letzten Zeile der Kette entsteht
dabei nicht einmal ein Gap - dauerhaft unsichtbar.
Fix: Version-Floor. Die erwartete Pruefstaerke leitet sich aus der Kette ab
(MIN(id) WHERE hashVersion >= 2), nicht aus der Selbstauskunft. Ab dieser
Grenze muss jede Zeile V2 sein; weicht die deklarierte Version ab, gilt die
Zeile selbst als manipuliert. Geprueft wird immer mit dem erwarteten Verfahren.
Die Grenze laesst sich durch Herabstufen einzelner Zeilen nicht verschieben.
Zusaetzlich konsultiert die Pruefung jetzt das Loeschungs-Manifest: neu
unexplainedGaps - nur Luecken ohne protokollierte Loeschung sind
erklaerungsbeduerftig. Vorher war das Manifest rein informativ, wodurch sich
eine boeswillige Loeschung als harmloser Gap tarnen konnte.
Verifiziert (PoC nachgebaut): Downgrade mit Nachfolger erkannt, Downgrade der
Tail-Zeile erkannt, Gegenrichtung (V1 faelschlich als V2) erkannt, keine
Falschmeldungen auf Bestandsdaten. tsc + vite build gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Problem 1 (Deutbarkeit): verifyIntegrity warf zwei voellig unterschiedliche
Befunde in einen Topf und meldete beides als "N manipulierte Eintraege". Eine
harmlose Verkettungsluecke sah damit aus wie ein Angriff - die Meldung war im
Alltag nicht deutbar und dadurch wertlos, dasselbe Muster wie beim
Refresh-Rauschen.
Fix: Rueckgabe um tamperedEntries (Inhalt nachtraeglich veraendert, ernst) und
chainGaps (Verkettung unterbrochen durch parallele Schreibvorgaenge oder
geloeschte Zeilen, meist harmlos) erweitert. invalidEntries bleibt als Summe
erhalten. Controller formuliert die Meldung eindeutig, Frontend-API-Typ
nachgezogen.
Problem 2 (Aufbewahrung): Token-Refreshes landen seit der Entrauschung als
Authentication/LOW. Diese Kombination traf auf keine spezifische Regel und fiel
in die Auffangregel * mit 3650 Tagen - das Rauschen waere 10 Jahre aufbewahrt
worden, echte Logins nur 2. Fix: Regel Authentication/LOW mit 90 Tagen, als
idempotente Migration und im Seed.
Sensitivitaet steuert die Aufbewahrung und ist keine Alarmstufe - normale
Logins und Zugriffe auf Bankdaten/Ausweise bleiben bewusst CRITICAL, ein
Herabstufen wuerde still die Aufbewahrungsfrist verlaengern.
Verifiziert: Live-Test gegen Dev-DB - echte Manipulation einer Zeile wird als
manipuliert erkannt und nicht mit Luecken verwechselt, Ketten-Luecken bleiben
bei 7, Originalzustand exakt wiederhergestellt. tsc + vite build gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
POST /auth/refresh wurde als CREATE / "Anmeldung erstellt" / CRITICAL /
anonymous geloggt und sah damit wie eine anonyme Login-Flut aus. Es ist
aber der regulaere Silent-Refresh des Frontend-Interceptors (Access-Token
lebt nur im Speicher -> nach Reload/401 einmaliger Cookie-Refresh).
- Neuer AuditAction-Wert TOKEN_REFRESH (Migration 20260818120000,
idempotentes MODIFY COLUMN)
- determineAction() mappt /auth/refresh -> TOKEN_REFRESH, Label
"Sitzung verlaengert (Token erneuert)", Sensitivitaet explizit LOW
(statt Default Authentication -> CRITICAL)
- LOGIN/LOGOUT/LOGIN_FAILED bleiben unveraendert CRITICAL
- Frontend: Filter-Option + dezente Badge-Farbe + Typ-Union
- anonymous bewusst beibehalten (Endpoint ohne authenticate-Middleware)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bestandskunden ohne E-Mail bleiben editierbar. Beim Anlegen ist E-Mail
weiterhin Pflicht (Frontend required + Backend createCustomer). Die
Domain-Pruefung (keine verwaltete Provider-Domain) greift unveraendert bei
create UND update, falls eine E-Mail gesetzt wird.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ContractTask.contractId nullable (Migration). Neuer staff-only Endpoint
POST /tasks fuer allgemeine Aufgaben ohne Vertrag/Kunde. Ohne Vertrag gibt
es keinen Kunden -> visibleInPortal serverseitig immer false, Portal-Reply
403 bei contractloser Aufgabe, getAllTasks-Portal-Filter schliesst sie
automatisch aus (kein contract-Match).
Task-Modal (Mitarbeiter): Checkbox "Ohne Kunde (allgemeine Aufgabe)" blendet
Kunden-/Vertragsauswahl UND "Im Kundenportal sichtbar" aus. Task-Liste zeigt
solche Aufgaben als "Allgemeine Aufgabe (ohne Vertrag)" ohne Vertrags-Link/
Zum-Vertrag-Button.
Verifiziert: contractlose Aufgabe -> contractId null, visibleInPortal
erzwungen false (auch wenn true geschickt); mit Vertrag weiterhin waehlbar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Die private Kunden-E-Mail (Customer.email) darf nicht auf einer der bei den
E-Mail-Providern konfigurierten Domains (bzw. Subdomains) liegen - sonst
traegt man versehentlich eine unserer verwalteten Weiterleitungs-/Mailbox-
Adressen als private Adresse ein. Zudem ist E-Mail jetzt Pflichtfeld.
Backend: createCustomer/updateCustomer pruefen Pflicht + Domain (neue Helper
getConfiguredEmailDomains/emailUsesDomain im emailProvider-Service);
email aus nullableFields entfernt. Frontend: E-Mail-Feld required + Hinweis.
Verifiziert: Provider-Domain + Subdomain (case-insensitiv) verboten,
Fremd-Domains erlaubt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neues Feld EnergyContractDetails.noBonusDesired (Boolean, default false) +
Migration. Checkbox im Vertragsformular (Strom/Gas, bei den Bonus-Feldern),
Anzeige im Vertragsdetail wenn gesetzt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MaLo-ID (Marktlokation) gehoert zur (Liefer-)Adresse, nicht zum Vertrag.
Address bekommt maloIdElectricity + maloIdGas (getrennte Marktlokationen je
Sparte), pflegbar im AddressModal (nur Lieferadresse). Im Vertrag ist die
MaLo-ID jetzt ein Lesefeld, das je nach Vertragstyp die MaLo der gewaehlten
Lieferadresse zeigt; ContractDetail/-Modal ebenso.
Schema + Migration 20260814100000: 2 Spalten (idempotent) + Daten-Migration
(bestehende EnergyContractDetails.maloId -> jeweilige Lieferadresse,
ELECTRICITY->maloIdElectricity / GAS->maloIdGas). Migrationslogik verifiziert.
Dabei einen selbst verursachten Regressions-Bug gefixt: beim R156-Umbau waren
die 10 owner*-Adressfelder aus der Address-Whitelist gefallen -> Eigentuemer-
Sektion speicherte seit cb21a2c nicht mehr. Address-Whitelist jetzt via
Pick-Helper, programmatisch gegen alle DB-Spalten abgeglichen (owner* + MaLo
drin, id/customerId/Timestamps raus). BankCard/Document gegengeprueft: ok
(nur documentPath bewusst upload-only ausgeschlossen).
Verifiziert: tsc+build gruen; owner + maloId speichern wieder, Injection
(id/customerId) blockiert; Daten-Migration Strom->Strom / Gas->Gas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>