# 📋 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 Runde 10 – Live-Vollmacht-Konsistenz + DTO-Leaks in embedded Objekten** - **MEDIUM – Stale Token nach Vollmacht-Widerruf**: Selbst ein FRISCHER Portal-Login lieferte JWT mit `representedCustomerIds: [7]` und `representedCustomers: [{Nina,
}]`, obwohl die Vollmacht widerrufen war. Live-Check beim Datenzugriff funktionierte (403), aber die UI zeigte dem Vertreter weiter, dass er Nina vertreten könne. * **Fix**: `customerLogin` und `getCustomerPortalUser` (= /me + Refresh-Pfad) filtern `representingFor` jetzt zusĂ€tzlich ĂŒber `getAuthorizedCustomerIds()` – nur Beziehungen mit `isGranted: true` landen im Token und in /me. * Verifiziert: Customer 1 (vertritt 2,3 aber alle Vollmachten widerrufen) → JWT.representedCustomerIds = `[]`, /me ebenfalls. - **MEDIUM – DTO-Leak in embedded Objekten**: `GET /customers/:id` lieferte zwar Customer-Top-Level sanitisiert, aber `contracts[]` darin enthielt weiterhin `commission`, `notes`, `portalPasswordEncrypted`, `nextReviewDate`. Analog `notes` auf embedded customer in `/contracts/:id`. * **Fix**: `sanitizeCustomer(Strict)` ruft jetzt `sanitizeContract(Strict)` fĂŒr jedes Element in `contracts[]` auf. `notes` zu `PORTAL_HIDDEN_CUSTOMER_FIELDS` ergĂ€nzt (interne CRM-Vermerke). * Verifiziert: Portal-User sieht in `customers/1.contracts[*]` keine commission/notes/PW-Encrypted/nextReviewDate mehr; Admin sieht sie weiterhin (Workflow-Bedarf); `portalPasswordEncrypted` ist generell entfernt (Klartext nur via `/contracts/:id/password` mit Audit-Log). - **LOW – `/tasks?customerId=X` 200 statt 403 fĂŒr fremde IDs**: Konsistenz-Issue: nach Vollmacht-Widerruf gab der Endpoint leeres Array statt einen klaren 403-Fehler. Jetzt: wenn der Portal-User explizit nach einer customerId filtert, die er nicht (mehr) vertreten darf → 403 mit "Kein Zugriff auf diese Kundendaten". Verifiziert. - [x] **🚹 Pentest Runde 7 (Anschlussrunde) – Information-Disclosure + Input-Validation** - **MEDIUM – Interne Felder in Portal-Responses**: * `sanitizeCustomerStrict` strippt jetzt zusĂ€tzlich `portalTokenInvalidatedAt`, `portalLastLogin`, `portalPasswordMustChange`, `lastBirthdayGreetingYear`, `privacyPolicyPath`, `businessRegistrationPath`, `commercialRegisterPath`. * Neue `sanitizeContract` / `sanitizeContractStrict` / `sanitizeContracts(Strict)`: entfernt `portalPasswordEncrypted` (immer; ist nur ĂŒber den dedizierten `/password`-Endpoint mit Audit-Log abrufbar) und fĂŒr Portal- User zusĂ€tzlich `commission`, `notes`, `nextReviewDate`. * `getContract` + `getContracts` rufen jetzt die passende Sanitize-Variante je nach `req.user.isCustomerPortal` auf; Mitarbeiter sehen weiterhin commission/notes (Admin-Workflow), nur `portalPasswordEncrypted` ist generell entfernt (Klartext nur ĂŒber dedicated Endpoint). * Live-verifiziert: Portal sieht 0 Leaks, Admin sieht commission/notes weiterhin. - **LOW – Integer-Truncation bei IDs**: `parseInt('6abc')` → `6` hat alle Endpoints durchgewunken. Neuer middleware in `index.ts`: jedes URL-Pfad-Segment unter `/api`, das mit Ziffer beginnt aber nicht aus reinen Ziffern besteht, wird mit HTTP 400 abgelehnt. Heuristik trifft alle `/resource/(\D+)`-Patterns ohne dass jeder einzelne Sub-Router angefasst werden muss. * Live-verifiziert: `/customers/6abc` → 400 mit klarer Meldung, `/customers/3` weiterhin 200, `/contracts/1abc/history` → 400, normaler Pfade `/audit-logs/customer/3` → 200. - **INFO – Login-Rate-Limit „nach 6 nicht aktiv"**: Code-Stand `limit: 10` fĂŒr `loginRateLimiter`, lokal verifiziert: 11. Versuch = 429. Pentester sah vermutlich noch alten Build oder eine andere Lokation (PW-Reset hat `limit: 5`). Kein Code-Change. - [x] **🛠 Rate-Limit-Sperren: Admin-UI zum Freigeben** - Bei einer Pentest-Runde hat der Tester sich selbst durch zu viele Login-Versuche ausgesperrt → ohne Container-Restart kein Weg zurĂŒck. Jetzt: Admin sieht die Sperren und kann sie einzeln aufheben. - **Datenquelle fĂŒr die Liste**: `SecurityEvent`-Tabelle filtert nach `type = RATE_LIMIT_HIT` im 15-Min-Fenster (= Login-Window), gruppiert nach IP. Pro Eintrag: IP, zuletzt versuchte E-Mail, Limiter-Typ (Login / Passwort-Reset), Hit-Anzahl, Zeit seit letztem Hit. - **Reset**: ruft `loginRateLimiter.resetKey(ip)` und `passwordResetRateLimiter.resetKey(ip)` auf – exposiert von `express-rate-limit` v7. Idempotent, audited. - **Backend**: * `GET /api/settings/rate-limits/active` (`settings:read`) * `POST /api/settings/rate-limits/reset` (`settings:update`) mit Body `{ ipAddress }` * neuer Controller `rateLimitAdmin.controller.ts` - **Frontend**: neue Seite `/settings/rate-limits` mit Tabelle + Freigeben-Button, 15s Auto-Refresh; Kachel in Settings-Übersicht (orange, neben „Sicherheits-Monitoring"). - **Live-verifiziert (4 Schritte)**: 11 falsche Logins von 127.0.0.1 → 11. → 429; Liste zeigt IP + Email + Hits; POST Reset → 200; nĂ€chster Login mit falschem PW → 401 statt 429 (Sperre weg); Audit-Log enthĂ€lt Eintrag. - [x] **🚹 Pentest Runde 7 – Hit-List durchgegangen + kurzlebige Download-Tokens** - **Credential-Endpoints** (Contracts password/internet/sip/simcard + Stressfrei mailbox/send/reset-password): ALLE bereits durch `canAccessContract`/`canAccessStressfreiEmail` gesichert – keine LĂŒcke gefunden. - **`GET /customers/:id/portal/password`** (Klartext-Portal-Passwort- Abruf): hatte KEINEN `canAccessCustomer`-Check. Fix: eingefĂŒgt. Defense in depth gegen versehentlich falsch vergebene `customers:update`-Permission. - **Admin-Funktionen** (factory-reset, developer/*, audit-logs/rehash, audit-logs/customer): alle durch admin-level Permissions (`settings:update`, `developer:access`, `audit:admin`, `audit:read`) geschĂŒtzt – Portal-User haben diese nicht. - **Token-in-URL (NIEDRIG)**: Langlebige Access-JWTs landeten als `?token=` in URLs fĂŒr PDF-iframe, Audit-Log-Export, PDF-Generate und Portal-Privacy-PDF → nginx-Access-Logs, Browser-History, Referer-Header. * **Neuer Mechanismus**: `POST /api/auth/download-token` liefert ein kurzlebiges JWT mit `type: 'download'` und `exp: 60s`. * Auth-Middleware akzeptiert `type: 'download'` AUSSCHLIESSLICH via `?token=` Query, niemals als Bearer-Header. So kann ein in Logs geleaktes Download-Token nicht fĂŒr regulĂ€re API-Aufrufe missbraucht werden. * Frontend-Migration: 4 Stellen umgestellt (Audit-Log-Export, PDF-Template-Preview, PDF-Generate von ContractDetail + Modal, Portal-Privacy-PDF). `fileUrl` und `getAttachmentUrl` sind synchron und in vielen Components verstreut – Migration dieser bleibt als Folge-Aufgabe. * Live-verifiziert: Download-Token = 1773 Zeichen, type=download, exp-iat=60s, als Header → 401, als ?token= → 200. - [x] **🚹 Pentest Runde 6 – Sammelfix + Strukturelles Audit (8 Findings + Audit-Sweep)** - **KRITISCH-01 `GET /emails/:id/thread`**: kein Owner-Check → Portal-Kunde konnte alle Mail-Threads durchsuchen. Fix: `canAccessCachedEmail` im Controller. - **KRITISCH-02 `GET /customers/:customerId/representatives/search`**: kein `canAccessCustomer` auf den Pfad → DSGVO-GAU, Portal-Kunde konnte mit Buchstaben-Brute-Force die komplette Kunden-DB auslesen. Fix eingefĂŒgt. - **HOCH-01 `GET /birthdays/upcoming`**: kein Portal-Filter → Name, E-Mail, Telefon, Geburtsdatum aller Kunden lesbar. Fix: `isCustomerPortal` → 403. - **HOCH-02 `*/contracts/:contractId/history`**: kein Owner-Check auf GET/POST/PUT/DELETE. Fix: `canAccessContract` in allen vier History-Handlern. - **HOCH-03 Mailbox-Endpoints**: `mailbox-accounts`, `unread-count`, `contracts/:id/emails/folder-counts` ohne Check. Fix: `canAccessCustomer` bzw. `canAccessContract` in allen drei. - **HOCH-04 Live-Vollmacht-Check in Tasks**: `getTasks`, `createSupportTicket`, `createCustomerReply`, `getAllTasks`, `getTaskStats` prĂŒften nur `representedCustomerIds.includes(...)` aus dem JWT – widerrufene Vollmachten hatten weiter Zugriff (JWT lebt bis zu 15min nach Widerruf). Neuer Helper `getPortalAllowedCustomerIds()` in `accessControl.ts` ruft `hasAuthorization()` live ab. Auch `updateCustomerConsent` (GDPR) auf diesen Pfad umgestellt. - **MITTEL-01 `confirmPasswordReset` Klartext-Speicherung**: Self-Service-Reset speicherte `portalPasswordEncrypted = encrypt(pw)`. Klartext-Speicherung ist nur fĂŒr Admin-OTPs sinnvoll. Fix: Field auf null, zusĂ€tzlich `portalPasswordMustChange = false`. - **MITTEL-02 Pagination-Total leakt globale Kunden-Anzahl**: `GET /customers` gab `total: 4271` auch wenn Portal-User nur 1 Kunde sah. Fix: `customer.service.ts` erweitert um `allowedIds`-Filter, der direkt in der DB-Query landet → die pagination zĂ€hlt nur ĂŒber erlaubte IDs. - **Strukturelles Audit-Sweep** (Sub-CRUD + Email-Operationen): Folgende Handler bekamen jetzt erstmals einen `canAccess*`- Check, defense in depth gegen falsch vergebene Rollen: `markAsRead`, `toggleStar`, `assignToContract`, `unassignFromContract`, `deleteEmail`, `getTrashEmails`, `getTrashCount`, `restoreEmail`, `permanentDeleteEmail`, `getAttachmentTargets`, `saveAttachmentTo`, `saveEmailAsPdf`, `saveEmailAsInvoice`, `saveAttachmentAsInvoice`, `saveAttachmentAsContractDocument`, `createFollowUp`, `createRenewal`, `snoozeContract`, `removeContractMeter`, `updateAddress`, `deleteAddress`, `updateBankCard`, `deleteBankCard`, `updateDocument`, `deleteDocument`, `updateMeter`, `deleteMeter`, `addMeterReading`, `updateMeterReading`, `deleteMeterReading`, `markReadingTransferred`, `addRepresentative`, `removeRepresentative`. - **Live-verifiziert** (Portal-User Customer 3 auf fremde IDs): `customers/1/representatives/search` → 403, `birthdays/upcoming` → 403 (Admin → 200), `emails/21/thread` → 403, `customers/2/mailbox-accounts` → 403, `emails/unread-count?customerId=2` → 403, `contracts/8/{history,folder-counts,follow-up,renewal,snooze}` → 403, eigene `customers/3` → 200, pagination.total fĂŒr Portal = 1 (statt 3), Customer 1 mit widerrufener Vollmacht → 0 fremde VertrĂ€ge. - [x] **🚹 Pentest Runde 5 – KRITISCH: change-initial-portal-password ohne Pflicht-Check** - **Realer Angriff**: Jeder Portal-User konnte jederzeit mit seinem eingeloggten Token `POST /api/auth/change-initial-portal- password` aufrufen und das eigene Passwort ohne Kenntnis des alten ersetzen. Der OTP-Flow-Endpoint hatte den Check `portalPasswordMustChange === true` nicht. - **Konsequenz**: Bei XSS oder kurzlebigem Token-Diebstahl konnte ein Angreifer das Passwort dauerhaft ĂŒbernehmen. - **Fix**: Eine Zeile in `auth.controller.ts` – `prisma.customer.findUnique` auf `portalPasswordMustChange`, bei `false` → 403 "Nicht erlaubt". - **Live-verifiziert**: ohne Flag → 403; mit Flag (nach send-credentials) → 200, danach Flag automatisch zurĂŒck auf `false` → erneuter Aufruf → 403. - [x] **Pentest Runde 5 – NIEDRIG: consentHash + Public-Grant-Response** - `consentHash` wurde ĂŒber `GET /api/customers/:id` zurĂŒckgegeben. Der Hash ist Pseudo-Credential fĂŒr den öffentlichen Consent-Link (wer ihn hat, sieht Customer-Name + Kundennummer ohne Auth und kann Einwilligungen erteilen). **Fix**: in `SENSITIVE_CUSTOMER_FIELDS` aufgenommen. Wer ihn legitim braucht, holt ihn ĂŒber `/gdpr/customer/:id/consent-status` (eigener Check). - `POST /api/public/consent/:hash/grant` gab den vollen `CustomerConsent[]`-Array inkl. IP-Adressen und `createdBy` (Kunden-Name) zurĂŒck. **Fix**: Response auf `{ granted: }` reduziert. Frontend nutzt eh nur `success`-Flag. - **Live-verifiziert**: `consentHash: null` in customer-Response, `consentHash` weiterhin in `/gdpr/.../consent-status`, Grant-Response liefert nur `{granted: 4}` ohne Extra-Keys. - [x] **🚹 Pentest Runde 4 – HOCH: Cockpit-IDOR (Portal-User sah ALLE Kunden)** - **Realer Angriff**: Portal-User Max bekam mit seinem Token `GET /api/contracts/cockpit` → komplette Vertragsliste ALLER Kunden (Customer-Namen, Vertragsnummern, StatĆ«s). - **Root Cause**: `contractCockpitService.getCockpitData()` filterte nicht nach Customer, weil das Cockpit ursprĂŒnglich nur fĂŒr Admins gedacht war. Die `contracts:read`-Permission haben aber auch Portal-User → Endpoint war erreichbar. - **Fix**: Service-Signatur erweitert auf `getCockpitData({ customerIds? })`. Wenn `customerIds` gesetzt sind, werden Haupt-Vertrags-Query, Consent-Maps, Ausweis- Warnungen und gemeldete ZĂ€hlerstĂ€nde allesamt auf diese IDs eingeschrĂ€nkt. Controller bestimmt `customerIds` analog zu `getContracts`: bei `isCustomerPortal` → eigene + vertretene Kunden (nur mit Vollmacht); sonst undefined (= alle). - **Live-verifiziert**: Admin sieht 17 VertrĂ€ge (3 Kunden); Portal-User Customer 1 sieht 12 (nur seine); Portal-User Customer 3 sieht 3 (nur seine); 0 Leaks. - [x] **🚹 Pentest Runde 3 – drei Findings gefixt** - **KRITISCH – `POST /api/developer/setup` ohne Auth (Privilege Escalation)**: Endpoint war komplett ohne Authentifizierung erreichbar und konnte der Admin-Rolle die `developer:access`- Permission verleihen → kompletter DB-Zugriff ĂŒber `/developer/*`. **Fix**: Endpoint ersatzlos gelöscht. Manuelles Setzen geht weiterhin ĂŒber `prisma/add-developer-permission.ts` (CLI). Live-verifiziert: `POST /api/developer/setup` → HTTP 404. - **HOCH – Customer-Login DoS auf Prod (fehlende Migration)**: `portalPasswordMustChange` war im Code, aber prod-DB kannte die Spalte nicht → Prisma warf bei jedem Kunden-Login. Root Cause: in dieser Session wurde `prisma db push` benutzt (kein Migration- File). **Fix**: handgenerierte Migration `20260516173552_portal_password_must_change/migration.sql` (via `prisma migrate diff` + `migrate resolve --applied`). Verifiziert durch shadow-DB-Reset + `migrate deploy`: Spalte landet korrekt in einer frischen DB. `entrypoint.sh` fĂŒhrt `migrate deploy` beim Container-Start bereits aus → Prod-Restart applied jetzt automatisch. - **MITTEL – Prisma-Internals-Leak im Login-Error-Body**: Bei unerwarteten Fehlern (Schema-Bruch, DB-Down) wurde `error.message` direkt zurĂŒckgegeben → Tabellen-/Spaltennamen leakten. **Fix**: Whitelist-Filter `safeLoginError()` in `auth.controller.ts`: nur bekannte Messages (`'UngĂŒltige Anmeldedaten'`, `'E-Mail und Passwort erforderlich'`) werden durchgereicht, alles andere wird zu generischem `'Anmeldung fehlgeschlagen'` und das Original landet im Server-Log. Greift fĂŒr Mitarbeiter- UND Portal- Login. Live-verifiziert: Spalte testweise gedropped → Client sieht generisch, Server-Log enthĂ€lt Original. - [x] **🔐 Einmalpasswort-Flow fĂŒr Portal-Credentials** - **Intention**: Wenn wir Zugangsdaten per E-Mail an den Kunden schicken, kennen wir das Passwort als Admin – das ist solange OK, bis er sich einmal eingeloggt hat. Danach soll er gezwungen sein, sich ein eigenes zu vergeben, und das per-Mail-Passwort ist tot. - **Datenmodell**: neues Feld `portalPasswordMustChange: Boolean @default(false)` am Customer. - **Flow**: 1. Admin klickt **Zugangsdaten versenden** → Flag wird gesetzt, Mail-Template weist explizit auf „Einmalpasswort" hin. 2. Kunde loggt sich mit dem OTP ein → Backend gibt `mustChangePassword: true` im Login-Response zurĂŒck UND **konsumiert das OTP sofort**: setzt `portalPasswordHash = null` und `portalPasswordEncrypted = null`. Ein zweiter Login mit demselben Passwort schlĂ€gt fehl (401). 3. Frontend (`ProtectedRoute`) sieht `mustChangePassword=true` und leitet auf `/change-initial-password` um – egal welche Route der Kunde aufrufen will, er kommt nicht weiter. 4. Auf der Seite gibt er ein neues, komplexes Passwort vor (Live-Hint mit ✓/○, dieselben Regeln wie Backend). 5. `POST /api/auth/change-initial-portal-password` speichert neuen Hash, **löscht das Encrypted-Feld** (Admin kann das eigene Passwort des Kunden nicht mehr im Klartext lesen), setzt `portalTokenInvalidatedAt = now()` und `portalPasswordMustChange = false`. 6. Frontend loggt aus, leitet zu `/login?changed=1`, Erfolgs-Banner: „Passwort wurde geĂ€ndert. Bitte mit dem neuen Passwort anmelden." - **Edge case**: Tab geschlossen ohne Setzen → Kunde ist ausgesperrt (OTP weg, eigenes Passwort nicht gesetzt). Lösung aus seiner Sicht: Passwort-vergessen-Funktion oder Admin versendet neue Zugangsdaten. - **Edge case**: Admin macht zwischendurch nochmal manuelles „Setzen" → `mustChange` wird automatisch wieder `false`. So kann ein versehentlich versendetes OTP problemlos durch ein direkt-gesetztes Passwort ersetzt werden. - **Live-verifiziert (10 Schritte)**: Setzen → Send → Flag in DB=true → Login mit OTP gibt mustChange=true zurĂŒck + Hash in DB ist null → Re-Login mit OTP → 401 → Change-Endpoint schwach → 400 → komplex → 200 → Login mit neuem PW → mustChange=false + tokenInvalidatedAt gesetzt. - [x] **🔐 Passwort-KomplexitĂ€t + Portal-Credentials-UX** - **Problem**: Bisher reichten 6 Zeichen fĂŒr gesetzte Passwörter (Portal-Login, User-Reset, Registrierung, User-Anlage). Das hat der Pentest bemĂ€ngelt, und es entsprach auch nicht dem, was wir selbst von Endkunden erwarten wĂŒrden. - **Lösung**: * `validatePasswordComplexity()` in `passwordGenerator.ts`: mind. 12 Zeichen + Großbuchstaben + Kleinbuchstaben + Ziffer + Sonderzeichen, mit detaillierter Fehlerliste auf deutsch. * Erzwungen in **5 Endpoints**: `setPortalPassword`, `confirmPasswordReset`, `register`, `createUser`, `updateUser`. - **Neue UX im Kunden-Portal-Block (CustomerDetail)**: * **Generate-Button**: erzeugt 16-Zeichen-Zufallspasswort, das garantiert allen KomplexitĂ€tsregeln entspricht, und fĂŒllt das Eingabefeld direkt aus. * **Send-Credentials-Button**: schickt Login-URL + Username + Klartext-Passwort an die Kunden-E-Mail. Funktioniert nur, wenn "Portal aktiviert" tatsĂ€chlich aktiviert ist. * **Live-KomplexitĂ€ts-Hint** beim Tippen: ✓/○-Liste zeigt sofort, welche Regeln noch fehlen. * `alert()`-Boxen durch Toast-Notifications ersetzt. - **Live-verifiziert**: schwaches Passwort `hallo123` → HTTP 400 mit Fehlerliste, komplexes Passwort `Hallo123!Test` → HTTP 200, Generator-Endpoint liefert 16-Zeichen-Passwort, Send-Credentials versendet Mail nur bei portalEnabled=true. - [x] **🌐 Real-IP hinter Nginx-Proxy-Manager** - **Problem**: Rate-Limiter und Security-Monitor haben statt der echten Client-IP nur die NPM-IP (`172.0.2.12`) geloggt. Damit wĂ€ren alle Threshold-basierten Blockings nutzlos – ein Brute- Force von 100 verschiedenen Clients wĂ€re fĂŒr uns 1 Quelle. - **Root Cause**: `app.set('trust proxy', 'loopback')` – das passt nur, wenn der Proxy auf 127.0.0.1 lĂ€uft. NPM lĂ€uft aber auf einem anderen Host, also wurde X-Forwarded-For ignoriert. - **Fix**: trust-proxy abhĂ€ngig von `HTTPS_ENABLED`: `HTTPS_ENABLED=true` → `1` (genau 1 Hop, der NPM), sonst `loopback` (Direkt-Verbindungen lokal). - **Live-verifiziert**: req.ip zeigt jetzt die echte Browser-IP statt der NPM-IP, Threshold-Events triggern korrekt. - [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 1–3: 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)