Commit Graph

21 Commits

Author SHA1 Message Date
duffyduck fcc3b04725 Vertrag: Kunden-/Vertragsnummer bei Vertriebsplattform
Viele Vertriebsplattformen vergeben eigene Nummern, die nicht mit
denen des Endanbieters identisch sind. Zwei neue optionale Felder
unter "Anbieter & Tarif".

- Schema: Contract.customerNumberAtSalesPlatform +
  contractNumberAtSalesPlatform, Migration mit IF NOT EXISTS.
- ContractForm: zwei neue Inputs direkt unter den entsprechenden
  Provider-Feldern.
- ContractDetail: eigene Zeilen mit CopyButton.
- Audit-Log-Mapping + Renewal-Copy + XSS-Strip-Whitelist mitgezogen.
- Bonus: contractNumberAtProvider war im Renewal-Copy und Audit-
  Label-Mapping fehlend – mitkorrigiert.
2026-06-03 18:13:17 +02:00
duffyduck 5508d59652 SIM-Karten: Checkbox "eSIM" zwischen Hauptkarte und Multisim
Hardware-Plastikkarte vs. eSIM-Profil ist eigene Eigenschaft – eSIM
kann sowohl Hauptkarte als auch Multisim sein, deshalb dritter
Toggle statt entweder/oder.

- Schema: SimCard.isEsim Boolean default false, Migration mit
  IF NOT EXISTS.
- Backend: vier SimCard-Schreibpfade in contract.service.ts (Create,
  Update, Follow-Up, Renewal).
- UI: dritte Checkbox in ContractForm zwischen Hauptkarte und
  Multisim. ContractDetail zeigt blauen eSIM-Badge.
2026-06-03 16:13:24 +02:00
duffyduck 9482424ade Pentest 57.7 MEDIUM + 57.8 MEDIUM: Consent-Hash-TTL + Zip-Slip-Härtung
57.7 (Consent-Hash ohne TTL):
- Neues Feld Customer.consentHashExpiresAt + Migration
  20260601300000_consent_hash_ttl mit IF NOT EXISTS. Bestandsdaten
  bekommen NOW()+30d als Default, damit frische Versand-Links nicht
  sofort sterben.
- TTL-Konstante CONSENT_HASH_TTL_DAYS = 30 in consent-public.service.
- getCustomerByConsentHash + grantAllConsentsPublic liefern null bzw.
  klare Fehlermeldung bei Ablauf; consentHashExpiresAt wird nicht in
  der Response durchgereicht (kein Oracle "unbekannt vs. abgelaufen").
- ensureConsentHash erneuert Hash + Frist, sobald der alte abgelaufen
  ist – Versand neuer Links bleibt friction-frei.
- consentHashExpiresAt in SENSITIVE_CUSTOMER_FIELDS (sanitize), damit
  der Standard-Customer-Endpoint kein Workflow-Info leakt.

57.8 (Zip-Slip / Zip-Bomb):
- Reject zusätzlich: leere Entry-Namen, Backslashes (Cross-OS-
  Confusion), Home-Dir-Expansion (`~`), explizite `..`-Segmente
  schon im Original-Namen (vor path.resolve).
- Zip-Slip-Check auf path.relative umgestellt – robuster als
  startsWith(prefix + sep), insbesondere bei nested Resolution.
- Zip-Bomb-Schutz: 500 MB pro Entry + 5 GB Gesamt-Uncompressed-
  Limit; bei Überschreitung Abbruch mit klarer Meldung.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 21:13:06 +02:00
duffyduck 61daff8df9 Rufnummern: Vorwahl als eigenes Feld – verlässliche PDF-Befüllung
Bisher steht in PhoneNumber.phoneNumber die kombinierte Nummer
("04264 836975"). Die Wechselauftrag-PDFs splittten heuristisch
auf Vorwahl/Anschluss, was bei Sonderformaten daneben ging.

Schema: PhoneNumber.areaCode String? (optional, Bestandsdaten
werden beim nächsten Edit nachgepflegt). Migration
20260601200000_phone_area_code mit IF NOT EXISTS.

