# 📋 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 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 -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 `