opencrm/backend/todo.md

389 lines
22 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 **[docs/TESTING.md](../docs/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] **🔄 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-Review + Hardening vor Production-Deployment (3 Runden)**
- Vollständiger Review aller kritischen Bereiche, dokumentiert in **[docs/SECURITY-REVIEW.md](../docs/SECURITY-REVIEW.md)**
- **Runde 1 6 kritische + 2 wichtige Findings gefixt:**
- CORS offen → `CORS_ORIGINS` explizit
- Helmet + Security-Headers
- JWT-Fallback-Secret entfernt (Fail-Fast beim Start)
- IDOR bei 7 Contract-Endpoints
- XSS via Email-Body (DOMPurify)
- Customer-API Data Exposure (Passwort-Hashes)
- Portal-JWT-Invalidation nach Passwort-Reset
- Body-Size-Limit 5 MB
- **Runde 2 Deep-Dive mit parallelen Audit-Agents, 5 weitere kritische + 2 wichtige:**
- Zip-Slip im Backup-Upload (Arbitrary File Write!)
- Mass Assignment bei Customer/User (Privilege Escalation via `roleIds`!)
- 13 weitere IDOR-Stellen (Meter-Readings, Email-Anhänge, StressfreiEmail-Credentials …)
- Path-Traversal bei Backup-Name und GDPR-Proof-Download
- **Runde 3 Tiefer Dive (8 weitere Hardenings):**
- JWT algorithm confusion: `jwt.verify` auf `algorithms: ['HS256']` festgenagelt
- `trust proxy = 1` für Rate-Limiter hinter Reverse-Proxy (sonst unwirksam)
- IDOR Invoice (alte `/api/energy-details/:ecdId/invoices`): jetzt `canAccessEnergyContractDetails` → Contract → customerId
- IDOR PDF-Template-Generator (`:id/generate/:contractId`): jetzt `canAccessContract`
- Email-Anhang-Download: Content-Type-Safelist (HTML/SVG nie inline) + `X-Content-Type-Options: nosniff` + Filename-CRLF-Sanitizing
- Provider/Tariff-GETs: `requirePermission('providers:read')` (Portal-Kunden sehen Provider-Liste nicht mehr)
- SMTP-Header-Injection: zentrale CRLF-Validierung in `smtpService.sendEmail` (schützt alle Caller)
- bcrypt cost 10 → 12 (OWASP 2026)
- **Runde 7 Letzter Schliff (SSRF + Logout):**
- **SSRF-Schutz** in `test-connection` und `test-mail-access`: ein
Admin-User konnte über die Plesk-API-URL bzw. SMTP/IMAP-Server-Felder
Connections zu beliebigen IPs auslösen (Cloud-Metadata-Endpoints,
Link-Local, AWS/GCP-Metadata-Hosts). Internal-Port-Scanning via
Timing-Differenzen war messbar (22/80/3306/5432/6379 unterschiedlich).
Fix: neuer Helper `utils/ssrfGuard.ts` blockiert vor jeder ausgehenden
Verbindung 169.254.0.0/16, 0.0.0.0/8, Multicast/Reserved-Ranges,
AWS-IPv6-Metadata, IPv6-Link-Local und bekannte Cloud-Metadata-
Hostnames (metadata.google.internal etc.). Loopback (127.0.0.0/8)
bleibt erlaubt für legitime Plesk/Postfix-Setups.
- **Logout-Endpoint** `POST /api/auth/logout`: setzt
`tokenInvalidatedAt` / `portalTokenInvalidatedAt` auf jetzt. Auth-
Middleware prüft das Feld und lehnt Tokens mit `iat` davor ab.
JWTs sind stateless ohne diesen Mechanismus bleibt ein
„abgemeldeter" Token bis zum natürlichen Expiry (7d) gültig.
- Live-verifiziert: 169.254.169.254/metadata.google.internal/0.0.0.0
werden mit 400 abgelehnt; 127.0.0.1 weiter erlaubt; Logout
invalidiert den Token sofort (HTTP 401 „Sitzung ungültig").
**Geprüft + sauber (Runde 7):**
- Public Consent (random Hash → 404, kein Brute-Force durch 122-bit-UUID)
- Magic-Bytes-Bypass beim Upload (HTML als image/png) → blockiert
- PDF-Generation mit injizierten manualValues → kein XSS-Vektor (PDFs sind keine Web-Renderer)
- Audit-Logs für Portal-User: 403
- Email-Config-Update als Portal: 403
- Backup-Endpoints als Portal: 403
- Query-Filter-Override (?customerId=X) → vom Portal-Filter ignoriert
**Bewusst NICHT gefixt (zu invasiv für v1.0):**
- Vollständige DNS-Resolution beim SSRF-Guard (gegen DNS-Rebinding)
kann legitimes CDN/Caching brechen, v1.1-Item.
- Per-File-Ownership-Check bei `/api/uploads` (siehe Runde 5).
- **Runde 6 Tiefer Live-Pentest (auf Wunsch des Users, „bevor andere es tun"):**
- 🚨 **`GET /api/customers` leakte als Portal-User die komplette
Kundendatenbank** (alle Namen, E-Mails, customerNumber etc.). Der
Single-Endpoint war Stage 4 mit `canAccessCustomer` gefixt, der List-
Endpoint nicht. Jetzt: Portal-User bekommt nur eigene + vertretene
Kunden (Filter im Controller).
- 🚨 **Rate-Limit-Bypass via `X-Forwarded-For`**: 12+ Login-Versuche
mit rotierenden XFF-Werten gingen alle durch ohne 429. `trust proxy = 1`
hat naiv jedem XFF-Wert vertraut. Jetzt: `trust proxy = 'loopback'`
XFF wird nur akzeptiert wenn die Connection von 127.0.0.1 / ::1 kommt
(= lokaler Reverse-Proxy). Plus: `LISTEN_ADDR=127.0.0.1` in Production-
Default, damit das Backend nicht direkt von außen ansprechbar ist.
- **Self-Grant + Existence-Disclosure in `toggleMyAuthorization`**:
- Portal-User konnte sich selbst Vollmacht erteilen (1→1) und
Datensätze für beliebige `representativeId`s anlegen (auch nicht-
existierende, scheiterte erst auf DB-Constraint mit Prisma-Stack-Leak).
- 404 vs 403 erlaubte Existence-Probing der gesamten customer-ID-Range.
- Fix: Self-Grant 400er. Existenz + aktives `CustomerRepresentative`-
Verhältnis in einem Query, beide Fehlfälle identisch 403.
- **Prisma-Error-Leak generisch in `toggleMyAuthorization`**: keine
Prisma-Stacks mehr im Response.
- Live-verifiziert: Customer-Liste 3 statt 3000 (jetzt nur erlaubte),
Self-Grant 400, Existence-Disclosure dicht (alle 403 uniform), Auth
auf `/api/customers/:id` 200/403 (kein 404-Leak).
**Geprüft + sauber (Runde 6):**
- Prototype Pollution beim Login → kein Effekt
- HTTP-Method-Override via Header → ignoriert
- Path-Traversal in Backup-Name → durch Regex blockiert
- Developer-Routes existieren nicht (404)
- Email-Endpoints (Send/Sync/Read mit fremder StressfreiEmail-ID) → 403
- Self-grant Vollmacht via `customers/X/representatives` → 403 (perm)
- `/api/customers/:id` GET: 200 für eigene, 403 sonst (kein 404-Leak)
**Offen für v1.1:**
- `/api/contracts/:id` GET liefert 404 für nicht-existente IDs (Existence-
Probing). Da contractIds aber nicht direkt mit personenbezogenen Daten
korrelieren, niedrig-Prio. Vereinheitlichung auf 403 wäre sauberer.
- Prisma-Error-Leaks in anderen Admin-Endpoints (z.B. `addInvoice` bei
Validation-Fehler) Defense-in-Depth-Kandidat.
- **Runde 5 Hack-Das-Ding-Audit (Live-Pentest + 3 parallele Audit-Agents):**
- 🚨 **`/api/uploads/*` war OHNE AUTH erreichbar** (DSGVO-GAU!) jetzt hinter
`authenticate`. Direkte <a href>-Links nutzen `?token=...` Query-Parameter,
unterstützt von auth-Middleware. Frontend-Helper `fileUrl(path)` hängt
Token automatisch an, 24 URLs migriert (CustomerDetail, ContractDetail,
InvoicesSection, PdfTemplates, GDPRDashboard).
- **Login-Timing-Side-Channel**: Bei ungültigem User fehlte `bcrypt.compare`
→ 110ms vs 10ms, User-Enumeration trivial. Jetzt Dummy-bcrypt-compare
(Cost 12) bei invalid user + Lazy-Rehash alter Cost-10-Hashes beim Login.
Live-verifiziert: 422ms vs 425ms Timing-Angriff dicht.
- **XSS via Privacy Policy / Imprint**: 4 Frontend-Seiten renderten
Backend-HTML ohne DOMPurify (`PortalPrivacy`, `ConsentPage`,
`PortalWebsitePrivacy`, `PortalImprint`). Admin-eingegebene
`<script>`-Tags wären bei jedem Portal-Kunden-Besuch ausgeführt worden.
Jetzt mit strikter Sanitize-Config (FORBID_TAGS/ATTR).
- **IDOR-Härtung Upload/Delete/SaveAttachment**: `canAccessContract` jetzt
in `uploadContractDocument`, `deleteContractDocument`, im generischen
`handleContractDocumentUpload` (Kündigungsschreiben + -bestätigungen)
und in `saveAttachmentAsContractDocument`. Defense-in-Depth, blockt
auch bei künftigen Staff-Scoping-Rollen.
- Global Error-Handler: `err.status` wird respektiert (413/400 statt 500).
**Offen für v1.1**:
- Per-File-Ownership-Check bei `/api/uploads/*` (aktuell reicht
Authentifizierung, kein Datei-spezifischer Owner-Check). Implementierung
bräuchte dedizierten `GET /api/files/download?path=...`-Endpoint mit
DB-Lookup, welche Ressource zur Datei gehört.
- TipTap-Link-Tool: `javascript:`-Protokoll blockieren (Admin-only erreichbar,
niedrig-Prio).
- **Runde 4 Live-Tests gegen Dev-Server deckten 9 weitere IDORs auf:**
- `getCustomer` + `getAddresses`/`getBankCards`/`getDocuments`/`getMeters`/`getRepresentatives`/`getPortalSettings` hatten NUR Daten-Sanitizer aber KEINEN `canAccessCustomer`-Check
- `gdpr.getCustomerConsents` + `getAuthorizations` + `checkConsentStatus` ebenso ungeschützt
- Portal-Kunde konnte live per `GET /api/customers/<fremde-id>` kompletten Fremdkunden-Datensatz auslesen → jetzt 403
- Error-Handler: `err.status` wird jetzt respektiert (413/400 statt pauschalem 500)
**Live-verifiziert als Portal-Kunde gegen fremden Test-Kunden #4:**
| Endpoint | Vorher | Nachher |
| -------------------------------------------- | ------------------------------- | ---------------------------- |
| `GET /api/customers/4` | 🚨 **200 mit Daten** | ✅ 403 |
| `GET /api/customers/4/addresses` | 🚨 200 | ✅ 403 |
| `GET /api/customers/4/bank-cards` | 🚨 200 | ✅ 403 |
| `GET /api/customers/4/documents` | 🚨 200 | ✅ 403 |
| `GET /api/customers/4/meters` | 🚨 200 | ✅ 403 |
| `GET /api/customers/4/representatives` | 🚨 200 | ✅ 403 |
| `GET /api/gdpr/customer/4/consents` | 🚨 200 mit Consent-Daten | ✅ 403 |
| `GET /api/gdpr/customer/4/authorizations` | 🚨 200 | ✅ 403 |
| `GET /api/gdpr/customer/4/consent-status` | 🚨 200 | ✅ 403 |
| Eigene Daten `/api/customers/1` | ✅ 200 | ✅ 200 (unverändert) |
| 12 MB Body | 500 „Interner Serverfehler" | ✅ 413 „Anfrage zu groß" |
| Malformed JSON | 500 „Interner Serverfehler" | ✅ 400 „Ungültiges JSON" |
- Deployment-Checkliste komplett
- [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)