ContractForm: aus "Rufnummer" werden zwei Felder – "Vorwahl" und
"Rufnummer". Beim Speichern sendet das Frontend areaCode separat
UND die kombinierte phoneNumber (für Listen/Suchen weiter
unverändert). Beim Edit-Load wird areaCode bevorzugt; falls leer,
splittet die UI heuristisch und prefillt beides – User kann
korrigieren und beim Speichern wird der saubere Wert persistiert.

PDF-Template-Service: phoneAreaCode[N] und phoneLocal[N]
verwenden jetzt primär den gespeicherten areaCode aus der DB
(verlässlich), Heuristik nur als Fallback für Altbestand. Die
Template-Variablen-Liste war bereits korrekt definiert, jetzt
ist die Datenquelle solide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 14:10:25 +02:00
duffyduck 4acfd9de1c SIM-Karten: Feld "Kartennutzer" für Firmen-/Familienverträge
Bei Firmenverträgen (Vertragsinhaber = Firma, Nutzer = Mitarbeiter)
und Familienverträgen (Inhaber = Eltern, Nutzer = Kind) brauchten
wir ein Feld, das den tatsächlichen Nutzer der SIM-Karte erfasst.

Backend: SimCard.cardUser (String?, optional), Migration
20260601100000_sim_card_user mit IF NOT EXISTS. Im Service durch
Create + Update propagiert.

Frontend: Input "Kartennutzer" pro SIM-Karte in ContractForm
(eigene Zeile oberhalb der technischen Felder Rufnummer/SIM-Nr/
PIN/PUK). In ContractDetail wird der Nutzer als "Nutzer: <Name>"
neben den Hauptkarte/Multisim-Badges angezeigt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 08:10:16 +02:00
duffyduck ad4c2bae1d Folgezähler-Deklaration in der Kundenakte (Auto-Propagation)
- Meter.predecessorMeterId (Self-Relation) + Migration
  20260530140000_meter_predecessor mit IF NOT EXISTS
- createMeter akzeptiert optional successorOf:
  {predecessorMeterId, installedAt?, finalReadingPrevious?}.
  Vorgänger wird validiert (gleicher Kunde + Typ); alle Verträge
  mit dem Vorgänger als aktuellen Zähler werden analog zu
  addSuccessorMeter automatisch auf den neuen Zähler umgestellt
  (ContractMeter-Eintrag mit removedAt/finalReading für den
  Vorgänger, neuer ContractMeter mit installedAt + nächster
  Position, energyDetails.meterId aktualisiert)
- MeterModal: Checkbox "Als Folgezähler deklarieren" + Dropdown
  Vorgänger + Wechseldatum + Endstand. Typ/Tarifmodell/Adresse
  werden vom Vorgänger übernommen und disabled. Info-Banner über
  Vertragsauto-Update

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 13:48:23 +02:00
duffyduck c099b41796 Zähler → Lieferadresse-Pflichtfeld + Vertragsfilter
- Meter.addressId (FK → Address, ON DELETE SET NULL) + Migration
  20260530100000_meter_address mit IF NOT EXISTS
- Service erzwingt beim Create: Lieferadresse vorhanden + zum
  Kunden gehörig + Typ DELIVERY_RESIDENCE
- MeterModal: Pflicht-Dropdown "Lieferadresse"; Save disabled
  ohne Adresse; Hinweis-Banner. Bestandszähler ohne Adresse zeigen
  "nicht zugeordnet – bitte über Bearbeiten nachpflegen"
- ContractForm: Zähler-Dropdown filtert auf Vertrags-Lieferadresse;
  deaktivierte Zähler bleiben sichtbar mit "(deaktiviert)"; bei
  Auswahl Toast-Warnung wegen möglichem Altvertrag

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 13:18:24 +02:00
duffyduck 20d42c5270 Energie-Bonus aufgeteilt in Sofort + Neukunden
EnergyContractDetails.bonus war ein einzelnes Feld. Strom-/Gas-
Verträge haben aber typischerweise zwei Boni (Sofort beim Wechsel
+ Neukunden-Bonus nach 12 Monaten), die getrennt verbucht werden
müssen.

