Files
opencrm/docs/todo.md
T
duffyduck 08310ac302 security: CRITICAL IDOR-Fix auf Stressfrei-Email-Sub-Routes
Pentest hat einen echten Credential-Exfiltration-Angriff erfolgreich
durchgespielt: als Portal-User von Kunde A komplette Klartext-IMAP/SMTP-
Zugangsdaten der Mailbox von Kunde B abgreifbar.

Root Cause: GET /api/stressfrei-emails/:id hatte canAccessStressfreiEmail-
Check, ALLE 8 Sub-Endpoints unter :id/* hatten nur `authenticate +
requirePermission('customers:read')` — was jeder Portal-User de facto hat.

Betroffene Controller (alle gefixt mit canAccessStressfreiEmail als erster
Zeile):

stressfreiEmail.controller.ts:
- updateEmail (PUT /:id)
- deleteEmail (DELETE /:id)
- resetPassword (POST /:id/reset-password)

cachedEmail.controller.ts:
- getMailboxCredentials (GET /:id/credentials) ← KRITISCHSTER, lieferte
  Klartext-IMAP/SMTP-Passwort + Server-Daten der fremden Mailbox
- getFolderCounts (GET /:id/folder-counts)
- syncAccount (POST /:id/sync)
- sendEmailFromAccount (POST /:id/send) — fremde Mailbox zum Versand
  missbrauchbar
- enableMailbox (POST /:id/enable-mailbox)
- syncMailboxStatus (POST /:id/sync-mailbox-status)

Security-Monitor: canAccessResourceByCustomerId emittiert bei jedem
Fehlversuch ein ACCESS_DENIED MEDIUM-Event. Threshold-Detection erzeugt
bei >5 Versuchen in 5 min ein CRITICAL SUSPICIOUS-Event + Sofort-Alert.

Live-verifiziert (Portal-User Kunde A versucht Email-ID von Kunde B):
- alle 8 Sub-Routes → HTTP 403
- eigene Email-ID → 200/400 (Ownership-Check OK)
- 8× ACCESS_DENIED MEDIUM im Security-Monitor

Doku in docs/SECURITY-HARDENING.md als Runde 13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:47:54 +02:00

480 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 📋 OpenCRM Todo-Liste
---
## 🔜 Offen
### Manuelle Tests (vor Release durchklicken)
Checklisten für Security + Email-Log-System stehen in **[TESTING.md](./TESTING.md)**.
Einmal komplett durchlaufen vor v1.0.0-Release.
### 🚀 SaaS-Ausbau: Instance-per-Customer + Admin-Portal + GoCardless
**Vision:** OpenCRM als SaaS anbieten. Jeder Kunde bekommt seine eigene
isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
über ein zentrales Admin-Portal.
**Architektur-Entscheidung:** Weg C (Instance-per-Customer)
- Pro Kunde eine eigene Docker-Instanz mit eigener DB
- Keine `tenantId` im CRM-Code → keine Security-Risiken durch vergessene Filter
- Komplette Datenisolation (DSGVO-freundlich)
- Updates können gestaffelt ausgerollt werden (erst 10% testen)
- Bei Kündigung: Docker-Image + DB-Export als "Mitnehm-Paket"
**Bewusst NICHT dabei:** eigener Mailserver. Stattdessen Plesk-Integration
(die wir schon haben) Kunde bekommt Mail-Zugang über unseren Plesk bei Bedarf.
---
**Admin-Portal (separate App, neben den CRM-Instanzen):**
- Kundenverwaltung: wer hat welchen Plan, Status (Trial/Active/Suspended/Cancelled)
- "Neuen Kunden anlegen" → Provisioning-Script
- DB anlegen (Master-DB kennt die Mapping)
- Docker-Container starten
- Subdomain konfigurieren (`kundenname.deincrm.de` via Caddy/Traefik)
- Initial-Admin-Account erstellen + Einladungs-Email senden
- Optional: Factory-Defaults für Stammdaten einspielen
- GoCardless-Integration (Webhook + Dashboard)
- Instanz-Management: Pause/Resume bei Zahlungsproblemen
- Logs & Metriken pro Instanz (optional)
- Support-Bereich (Tickets? oder einfach E-Mail)
---
**Abrechnung mit GoCardless (gocardless.com):**
- Zahlungsmethoden: SEPA-Lastschrift (Hauptfokus) + Kreditkarte (über GoCardless Embedded/Success)
- 30 Tage kostenlose Testphase ohne Zahlungsmittel
- Nach Trial: Mandats-Erfassung → regelmäßige Abbuchung
- Mehrere Pläne (z.B. Basic / Pro / Enterprise) mit unterschiedlichen Features
- Webhook-Endpoint im Admin-Portal:
- `payment_confirmed` → Instanz aktiv lassen
- `payment_failed` → Banner im CRM, nach X Tagen pausieren
- `mandate_cancelled` → Kündigungs-Flow
- Rechnungsstellung: GoCardless liefert Zahlungsbelege, aber **echte Rechnungen**
(mit USt-ID, Rechnungsnummer etc.) müssen wir selbst generieren
(evtl. über das existierende PDF-Template-System aus dem CRM nutzen)
---
**Provisioning-Flow (grober Entwurf):**
1. Kunde registriert sich auf Landing Page (Name, Firma, E-Mail, Wunsch-Subdomain)
2. Admin-Portal: Trial-Instanz starten
- DB erstellen, Docker-Container hochfahren, Caddy-Config für Subdomain
- Einladungs-Email mit Admin-Login + Passwort-Reset-Link
3. Tag 25: Erinnerungs-Email "Deine Trial läuft bald ab"
4. Tag 30: Banner im CRM "Jetzt bezahlen oder pausieren"
5. Kunde erfasst GoCardless-Mandat im Admin-Portal-Login
6. Bei erfolgreicher Zahlung: Instanz bleibt aktiv
7. Bei fehlender Zahlung nach 7 Tagen: Instanz pausiert (DB bleibt, UI zeigt Hinweis)
---
**Technische Bausteine für später:**
- Master-DB mit Tenant-Tabelle (Name, Subdomain, DB-Name, Plan, Status, GoCardlessIDs)
- Caddy oder Traefik als Reverse-Proxy mit Auto-SSL (Let's Encrypt)
- Docker-Orchestrierung: einzelne `docker-compose.yml` pro Kunde oder Docker-Swarm/K8s
- Backup-Strategie: pro Tenant separate Backups + zentrale Master-DB-Backups
- Monitoring: ein Fail macht nicht alle down, aber wir müssen es mitbekommen
- Logs zentral: z.B. Loki + Grafana für aggregierte Logs aller Instanzen
---
**Grobe Zeitschätzung:**
- Admin-Portal (MVP): ~1 Woche
- GoCardless-Integration + Webhooks: ~3-5 Tage
- Provisioning-Automatisierung (Docker + Caddy): ~1 Woche
- Landing Page + Checkout: ~3-5 Tage
- Tests + Polishing: ~1 Woche
- **Gesamt: ~3-4 Wochen**
**Vorbereitung JETZT (einfach, macht später Arbeit leichter):**
- ✅ Factory-Defaults System (schon erledigt, hilft beim Provisioning)
- ✅ Domain/Label dynamisch per Provider (schon erledigt)
- Docker-Compose aufräumen, Env-Variablen dokumentieren (klein, ein Tag)
- Backup-Script robust + wiederherstellbar (haben wir schon weitgehend)
---
## ✅ Erledigt
- [x] **🚨 KRITISCH: IDOR auf Stressfrei-Email-Sub-Routes (Pentest-Fund)**
- **Realer Angriff erfolgreich durchgespielt**: Portal-User konnte über
`/api/stressfrei-emails/{id}/credentials` die kompletten Klartext-
IMAP/SMTP-Zugangsdaten der Mailbox eines anderen Kunden abrufen.
- **Root Cause**: der Haupt-Endpoint `GET /:id` hatte
`canAccessStressfreiEmail`-Check, die **8 Sub-Endpoints** unter
`:id/*` hatten alle KEINEN Ownership-Check (nur `authenticate +
requirePermission('customers:read')`, was Portal-User von Haus aus
haben).
- **Fix**: `canAccessStressfreiEmail(req, res, id)` als erste Zeile in
allen 9 betroffenen Controllern: `getMailboxCredentials`,
`getFolderCounts`, `syncAccount`, `sendEmailFromAccount`,
`enableMailbox`, `syncMailboxStatus`, `resetPassword`, `updateEmail`,
`deleteEmail`.
- **Security-Monitor**: `canAccessResourceByCustomerId` emittiert
bei jedem Fehlversuch automatisch ein `ACCESS_DENIED MEDIUM`-Event
→ Threshold-Detection (>5 in 5 min) erzeugt `CRITICAL SUSPICIOUS` +
Sofort-Alert.
- **Live-verifiziert**: Portal-User Kunde A probiert Email-ID von
Kunde B durch alle 8 Sub-Routes → **alle 8× HTTP 403**, eigene
Email-ID kommt sauber durch (200/400), 8× `ACCESS_DENIED`-Events
im Security-Monitor.
- [x] **🛡️ JWT-Tokens raus aus localStorage Refresh-Cookie-Pattern**
- Pentest-Finding „JWT in localStorage (MITTEL)": bei XSS könnte JS
den Token klauen + alle Anbieter-Credentials abrufen. Lösung:
Branchenstandard für SPAs.
- **Access-Token**: kurzlebig (15 min), lebt nur im
JavaScript-Memory (Modul-State + AuthContext). Kein localStorage
mehr → XSS-Angriff klaut maximal einen 15-min-Token, mit dem er
eh nicht weit kommt.
- **Refresh-Token**: 7 Tage Lifetime, im **httpOnly-Cookie** (`Secure`
bei HTTPS_ENABLED, `SameSite=Strict`, `Path=/api/auth`). JavaScript
hat **keinen Zugriff** → XSS kann ihn nicht klauen.
- Backend:
* `signAccessToken/signRefreshToken` mit `type`-Claim als
Unterscheidung; Auth-Middleware lässt nur `type=access` durch
* Login + Customer-Login setzen Cookie + geben Access im Body
* `POST /api/auth/refresh` liest Cookie, gibt neuen Access aus,
rotiert Refresh-Cookie, prüft `tokenInvalidatedAt`
(sofortige Invalidation bei Rolle-Ändern/Logout)
* Logout löscht Cookie + setzt `tokenInvalidatedAt`
* `cookie-parser` als neue dependency
- Frontend:
* `api.ts`: in-memory `tokenStore` + axios-Interceptor mit
Auto-Refresh-Retry bei 401 (single-flight gegen
Concurrent-Requests)
* `AuthContext`: beim App-Start `/auth/refresh` aufrufen → wenn
Cookie noch gültig, ist der User automatisch eingeloggt
(kein Re-Login nach Tab-Reload trotz memory-only Access-Token)
* 9 alte `localStorage.getItem('token')`-Stellen migriert auf
`getAccessToken()` (PDF-Vorschau-iframe, Audit-Log-Export,
Backup-Download, File-Download-URL, …)
- Live verifiziert: Login setzt Cookie+Bearer, API-Calls mit
Bearer→200, ohne→401, Refresh-Endpoint rotiert Cookie sauber,
Refresh-Token wird als Bearer (Access) abgelehnt („Falscher
Token-Typ"), Logout löscht Cookie + invalidiert Token.
- [x] **🔒 Audit-Log für alle Klartext-Passwort-Reads**
- Pentest-Finding „Klartext-Passwörter über API abrufbar (HIGH,
post-auth)" → reversible Verschlüsselung ist by-design (Feature
„Anbieter-Login anzeigen" braucht es), aber jeder Decrypt-Vorgang
sollte im Audit-Log auftauchen. Bisher: keiner der 6 Endpoints
schrieb ein Log.
- Audit-Logs jetzt für: `getPortalPassword`, `getContractPassword`,
`getSimCardCredentials`, `getInternetCredentials`,
`getSipCredentials`, `getMailboxCredentials`.
- `action: 'READ'`, eigene Resource-Types (PortalPassword,
ContractPassword, SimCardCredentials, InternetCredentials,
SipCredentials, MailboxCredentials), alle mit `sensitivity:
CRITICAL` über die Sensitivity-Map.
- Label nennt explizit „Klartext … entschlüsselt" + Ressourcen-ID,
damit im Audit-Log-Viewer auf einen Blick erkennbar ist, was
passiert ist (DSGVO-Nachvollziehbarkeit + Insider-Threat-Erkennung).
- [x] **↗ E-Mail-Postfach: Weiterleiten + Erneut senden**
- **Weiterleiten** (Compose-Modal-Erweiterung): neuer Button im
EmailDetail öffnet das ComposeEmailModal im Forward-Modus
To-Feld leer (User trägt den neuen Empfänger ein), Betreff mit
„Fwd:"-Prefix, Body mit zitierten Original-Headern (Von, An,
Datum, Betreff) + Original-Text.
- **Erneut senden** (One-Click): schickt die Mail noch einmal an
die ursprüngliche Empfänger-Adresse (= die Stressfrei-Adresse
selbst). Damit läuft sie durch die heute hinterlegten Forwards
und landet beim aktuell konfigurierten Kunden-Postfach Use-Case:
Stressfrei-Adresse wurde nach Empfang umgestellt, Original ist nur
in der alten Inbox. Confirm-Dialog mit Hinweis, dass Anhänge nicht
erneut mit gesendet werden (Weiterleiten dafür nutzen). Toast für
Erfolg/Fehler.
- [x] **🔍 E-Mail-Postfach: Suche + erweiterte Filter (Variante B)**
- Suchleiste über der Email-Liste durchsucht parallel Subject,
From-Address/Name und Body.
- Filter-Button mit Badge (Anzahl aktiver Filter) klappt eine Box mit
Detail-Filtern auf: Von, An, Betreff, Inhalt, Datum von/bis,
Anhang-Dateiname, Mit/Ohne Anhang, Gelesen-Status, Markiert-Status.
Alle Filter werden im Backend mit UND verknüpft.
- „Alle zurücksetzen"-Button räumt komplett auf.
- Backend: `GET /api/customers/:id/emails` nimmt die Filter als
Query-Parameter entgegen, `getCachedEmails` übersetzt sie in eine
Prisma `where`-Klausel.
- **Bewusst nicht gebaut**: voller AND/OR-Builder mit Plus-Button und
Bool-Verschachtelung Trade-off-Diskussion mit User: reale
Use-Cases sind quasi immer AND, UI-Komplexität verschachtelter
Bool-Builder bringt mehr Bedienprobleme als Mehrwert.
- [x] **🔁 Stressfrei-Adressen: Weiterleitungen + Passwort manuell synchronisieren**
- Refresh-Icon-Button in der Action-Reihe jeder Stressfrei-Adresse
(Tooltip erklärt: „ersetzt die Forwards am Provider durch
Kunden-Stamm-E-Mail + Service-Adresse"). Use-Case: nach Änderung der
Stamm-E-Mail eines Kunden, oder nach Wechsel der
`defaultForwardEmail` in den Provider-Settings.
- **Bei `hasMailbox: true`** wird zusätzlich das im CRM verschlüsselt
hinterlegte Mailbox-Passwort am Provider neu gesetzt. Self-Healing
für den Fall, dass jemand im Plesk-UI manuell ein anderes Passwort
gesetzt hat und IMAP/SMTP im CRM nicht mehr passt.
- Backend nutzt Plesk's `updateForwardTargets` (`set:email1,email2`
→ ersetzt komplett, idempotent) + bei Mailbox auch
`updateMailboxPassword` (Plesk-Passwort-Update).
- Endpoint: `POST /api/stressfrei-emails/:id/sync-forwarding`,
`customers:update`-Permission, Audit-Log mit Forward-Targets +
Passwort-Reset-Marker.
- Self-Healing: `isProvisioned`-Flag wird bei erfolgreichem
Provider-Aufruf automatisch auf `true` korrigiert (historischer Bug:
Flag wurde beim `createEmail` mit `provisionAtProvider: true` nie
gesetzt jetzt behoben + Backfill via Sync).
- Erfolgs-/Fehler-Meldungen via `react-hot-toast` (statt `alert()`)
mit Liste der gesetzten Forward-Targets + Hinweis ob Passwort-Reset
durchgeführt wurde.
- In der Kundenakte (Stammdaten → Kontakt → E-Mail) externes
Link-Icon, das in neuem Tab direkt den Stressfrei-Tab des Kunden
öffnet sichtbar nur wenn Stressfrei-Adressen vorhanden sind.
- [x] **🛡️ Pentest-Hardening-Runde 11: Header-Hygiene**
- **HSTS-Doppel-Header** (18× low im Audit): Helmet's
`Strict-Transport-Security` komplett deaktiviert. Der Nginx Proxy Manager
vor der CRM-VM setzt HSTS bereits, doppelter Header verletzte RFC 6797.
- **Cache-Control** (≥10× info im Audit):
`/api/*` bekommt `no-store` (sensible JSON-Daten),
SPA-HTML (`/`, `/sitemap.xml`, `/robots.txt`, `/vite.svg`) bekommt
`no-store, must-revalidate` (sonst hängt Browser an alter index.html
fest nach Deploy),
`/assets/*` (Vite-Build mit Content-Hash im Filename) bekommt
`public, max-age=31536000, immutable`.
- **CSP No-Fallback-Direktiven** (2× medium): `worker-src`, `manifest-src`,
`media-src` explizit auf `'self'` ZAP markiert sonst „Failure to
Define Directive with No Fallback".
- Bewusst NICHT angefasst: `style-src 'unsafe-inline'` (Tailwind/React-
inline-styles, kompletter Refactor unverhältnismäßig).
- Live verifiziert: Headers für `/`, `/api/*`, `/assets/*.js` und SPA-
Fallback-Pfade alle wie erwartet.
- [x] **🐛 PDF-Vorschau im PDF-Template-Editor lädt nicht**
- CSP-Direktive `frame-ancestors 'none'` blockte ALLE iframe-Embeddings
der eigenen Resourcen, auch same-origin Browser zeigte je nach
Variante "Verbindung abgelehnt" oder CSP-Violation.
- Fix: `frame-ancestors 'self'` (statt `'none'`). App darf eigene
Resourcen embeden (z.B. die annotierte PDF-Vorschau), externe Sites
bleiben weiterhin gesperrt.
- [x] **🔁 Factory-Defaults Sync-Scripts (dev ↔ prod ↔ Image)**
- `./factory-export.sh` zieht eine ZIP per API in `factory-exports/`
(gitignored Drop-Box).
- `./factory-import.sh [zip]` lädt die ZIP per API in eine andere Instanz
ohne Argument wählt es die jüngste ZIP automatisch.
- `./factory-import.sh --save-as-builtin` entpackt die ZIP zusätzlich nach
`backend/factory-defaults/` (vorher aufgeräumt). Damit landet sie beim
nächsten `docker-compose up --build` als Werkseinstellung im Image und
seedet frische DBs automatisch.
- Konfigurierbar per Env: `OPENCRM_URL`, `OPENCRM_EMAIL`,
`OPENCRM_PASSWORD` (sonst interaktive Abfrage).
- README-Abschnitt „Factory-Defaults: Stammdaten-Kataloge teilen"
komplett überarbeitet (drei Transport-Pfade, Auto-Seed, Whitelist).
- [x] **🚀 Auto-Seed: Werkseinstellungen beim Erst-Deploy**
- Inhalt von `backend/factory-defaults/` wird via Dockerfile als
`/app/factory-defaults-builtin/` ins Image gebrannt.
- Entrypoint spielt sie nach erfolgreichem Auto-Seed (frische DB) automatisch
via `tsx scripts/seed-factory-defaults.ts` ein steuerbar über
`FACTORY_DEFAULTS_DIR`.
- Damit bringen neue VMs sofort Anbieter, Tarife, PDF-Auftragsvorlagen +
Datenschutzerklärung/Impressum mit, ohne manuelles UI-/CLI-Import.
- Bestehende Installs werden NIE überschrieben (Trigger nur wenn der
Auto-Seed im selben Start-Lauf gelaufen ist).
- [x] **📦 Factory-Defaults: HTML-Templates + Import via UI**
- Datenschutzerklärung, Impressum, Vollmacht-Vorlage und Website-Datenschutz
werden jetzt mit ins Factory-Defaults-ZIP gepackt (`app-settings/`-Ordner,
Whitelist-geschützt andere AppSetting-Keys werden ignoriert).
- Import läuft jetzt auch über die UI (Einstellungen → Factory-Defaults →
„ZIP hochladen"). Der CLI-Weg `npm run seed:defaults` bleibt erhalten und
wurde gleichermaßen um die HTML-Templates erweitert.
- Zwei-Wege-Roundtrip live verifiziert: Export → AppSetting löschen →
Import → Wert wieder vollständig hergestellt; Counts in Audit-Log.
- [x] **🐛 Benutzer-Verwaltung: DSGVO- + Entwickler-Zugriff zuweisbar**
- Mass-Assignment-Whitelist (`pickUserUpdate`) hat `hasGdprAccess` /
`hasDeveloperAccess` rausgefiltert → Service erhielt sie nie → Rollen
DSGVO/Developer waren in der UI nicht zuweisbar (Checkbox ohne Wirkung).
- Beide Felder zur Whitelist hinzugefügt + Audit-Log liest die Pre-Werte
jetzt aus den geladenen Rollen (kein False-Positive-Change mehr).
- [x] **🔒 HTTPS-only-Header per Flag (`HTTPS_ENABLED`)**
- HSTS + `upgrade-insecure-requests` (CSP) sperrten den Browser bei
direktem `http://ip:port`-Zugriff aus (`ERR_SSL_PROTOCOL_ERROR`).
- Beide Header default OFF, kommen nur mit `HTTPS_ENABLED=true` (sobald
TLS-Reverse-Proxy davor steht).
- [x] **🗃️ Prisma-Migrations-System (statt `db push`)**
- Initial-Migration `0_init` aus aktuellem Schema generiert
(`prisma migrate diff --from-empty --to-schema-datamodel`).
- 24 alte gedriftete Migrations gelöscht frischer Start.
- `migration_lock.toml` für MySQL hinzugefügt.
- Container-Entrypoint umgebaut:
- Auto-Baseline-Detection: bestehende DB ohne `_prisma_migrations`
`migrate resolve --applied 0_init` läuft automatisch.
- Statt `db push --accept-data-loss` jetzt `migrate deploy` (idempotent,
datenerhaltend, keine stillen DROPs mehr).
- Neuer npm-Script `schema:sync` (lokal/Dev): legt automatisch eine
versionierte Migration mit Zeitstempel-Namen an
(`prisma migrate dev --name auto_$(date +%Y%m%d_%H%M%S)`).
- Workflow ab jetzt: schema.prisma ändern → `npm run schema:sync`
Migration committen → Push → Container-Restart wendet sie automatisch an.
- [x] **🔄 Automatische Vertrags-Status-Übergänge**
- Nightly-Cron (02:00 + Catch-up 60s nach Start): alle Verträge mit
`status=ACTIVE` und `endDate < heute``EXPIRED` (mit Audit-Log).
- Beim Upload der Kündigungsbestätigung (`cancellationConfirmationPath`):
wenn Vertrag aktuell `ACTIVE` → auf `CANCELLED` setzen (Audit-Log).
Frontend fragt per Modal das Bestätigungs-Datum ab (Default: heute),
wird direkt als `cancellationConfirmationDate` gespeichert.
Der "Optionen"-Upload löst den Status-Wechsel bewusst NICHT aus, da er
für Vertragsänderungen (nicht echte Kündigungen) gedacht ist, setzt
aber `cancellationConfirmationOptionsDate` analog.
- Beim Upload einer `Lieferbestätigung` (ContractDocument via direkt-Upload
oder Email-Anhang-Import): wenn Vertrag aktuell `DRAFT` → auf `ACTIVE`
setzen + `startDate` auf das erfasste Lieferdatum (falls leer).
Frontend zeigt Datums-Input conditional, wenn Typ "Lieferbestätigung"
ausgewählt ist.
- Keine neuen Status eingeführt: `cancellationSentDate` vs.
`cancellationConfirmationDate` genügen, um "gesendet vs. bestätigt"
abzubilden. `ACTIVE` bleibt bis zur Bestätigung.
- [x] **🛡️ Security-Hardening vor Production-Deployment (10 Runden)**
- Vollständige Story inkl. aller Live-Test-Tabellen + Trade-offs:
**[SECURITY-HARDENING.md](./SECURITY-HARDENING.md)**
- Erste 2 Runden zusätzlich ausführlich in
[SECURITY-REVIEW.md](./SECURITY-REVIEW.md)
- Highlights:
- Runde 13: CORS, Helmet, JWT-Fallback, IDOR-Welle 1, XSS, Mass
Assignment, Zip-Slip, Path-Traversal, JWT-Algorithm, Rate-Limiter
- Runde 4: 9 Live-IDORs (customer.\*/gdpr.\*) + Error-Handler
- Runde 5: `/api/uploads`-Auth (DSGVO-GAU), Login-Timing,
Privacy-Policy-XSS
- Runde 6: Customer-List-Leak, XFF-Rate-Limit-Bypass,
Self-Grant + Existence-Disclosure
- Runde 7: SSRF-Schutz (Cloud-Metadata-Block), Logout-Endpoint
- Runde 8: DNS-Rebinding-Schutz, Per-File-Ownership-Check
- Runde 9: `npm audit fix` (8 Vulns weg), Audit-Chain-Rehash, keine
neuen Critical-Findings → diminishing returns erreicht
- Runde 10: Security-Monitoring (SecurityEvent-Tabelle + Hooks an
Login/IDOR/SSRF/Reset/Logout/JWT-Reject + Threshold-Detection +
Sofort-Alert für CRITICAL + Hourly-Digest + UI in Einstellungen)
- Deployment-Checkliste komplett (in HARDENING.md)
- [x] **🎉 Version 1.0.0 Feinschliff: Passwort-Reset + Rate-Limiting + Auto-Geburtstagsgrüße**
- **Passwort vergessen-Flow** (Login → "Passwort vergessen?" Link)
- Email-Reset-Token mit 2h Gültigkeit (kryptografisch sicher: 32 Byte Random)
- Funktioniert für Mitarbeiter UND Portal-Kunden (Typ-Auswahl)
- User-Enumeration-Schutz: immer 200 OK, egal ob Email existiert
- Reset-Link per Email mit schönem HTML-Template
- Nach Reset: alle bestehenden Sessions werden gekickt
- **Rate-Limiting** gegen Brute-Force
- Login: 10 Versuche pro 15 Min pro IP (erfolgreiche zählen nicht)
- Passwort-Reset-Anfrage: 5 Versuche pro Stunde pro IP
- **Cron-Job für automatische Geburtstagsgrüße**
- Täglich 08:00 Uhr: alle Kunden mit heutigem Geburtstag + autoBirthdayGreeting=true
- Email-Versand über System-E-Mail, Du/Sie-abhängiger Text
- Catch-up 30s nach Server-Start (falls Server am Geburtstag kurz down war)
- Marker lastBirthdayGreetingYear verhindert Doppel-Versand
- [x] **Mandantenfähigkeit: Domain + Kunden-E-Mail-Label dynamisch pro Provider**
- Neues Feld `customerEmailLabel` am EmailProviderConfig (z.B. "Stressfrei-Wechseln", "Meine-Firma")
- Wenn leer, wird das Label automatisch aus der Domain abgeleitet ("stressfrei-wechseln.de" → "Stressfrei-Wechseln")
- Neuer Frontend-Hook `useProviderSettings()` liefert Domain + Label
- Alle hardcoded "Stressfrei-Wechseln" und `@stressfrei-wechseln.de` Strings durch dynamische Werte ersetzt
(CustomerDetail, ContractForm, ContractDetail, EmailClientTab, Settings)
- Modal-Eingabefeld "Bezeichnung für Kunden-E-Mails" in Provider-Einstellungen
- Notwendig für Multi-Mandanten-Betrieb wenn das CRM an Dritte vermietet wird
- [x] **Factory-Defaults: Export + Import von Stammdaten-Katalogen**
- Enthält: Anbieter, Tarife, Kündigungsfristen, Laufzeiten, Vertragskategorien, PDF-Auftragsvorlagen (+ PDF-Dateien)
- Enthält NICHT: Kundendaten, Verträge, Dokumente, Emails, Einstellungen (dafür gibt es den Datenbank-Backup)
- Neue Einstellungsseite „Factory-Defaults" mit Übersicht (Anzahl pro Kategorie) und Export-Button
- Export: ZIP mit manifest.json + Kategorie-JSONs + PDF-Dateien, Download über Browser
- Import-Script: `npm run seed:defaults` liest `backend/factory-defaults/`, merged mehrere JSONs pro Kategorie, upsertet idempotent + kopiert PDFs in uploads/
- Ordner `backend/factory-defaults/` gitignoriert (außer .gitkeep + README), damit firmen-spezifische Kataloge nicht ins Repo kommen
- [x] **Email-Anhänge → Vertragsdokumente + Rechnungen für alle Vertragstypen**
- Im SaveAttachmentModal (bei einem per Email zugeordneten Vertrag) gibt es jetzt drei Modi:
1. **Als Dokument** (in feste Slots wie Kündigungsschreiben) wie bisher
2. **Als Vertragsdokument** neu, mit Typ-Dropdown (Auftragsformular, Lieferbestätigung, Vertragsunterlagen, Vollmacht, Widerrufsbelehrung, Preisblatt, Sonstiges) + Notizen
3. **Als Rechnung** jetzt für **alle** Vertragstypen (vorher nur Strom/Gas)
- Gleiches gilt für das Speichern der gesamten Email als PDF-Rechnung
- Neuer Backend-Endpoint `saveAttachmentAsContractDocument` für die flexible ContractDocument-Tabelle
- [x] **Geburtstag-Management-Modal in Kundenstammdaten**
- Neuer Button (Cake-Icon) neben Geburtsdatum öffnet Modal
- **Gruß zurücksetzen:** setzt `lastBirthdayGreetingYear` auf null zurück (fürs Debugging + Fallback)
- **Gruß jetzt senden:** per Email (direkt), WhatsApp/Telegram/Signal (öffnet vorbefülltes Fenster)
- Beide Aktionen mit Ja/Nein-Bestätigungsdialog (kein versehentliches Klicken)
- Text respektiert Du/Sie-Einstellung des Kunden
- Checkbox "Automatisch senden" mit Kanal-Dropdown (neue Felder am Customer)
- Audit-Log für Reset + Send
- [x] **Anrede-Verhältnis Du/Sie pro Kunde**
- Neues Feld `useInformalAddress` in Stammdaten (auch bei Firmenkunden)
- Default: Sie (formell)
- Geburtstagsgruß im Portal nutzt die Anrede: "Du"-Kunden bekommen "Herzlichen Glückwunsch, Max!", "Sie"-Kunden "Herzlichen Glückwunsch, Herr Müller!"
- Komplett konsistent auch bei nachträglichen Glückwünschen ("hattest" vs "hatten")
- [x] **Geburtsdatum + Geburtsort auch bei Firmenkunden**
- Felder werden jetzt unabhängig vom Kundentyp angezeigt
- Ermöglicht z.B. Geburtstage für Ansprechpartner bei Firmen
- [x] **Geburtstagskalender + Geburtstagsgruß-Modal**
- Admin: Section im Vertrags-Cockpit mit Kunden, die in den nächsten 30 Tagen oder letzten 7 Tagen Geburtstag haben
- Portal: Modal mit Gruß am Geburtstag (inkl. nachträglichem Glückwunsch bis 7 Tage danach)
- Wird pro Jahr nur einmal angezeigt
- [x] **Typspezifische Zusatzinfos in Vertragslisten**
- Strom/Gas → "Lieferadresse: ..."
- DSL/Glasfaser/Kabel → "Anschlussadresse: ..."
- Mobilfunk → "Rufnummer: ..."
- KFZ → "Kennzeichen: ..."
- Sichtbar in Admin-Liste, Portal-Liste und Kunden-Tab
- [x] **Datenschutzerklärung PDF ↔ Online-Einwilligungen synchronisieren**
- PDF hochgeladen → alle 4 Consents auf GRANTED
- Haken entfernt im Portal → PDF löschen + Tabs sperren
- Entsperrung nur durch alle Haken oder neues PDF
- [x] **Zweitarif-Zähler (HT/NT)** bei Strom + Verbrauchsberechnung
- [x] **Datumsformate vereinheitlichen** (01.01.2026 statt 1.1.2026)
- [x] **Audit-Log aussagekräftig** (Vorher/Nachher bei allen Änderungen)
- [x] **Impressum + Website-Datenschutzerklärung** im Kundenportal
- Editor in Einstellungen
- Vorschlagstexte
- [x] **Consent-Bestätigungs-Flow per Email**
- Alle Hebel müssen gesetzt sein
- Bestätigungsbutton + Bestätigungsemail
- [x] **Vertragsdokumente-Upload** (Auftragsformular, Lieferbestätigung, Vertragsunterlagen als PDF/PNG)
- [x] **Bug: Stressfrei-Email im Auftragsgenerator** (funktioniert jetzt im Vertrag)
- [x] **PDF-Auftragsvorlagen-System**
- Template-Editor in Einstellungen
- PDF hochladen, Formularfelder automatisch auslesen
- CRM-Felder zuordnen (visuell mit Vorschau)
- Seitenweise Sortierung der Felder
- Dynamische Rufnummern-Felder mit Vorwahl-Extraktion
- Nicht zugeordnete Felder bleiben editierbar
- Auftrag generieren aus Vertragsdaten (Button im Vertrags-Detail)
- [x] **Eigentümer-Verwaltung**
- An Adresse gehängt (Firma, Vorname, Nachname, Anschrift, Kontakt)
- Fallback auf Kundendaten wenn leer
- Nur bei Liefer-/Meldeadressen (nicht Rechnung)
- Namens-Kombinationen (Firma + Vorname + Nachname etc.)
- [x] **Gruppenauswahl Liefer-/Rechnungs-/Eigentümer-Adresse** im Auftragsgenerator
- [x] **Objekttyp + Lage + Lage des Anschlusses** bei Festnetz-Verträgen (DSL/Glasfaser/Kabel)
- [x] **Bankverbindung-Fallback** im PDF-Generator (neueste aktive Bankverbindung des Kunden)