# 📋 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] **🔒 Pentest R91 – NaN-Bypass auf accountId-Query-Param** - R91.1 (LOW): `accountId=abc` → `parseInt('abc')` = `NaN` → der Ternary im Controller gab `NaN` an den Service, `if (NaN)` ist falsy → der Postfach-Filter fiel weg. Folge: ein Portal-User mit ungĂŒltigem `accountId` sah alle Mailbox-Mails fĂŒr seinen Vertrag statt nur die aus dem gewĂ€hlten Postfach (kein Cross-Customer- Leak — `canAccessContract` greift weiter). - Fix: zentraler `parsePositiveIntParam()` im `cachedEmail.controller.ts`, der nur positive Ganzzahlen aus dem Query-String akzeptiert und alles andere zu `undefined` macht. Eingesetzt in allen 5 Endpunkten, die `accountId`/`contractId` aus Query nehmen (Contract-Emails, Contract-Folder-Counts, Customer-Emails, Trash, Trash-Count) – auch da, wo der Pentester nicht getestet hat, weil derselbe Pattern ĂŒberall stand. - [x] **🐞 E-Mail-Ansicht: Postfach-Filter griff in Trash/Sent nicht** - Bug-Bericht 2026-06-21: im Vertrags-Tab (Gesendet/Gelöscht) und im Kunden-Haupt-Postfach (Gelöscht) wurden E-Mails aus ALLEN PostfĂ€chern des Kunden angezeigt, egal welches Postfach im Selector aktiv war. Im Vertrag fehlte zusĂ€tzlich der Vertrags-Filter fĂŒr den Papierkorb. - Backend: - `getEmailsForContract` controller akzeptiert jetzt `accountId`- Query-Param und reicht ihn als `stressfreiEmailId` an `getCachedEmails` weiter (der hat den Filter eh schon implementiert, nur niemand hat ihn aufgerufen). - `getTrashEmails` (controller + service) akzeptiert `accountId` und `contractId` als optionale Filter. Default-Verhalten unverĂ€ndert, wenn keiner gesetzt ist. - `getFolderCountsForContract` akzeptiert optional `stressfreiEmailId`, bekommt zusĂ€tzlich `trash` + `trashUnread` ins Result – sonst lĂ€ge der Trash-Badge im Vertrag wieder account-global, wĂ€hrend die Liste contract-scoped ist. - Frontend: - `cachedEmailApi.getForContract` / `getTrash` / `getContractFolderCounts` nehmen den Filter entgegen. - `ContractEmailsSection` reicht `selectedAccountId` in alle drei Queries durch und nimmt es in den queryKey mit auf – sonst greift der React-Query-Cache beim Postfach-Wechsel nicht. Der Trash-Badge kommt jetzt aus den contract-scoped Counts, damit Badge und Liste synchron laufen. - `EmailClientTab` reicht `selectedAccountId` in die Trash-Query durch (Inbox/Sent waren schon korrekt). - [x] **🔒 Pentest R89 – Provider-Adressfelder hĂ€rten** - R89.1 (MEDIUM): `sanitizeNotes(
, 500)` macht silent `slice(0, 500)` statt 400 – 501+ Zeichen wurden auf 500 abgeschnitten und mit 200 OK gespeichert. - R89.2 (LOW): `stripHtml` lief vor dem Length-Check – `` reduzierte auf leeren String → `null` in der DB → vorheriger Wert silent ĂŒberschrieben (R87.1-Pattern auf Adress-Feldern). - Fix: eigener `validateProviderAddress()` in `sanitize.ts`. Raw-Input, max 500 → `ApiError(400)`, Blacklist `<`, `>`, Tab, alle Control- Chars außer `\n`. CRLF → LF normalisiert vor Length-Check. EingehĂ€ngt in `stripProviderStrings`. - R89.3 (Quotes) + R89.4 (`\n`): bewusst nicht gefixt – Pentester bestĂ€tigt "kein unmittelbares Risiko", React escaped korrekt, sind legitime Bestandteile mehrzeiliger Postadressen. - Doku in `SECURITY-HARDENING.md § Runde 89`. - [x] **🆕 Anbieter: Kontakt + KĂŒndigung als Stammdaten** - Sieben neue optionale Felder am `Provider`-Modell: `contactEmail`, `contactPhone`, `contactFax`, `contactAddress`, `cancellationEmail`, `cancellationFax`, `cancellationAddress`. Postadressen als `TEXT` (mehrzeilig), Rest `VARCHAR(191)`. Migration `20260621100000_provider_contact_and_cancellation` mit `IF NOT EXISTS`. - Modal „Anbieter bearbeiten" bekommt eine neue Sektion **Kontakt & KĂŒndigung** unterhalb der Auto-Login-Felder, getrennt in zwei Untergruppen (Kontakt / KĂŒndigung) mit kleinen Headern. Email-/Telefon-/Fax-Felder als Single-Line-Inputs, Postadressen als `