Migration 20260524100000_split_energy_bonus:
- ADD COLUMN IF NOT EXISTS instantBonus, newCustomerBonus
- bestehende `bonus`-Werte → instantBonus (Annahme: Sofort)
- DROP COLUMN IF EXISTS bonus

UI:
- ContractForm zeigt zwei Input-Felder
- Detail-Ansicht zeigt beide einzeln + Gesamtbonus
- Kostenvorschau listet beide einzeln, dann Gesamt, dann effektive
  Jahreskosten

Cost-Calc: calculateCosts() bekommt beide Boni; CostCalculation
liefert instantBonus, newCustomerBonus, totalBonus.

PDF-Template: drei neue Variablen energyDetails.instantBonus,
.newCustomerBonus, .totalBonus.

Live-verifiziert auf dev: PUT mit beiden Werten → DB persistiert,
GET liefert zurueck.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:27:54 +02:00
duffyduck 37df8c0c4a Backup-Operations-Log + EBUSY-Fix beim Restore
Backup-Seite zeigt zwei neue Log-Panels: links Backup-Erstellung,
rechts Backup-Wiederherstellung. Jeder Eintrag mit ✓/✗-Status,
Summary, Timestamp + User. Klick öffnet Modal mit vollständigem
Verlauf – alle console.log/error/warn/info-Zeilen werden während
der Operation in einen Puffer mitgefangen und im fullLog-Feld
persistiert. Auto-Refresh alle 5s.

Persistenz: neue Tabelle BackupLog mit Migration
20260519100000_backup_log (CREATE TABLE IF NOT EXISTS für Re-Deploys
auf DBs mit Vorab-db-push). fullLog auf 1 MB gecappt.

Endpoints (settings:update):
- GET /api/settings/backup-logs?operation=CREATE|RESTORE&limit=50
- GET /api/settings/backup-logs/:id

EBUSY-Fix: Der neue Log-Verlauf hat sofort einen alten Bug
sichtbar gemacht. backup.service.restoreBackup rief
deleteDirectory(UPLOADS_DIR) auf, dessen finales rmdirSync auf
/app/uploads ein EBUSY warf – das Verzeichnis ist im Container ein
Bind-Mount und lässt sich nicht aushängen. Fix: neuer Helper
emptyDirectory() löscht nur die Inhalte, das Verzeichnis bleibt
stehen.

Live-verifiziert: 4867 Datensätze + 1 Datei in 13.2s
wiederhergestellt; Log-Modal zeigt den vollständigen Verlauf.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 11:53:04 +02:00
duffyduck b87a2a3d08 fix: Cleanup im echten Entrypoint + idempotente Migration
Beim Audit der Container-Pipeline zwei Bugs gefunden:

1) backend/docker-entrypoint.sh (= der wirklich ausgeführte Entrypoint
   laut Dockerfile) ruft jetzt das Cleanup-Script auf. Der Cleanup-
   Aufruf hing bisher fälschlich in docker/entrypoint.sh – ein
   alternatives Setup, das von der Standard-Compose-Konfiguration
   NICHT genutzt wird. Folge: das Cleanup ist auf prod nie gelaufen.

2) Migration 20260516173552_portal_password_must_change nutzt jetzt
   `ADD COLUMN IF NOT EXISTS`. Auf prod-DBs, die zwischen den Runden
   per `prisma db push` updated wurden (z.B. weil der erste Build
   mit `db push` provisioniert war), existiert die Spalte bereits.
   Ohne IF NOT EXISTS würde migrate deploy beim Hochziehen einer
   neueren Version mit "Duplicate column" abbrechen.

MariaDB ≥ 10.0.2 + MySQL ≥ 8.0.27 unterstützen IF NOT EXISTS für
ALTER TABLE ADD COLUMN – beides ist in unserer Compose-Konfig
abgedeckt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:24:33 +02:00
duffyduck a7d12b8540 Security-Hardening Runde 7: Pentest Runde 3 (3 Findings)
KRITISCH – Privilege Escalation:
POST /api/developer/setup war ohne Auth erreichbar und konnte
developer:access der Admin-Rolle hinzufügen → volle DB-Kontrolle
via /developer/*-Routen. Endpoint ersatzlos entfernt; manuelles
Setzen geht über prisma/add-developer-permission.ts (CLI).

HOCH – Fehlende Migration auf Prod:
portalPasswordMustChange war im Code, aber prod-DB hatte die
Spalte nicht → jeder Kunden-Login warf Prisma-Schema-Error → DoS.
Root Cause: db push statt migrate dev während Entwicklung →
kein Migration-File im Repo. Fix: handgenerierte Migration
20260516173552_portal_password_must_change/migration.sql, lokal
mit migrate resolve --applied registriert, durch shadow-DB-Reset
verifiziert. entrypoint.sh führt migrate deploy bereits aus.

MITTEL – Prisma-Internals-Leak im Login-Error:
error.message wurde 1:1 an den Client gegeben → bei DB-Schema-
Fehlern leakten Tabellen- und Spaltennamen. Whitelist-Filter
safeLoginError() in auth.controller.ts: nur 'Ungültige
Anmeldedaten' und 'E-Mail und Passwort erforderlich' werden
durchgereicht, alles andere wird zu generischem 'Anmeldung
fehlgeschlagen' maskiert. Original landet im Server-Log.

Live-verifiziert:
- POST /api/developer/setup → HTTP 404
- Falsches Customer-PW → 'Ungültige Anmeldedaten' (keine Internals)
- Spalte testweise gedropped → 'Anmeldung fehlgeschlagen' (generisch),
  Original-Message nur im Server-Log
- Shadow-DB-Reset + migrate deploy → Spalte korrekt erzeugt

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:39:02 +02:00
duffyduck 27a0fdbc45 db: Prisma-Migrations-System statt db push (datenerhaltend)
`db push --accept-data-loss` konnte bei Schema-Änderungen still Daten verlieren
(Renames, Type-Changes, NOT NULL ohne Default). Umstellung auf versionierte
Migrations:

- 0_init aus aktuellem Schema generiert (alte gedriftete Migrations entfernt)
- entrypoint: Auto-Baseline für bestehende DBs ohne `_prisma_migrations`,
  dann `migrate deploy` (idempotent, kein Daten-Loss)
- npm run schema:sync: legt automatisch eine Migration mit Zeitstempel an
  (`prisma migrate dev --name auto_<ts>`)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 17:02:35 +02:00
duffyduck c3edb8ad2e gdpr audit implemented, email log, vollmachten, pdf delete cancel data privacy and vollmachten, removed message no id card in engergy car, and other contracts that are not telecom contracts, added insert counter for engery 2026-03-21 11:59:53 +01:00
duffyduck e0fc26795e added contract history 2026-02-08 19:24:37 +01:00
duffyduck 4f588015a4 contractnumber provider added, old provider number field only if no previous contact exist 2026-02-08 14:34:56 +01:00
duffyduck 2ab2bb7562 snooze vor expired, contracts, display snoozed contracts if an item is missing, un snooze implemented, fixed invoice upload bug 2026-02-08 13:08:58 +01:00
duffyduck ad2b8ea5b6 added invoices and status in cockpit, created info button for contract status types 2026-02-08 01:18:12 +01:00
duffyduck e8919cfa81 added extra field kwh at m3, expand cost field to 10 komma, added maloid,counter add dialog, auto set unit 2026-02-05 20:34:45 +01:00
duffyduck f21eb20715 seperate delivery and billig adresses in contract added 2026-02-04 08:48:25 +01:00
duffyduck 8c9e61cf17 added backup and email client 2026-02-01 00:02:35 +01:00
Stefan Hacker e209e9bbca first commit 2026-01-29 01:16:54 +01:00