# OpenCRM Web-basiertes CRM-System für Kundenverwaltung mit Verträgen (Energie, Telekommunikation, KFZ-Versicherung). **Version: 1.1.0** ([Changelog](#changelog)) ## Features - **Kundenverwaltung**: Privat- und Geschäftskunden mit Stammdaten - **Adressen**: Mehrere Liefer-/Melde- und Rechnungsadressen pro Kunde - **Bankkarten**: Mit Ablaufdatum, Aktiv-Status und Dokument-Upload (PDF) - **Ausweise**: Personalausweis, Reisepass, etc. mit Ablaufdatum und Dokument-Upload (PDF) - **Zähler**: Strom-/Gaszähler mit Zählerstandhistorie, Pflicht-Zuordnung zur Lieferadresse und Folgezähler-Kette (Auto-Propagation auf alle Verträge mit dem Vorgänger) - **Rechnungen**: Rechnungsverwaltung für Energieverträge mit Dokumenten-Upload - **Vertrags-Cockpit**: Dashboard zur Überwachung offener Aufgaben (fehlende Dokumente, Rechnungen) - **Auto-Vertragsstatus**: Lieferbestätigung-Upload setzt `DRAFT` → `ACTIVE` (mit Vertragsbeginn), Kündigungsbestätigung-Upload setzt `ACTIVE` → `CANCELLED` (mit Datum), nightly-Cron setzt `ACTIVE`-Verträge mit abgelaufenem `endDate` auf `EXPIRED` - **Verträge**: - Energie (Strom, Gas) - Telekommunikation (DSL, Glasfaser, Mobilfunk, TV) - KFZ-Versicherung - **Folgeverträge**: Automatische Datenübernahme bei Anbieterwechsel - **Vertriebsplattformen**: Verwaltbar über WebUI - **Email-Provisionierung**: Automatische E-Mail-Weiterleitung bei Plesk/cPanel/DirectAdmin - **Berechtigungssystem**: Admin, Mitarbeiter, Nur-Lesen, Kundenportal - **Verschlüsselte Zugangsdaten**: Portal-Passwörter AES-256-GCM verschlüsselt - **DSGVO-Compliance**: Audit-Logging mit Hash-Chain-Integritätsprüfung, Einwilligungsverwaltung, Datenexport, Löschanfragen - **Sicherheits-Monitoring**: Realtime-Logging von Login-Fehlversuchen, IDOR-Abwehr, SSRF-Blocks, JWT-Manipulation; Threshold-Detection (Brute-Force, IDOR-Probing) mit Sofort-E-Mail-Alerts und stündlichem Digest – siehe Einstellungen → Monitoring - **Production-Hardening**: 10 dokumentierte Hardening-Runden inkl. CORS, Helmet, IDOR-Schutz, Rate-Limiting, SSRF/DNS-Rebinding-Block, Per-File-Ownership-Check, mehr in [docs/SECURITY-HARDENING.md](docs/SECURITY-HARDENING.md) - **Developer-Tools**: Datenbank-Browser und interaktives ER-Diagramm ## Tech Stack - **Frontend**: React 18, TypeScript, Tailwind CSS, React Query - **Backend**: Node.js, Express 4.x, TypeScript - **Datenbank**: MariaDB - **ORM**: Prisma - **Auth**: JWT-Access-Token (Memory, 15 min) + Refresh-Token im httpOnly-Cookie (7 Tage). Rollen-basierte Zugriffskontrolle. XSS klaut maximal einen 15-min-Access-Token, der Refresh-Cookie ist JS-unzugänglich. > **Hinweis zu Express 5:** Das Projekt verwendet bewusst Express 4.x (nicht 5.x). Express 5 ist seit Jahren in der Beta-Phase und noch nicht offiziell stable. Bei der Installation darauf achten, dass `@types/express` zur Express-Version passt: > - Express 4.x → `@types/express@^4.17.x` > - Express 5.x → `@types/express@^5.x` (erst bei offiziellem Release empfohlen) ## Quick-Start mit Docker (empfohlen) Komplettes Setup mit MariaDB + OpenCRM + Adminer (DB-UI) in 3 Befehlen: ```bash git clone cd opencrm cp .env.example .env # Werte anpassen, Secrets rotieren! docker compose up -d ``` Browser: - **CRM**: http://localhost:3010 (Login: `admin@admin.com` – Initial-Passwort siehe unten) - **Datenbank-UI** (Adminer): http://localhost:8081 (Server: `db`, User: `root`, DB: `opencrm`) Alle persistenten Daten liegen in `./data/`: | Pfad | Inhalt | |------|--------| | `./data/db/` | MariaDB-Datafiles | | `./data/uploads/` | User-Uploads (PDFs, Bilder) | | `./data/factory-defaults/` | Stammdaten-Kataloge | | `./data/backups/` | DB-Backups (`npm run db:backup`) | Ports + Pfade konfigurierst du in `./.env` (Default-Werte siehe `.env.example`). > **Erste Inbetriebnahme:** Bei leerer DB läuft der Seed automatisch > (kein Eingriff nötig). Nur wenn du eine bereits befüllte DB erneut > seeden willst, einmalig `RUN_SEED=true` in der `.env` setzen, > `docker compose up -d` ausführen, dann wieder auf `false`. > > Der initiale Admin-User ist **`admin@admin.com`**. Das Passwort wird > beim Seed zufällig generiert (28 Zeichen) und genau einmal in den > Container-Logs ausgegeben: > ```bash > docker logs opencrm-app 2>&1 | grep -A 5 "Initial-Passwort" > ``` > Alternativ kannst du **vor dem ersten Start** ein eigenes Passwort > setzen, indem du `SEED_ADMIN_PASSWORD=…` (mindestens 25 Zeichen, sonst > wird's ignoriert) in der `.env` einträgst. Nach dem ersten Login > sollte das Passwort über *Einstellungen → Mein Profil* geändert > werden. ## Voraussetzungen - Docker & Docker Compose v2 - Für Backend-Entwicklung außerhalb von Docker: Node.js 20+ und npm ## Installation für Entwicklung (ohne Container) ### 1. Repository klonen ```bash git clone cd opencrm cp .env.example .env # Konfiguration anpassen ``` ### 2. MariaDB-Container starten ```bash docker compose up -d db ``` Das startet nur die Datenbank (mit Daten in `./data/db/`). Konfiguration kommt aus `./.env`: - **Port:** wie in `DB_PORT` (Standard: 3306, intern auf 127.0.0.1) - **Datenbank/User/Passwort:** wie in `DB_*`-Variablen Warte ca. 10 Sekunden bis die Datenbank bereit ist. ### 3. Backend einrichten ```bash cd backend # Dependencies installieren npm install # .env-Datei erstellen (falls noch nicht vorhanden) cp .env.example .env ``` Die `.env`-Datei sollte folgende Werte enthalten: ```env # Database DATABASE_URL="mysql://root:rootpassword@localhost:3306/opencrm" # JWT – Access-/Refresh-Token-Pattern (SPA-Standard) # Access-Token (Bearer-Header, nur im Browser-Memory, kurzlebig) # Refresh-Token (httpOnly-Cookie, lang) # Beide werden mit JWT_SECRET signiert; Refresh wird nur am # /api/auth/refresh-Endpoint akzeptiert (type-Claim). JWT_SECRET="change-this-to-a-very-long-random-secret-in-production" JWT_EXPIRES_IN="15m" # Access-Token-Lifetime (Default: 15m) JWT_REFRESH_EXPIRES_IN="7d" # Refresh-Token-Lifetime (Default: 7d) # Encryption (for portal credentials) - generate with: openssl rand -hex 32 ENCRYPTION_KEY="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" # Server PORT=3001 NODE_ENV=development ``` ### 4. Datenbank initialisieren ```bash # Prisma Client generieren und Migrationen ausführen npx prisma migrate dev # Seed-Daten einspielen (Admin-User, Rollen, Berechtigungen) npm run db:seed ``` ### 5. Frontend einrichten ```bash cd ../frontend # Dependencies installieren npm install ``` ## Anwendung starten ### Backend starten (Terminal 1) ```bash cd backend npm run dev ``` Das Backend läuft auf `http://localhost:3001` ### Frontend starten (Terminal 2) ```bash cd frontend npm run dev ``` Das Frontend läuft auf `http://localhost:5173` ## Erster Login Nach dem Seed sind folgende Zugangsdaten verfügbar: - **E-Mail:** `admin@admin.com` - **Passwort:** wird beim Seed zufällig generiert (28 Zeichen) und einmal in den Container-Logs ausgegeben: ```bash docker logs opencrm-app 2>&1 | grep -A 5 "Initial-Passwort" ``` Beispiel-Ausgabe: ``` ======================================================== Admin-User: admin@admin.com Initial-Passwort: <28-zeichen-zufalls-passwort> ⚠️ Dieses Passwort wird hier EINMAL ausgegeben! Bitte sofort nach dem ersten Login ändern. ======================================================== ``` Wer das Log nicht mehr findet (z.B. Container schon länger gerollt), setzt `SEED_ADMIN_PASSWORD=` in der `.env` und startet einmalig mit `RUN_SEED=true` neu – der Seed ist idempotent und überschreibt das Admin-Passwort. Danach `RUN_SEED` wieder auf `false`. > **Tipp:** Wer von Anfang an ein eigenes Admin-Passwort haben möchte, > setzt `SEED_ADMIN_PASSWORD=` **bevor** der erste > `docker compose up -d` läuft. Der Wert wird dann statt des Zufalls- > passworts verwendet (siehe `.env.example`). > **Wichtig:** Vor dem ersten Production-Deployment Secrets rotieren – > siehe [Production-Deployment](#production-deployment). ### Zugangsdaten auf bestehender DB neu setzen (Reseed) Nützlich z.B. auf **Staging/Test nach einem DB-Reset**, wenn Admin-/Portal- Passwörter randomisiert sind und man wieder **bekannte** Zugangsdaten braucht (etwa für einen Pentester). **Der Seed ist datenerhaltend:** Er `upsert`et nur die Rollen und den Admin-User (`admin@admin.com`) und synchronisiert die Rollen-Berechtigungen. **Kundendaten, Verträge, Bankkarten usw. werden NICHT angefasst** (das einzige `deleteMany` betrifft ausschließlich Rollen↔Permission-Zuordnungen). Ein erneuter Seed setzt also faktisch nur das Admin-Passwort neu. **Admin-Passwort deterministisch neu setzen:** ```bash # 1) In der .env / docker-compose environment setzen: SEED_ADMIN_PASSWORD= RUN_SEED=true # erzwingt den Seed auch bei nicht-leerer DB # 2) Container neu starten – der Entrypoint seedet erneut (upsert): docker compose up -d # 3) Login: admin@admin.com / # 4) Danach RUN_SEED wieder auf false (sonst re-seedet jeder Restart – harmlos, # aber unnötig). SEED_ADMIN_PASSWORD kann gesetzt bleiben. ``` > ⚠️ `SEED_ADMIN_PASSWORD` **muss ≥ 25 Zeichen** sein, sonst wird es ignoriert > (Passwort-Policy) und der Seed generiert wieder ein Zufallspasswort (→ Logs, > siehe oben). **Portal-Login (Kunde):** Portal-Passwörter werden **nicht** vom Seed gesetzt. Nach dem Admin-Login lässt sich in der Kundenakte für einen (Test-)Kunden das Portal-Passwort im UI **anzeigen/neu setzen** – damit hat man wieder einen bekannten Portal-Zugang für Scoping-Tests. ## Production-Deployment Vor dem öffentlichen Schalten der Instanz muss in der Production-`.env`: ```env NODE_ENV=production # Pflicht-Rotation – per `openssl rand` neu generieren! JWT_SECRET=$(openssl rand -hex 64) # min. 32 Zeichen ENCRYPTION_KEY=$(openssl rand -hex 32) # genau 64 Hex-Zeichen # Backend nur lokal lauschen lassen, public-Verkehr läuft über Reverse-Proxy LISTEN_ADDR=127.0.0.1 # Bei separatem Frontend-Host: erlaubte Origins CORS_ORIGINS=https://crm.deine-domain.de # Siegel fuer das Audit-Log – schuetzt die Beweisspur vor nachtraeglicher # Faelschung. Siehe Abschnitt "Audit-Siegel" weiter unten. AUDIT_HMAC_KEY=$(openssl rand -hex 32) ``` ### Deployment-Modus: On-Prem vs. Cloud OpenCRM ist primär als **On-Prem-Anwendung** designed (eigener Server / VM, hinter Reverse-Proxy). Für **Cloud-Deployments** (öffentlich erreichbares Backend, Shared-Infrastructure, Hyperscaler) gibt es einen zusätzlichen SSRF-Schalter: ```env # Cloud-Deploy: zusätzlich alle privaten IP-Ranges für Provider-/SMTP- # Test-Connection blockieren (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, # 192.168.0.0/16, ::1, fc00::/7, localhost). Default false, weil # On-Prem-Setups oft Plesk/Dovecot auf 127.0.0.1 brauchen. SSRF_BLOCK_PRIVATE_IPS=true ``` ### Audit-Siegel (`AUDIT_HMAC_KEY`) **Worum geht es?** OpenCRM führt ein Audit-Log: eine lückenlose Aufzeichnung, wer wann welche Daten gesehen oder geändert hat. Das ist die Beweisspur, wenn es Streit gibt, etwas verschwindet oder der Datenschutz nachfragt. Damit diese Aufzeichnung etwas wert ist, muss man ihr ansehen können, ob jemand nachträglich daran herumgeschrieben hat. Dafür trägt jeder Eintrag einen Fingerabdruck, der auch den Fingerabdruck des vorherigen Eintrags enthält – wie eine Kette. Ändert jemand einen alten Eintrag, passen die Fingerabdrücke nicht mehr und es fällt auf. **Und wozu dann noch ein Schlüssel?** Weil jemand mit Zugriff auf die Datenbank die ganze Kette neu berechnen könnte. Er ändert also einen Eintrag – zum Beispiel einen fehlgeschlagenen Login-Versuch in einen erfolgreichen – und zieht anschließend alle Fingerabdrücke glatt. Danach sieht die Fälschung echt aus. Mit `AUDIT_HMAC_KEY` bekommt jeder Eintrag zusätzlich ein **Siegel**, das sich nur mit diesem Schlüssel erzeugen lässt. Der Schlüssel liegt in der `.env`-Datei, also **außerhalb der Datenbank**. Wer nur die Datenbank in die Hände bekommt, kann damit nichts fälschen, ohne dass es auffliegt. > Bildlich: Die Fingerabdruck-Kette ist die fortlaufende Nummerierung der > Aktenseiten. Der Schlüssel ist der Stempel des Notars. Die Akte steht zwar > im Schrank – aber den Stempel hat nur der Notar. **Einrichten** ```bash # Einmalig pro Umgebung einen Schlüssel erzeugen openssl rand -hex 32 ``` **In welche Datei?** Das hängt davon ab, wie du OpenCRM betreibst: | Betrieb | Datei | |---|---| | **Docker** (Standard) | **`.env` im Projektstamm** | | Backend direkt, ohne Container (Entwicklung) | `backend/.env` | Beim Docker-Betrieb reicht der Eintrag in der Stamm-`.env` – `docker-compose.yml` reicht die Variable an den Container weiter. Ein Eintrag in `backend/.env` wirkt dort **nicht**, weil der Container diese Datei nicht liest. Wichtig: **pro Umgebung ein eigener Schlüssel** (Entwicklung, Test, Produktion) – und niemals ins Git-Repository. **Bestehende Installation: bisher kein Schlüssel gesetzt?** Genau so ist es gedacht – der Schlüssel kam mit einem Update dazu. Du kannst ihn jederzeit nachträglich setzen: 1. Schlüssel erzeugen (`openssl rand -hex 32`), in die `.env` eintragen 2. Backend neu starten 3. Fertig – mehr ist nicht nötig Was dabei mit deinen **bereits vorhandenen** Einträgen passiert: - Sie bleiben **unverändert** und weiterhin gültig. Es wird nichts neu berechnet und nichts nachträglich gesiegelt. - Ab dem Neustart werden **neue** Einträge gesiegelt. Alt und neu liegen friedlich nebeneinander, die Prüfung erkennt für jeden Eintrag automatisch, nach welchem Verfahren er zu prüfen ist. - Es gibt **keine** Fehlalarme dadurch. (Nachgemessen auf einer Installation mit 4903 alten, 57 mittleren und 67 gesiegelten Einträgen: 0 Beanstandungen.) Rückwirkend lässt sich der Schutz nicht herstellen – ältere Einträge bleiben ungesiegelt. Das ist kein Fehler, sondern liegt in der Natur der Sache: Ein Siegel kann man nur beim Anlegen vergeben, nicht nachträglich. Je früher du den Schlüssel setzt, desto größer der geschützte Zeitraum. > ⚠️ **Einmal gesetzt, nicht wieder abschalten.** Fehlt der Schlüssel später > (z. B. weil er beim Container-Rebuild aus der `.env` verlorenging), läuft die > Anwendung normal weiter – aber die in dieser Zeit entstandenen Einträge sind > ungesiegelt und werden von der Prüfung **beanstandet**, sobald der Schlüssel > wieder da ist. Das ist Absicht: Ein ungesiegelter Eintrag inmitten > gesiegelter ist von einer Fälschung nicht zu unterscheiden. Nimm den > Schlüssel deshalb in deine Deploy-Checkliste auf. Ein noch nicht ersetzter Platzhalter (``) zählt bewusst **nicht** als Schlüssel – sonst würde mit einem öffentlich bekannten Wert gesiegelt. Das Backend schreibt in diesem Fall eine Warnung ins Log und lässt das Siegel aus. **Häufige Fragen** | Frage | Antwort | |---|---| | Was, wenn ich den Schlüssel gar nicht setze? | Nichts fällt aus. Das Audit-Log läuft normal weiter, nur ohne dieses zusätzliche Siegel. | | Was, wenn ich ihn verliere? | Lässt du das Feld dann **leer**, gelten die gesiegelten Einträge als **„nicht prüfbar"** – ausdrücklich nicht als gefälscht, also kein Fehlalarm. Der Nachweis für diesen Zeitraum ist aber weg. Deshalb: sichern wie ein Passwort. | | Und wenn ich stattdessen einen **neuen** Schlüssel eintrage? | Dann werden die alten Einträge als **„manipuliert" gemeldet** – das System kann einen falschen Schlüssel nicht von einer echten Fälschung unterscheiden. Trage den alten Schlüssel deshalb zusätzlich unter `AUDIT_HMAC_KEY_OLD` ein (siehe unten). | | Muss ich ihn irgendwo eintragen außer in der `.env`? | Nein. Einmal setzen, Backup anlegen, fertig. | | Verlangsamt das etwas? | Nein, spürbar nicht. | **Schlüssel wechseln (`AUDIT_HMAC_KEY_OLD`)** Möchtest du den Schlüssel austauschen – etwa weil du vermutest, dass er in falsche Hände geraten ist – geht das ohne Datenverlust: ```env AUDIT_HMAC_KEY= AUDIT_HMAC_KEY_OLD= ``` Neue Einträge werden ab sofort mit dem neuen Schlüssel gesiegelt, die bisherigen bleiben über den alten Schlüssel weiterhin prüfbar. > ⚠️ **`AUDIT_HMAC_KEY_OLD` nicht voreilig leeren.** Solange Einträge > existieren, die mit einem alten Schlüssel gesiegelt wurden, muss dieser dort > stehen bleiben. Entfernst du ihn, werden diese Einträge als **„manipuliert" > gemeldet** – nicht als „nicht prüfbar". Das System kann einen falschen > Schlüssel nicht von einer echten Fälschung unterscheiden. Leeren kannst du > das Feld gefahrlos erst, wenn die betroffenen Einträge durch die > Aufbewahrungsfristen ohnehin gelöscht sind. **Mehrfach gewechselt?** Das Feld nimmt mehrere Schlüssel kommagetrennt auf – vom jüngsten zum ältesten: ```env AUDIT_HMAC_KEY= AUDIT_HMAC_KEY_OLD=,, ``` Ohne das würde beim zweiten Wechsel der zuerst genutzte Schlüssel verloren gehen – und alle damit gesiegelten Einträge fälschlich als manipuliert erscheinen. **Prüfen, ob alles in Ordnung ist** Die Prüfung läuft derzeit nur über die API (eine Schaltfläche in der Oberfläche gibt es dafür noch nicht) – als angemeldeter Benutzer mit dem Recht `audit:read`: ```bash curl -X POST https://crm.deine-domain.de/api/audit-logs/verify \ -H "Authorization: Bearer " ``` Die Antwort enthält einen Klartext-Satz im Feld `message` und unterscheidet zwei Dinge: - **„Manipulierte Einträge"** – jemand hat einen bestehenden Eintrag nachträglich verändert. Das ist ernst. - **„Strukturelle Lücken"** – die Kette hat eine Unterbrechung, die Inhalte sind aber unverändert. Meist harmlos (z. B. gelöschte alte Einträge). Notiere dir die Zahl nach dem ersten Deploy: Bleibt sie konstant, ist alles in Ordnung. Steigt sie, lohnt ein Blick.
Technische Details (für Entwickler/Admins) **Verfahren.** Jeder Audit-Eintrag trägt einen Hash über seine Inhaltsspalten plus den Hash des Vorgängers (`previousHash`) – daraus entsteht die Kette. Es gibt drei Hash-Versionen, die Spalte `hashVersion` hält fest, welche verwendet wurde: | Version | Verfahren | Abdeckung | |---|---|---| | 1 | SHA-256 | 7 Felder – Altbestand vor der Härtung | | 2 | SHA-256 | alle 24 Inhaltsspalten, unsigniert | | 3 | **HMAC-SHA256** mit `AUDIT_HMAC_KEY` | alle 24 Inhaltsspalten, signiert | Bestandsdaten werden **nicht** nachträglich neu berechnet – ein Rehash würde die Beweiskraft der Vergangenheit überschreiben. Alte Einträge bleiben mit ihrer Version gültig. **Version-Floor.** Die zu erwartende Version wird aus der Kette abgeleitet (erste je mit Version *n* geschriebene Zeile), **nicht** aus der Selbstauskunft der Zeile. Sonst ließe sich per Downgrade (`hashVersion` 3 → 1) die schwächere Prüfung erzwingen und anschließend ein gültiger Hash über die wenigen abgedeckten Felder nachziehen. Zusätzlich gilt: Eine unerklärte Lücke direkt vor einer signierten Zeile ist ein Befund, kein struktureller Zufall – deren `previousHash` lässt sich ohne Schlüssel nicht fälschen. **Löschungen.** `POST /api/audit-logs/cleanup` schreibt ein Manifest (ID-Bereich, Anzahl, Policy, Cutoff) als eigenen verketteten Eintrag. Die Prüfung liest es aus und meldet nur Lücken **ohne** dokumentierte Löschung als erklärungsbedürftig (`unexplainedGaps`) – sonst könnte sich eine böswillige Löschung als harmlose Lücke tarnen. **Rückgabe von `POST /api/audit-logs/verify`:** | Feld | Bedeutung | |---|---| | `tamperedEntries` | Inhalt nachträglich verändert, Versions-Downgrade oder gebrochener Anker – **ernst** | | `chainGaps` | Verkettung unterbrochen (alle) | | `unexplainedGaps` | Teilmenge davon ohne dokumentierte Löschung | | `unverifiableEntries` | signiert, aber kein Schlüssel konfiguriert – **kein** Manipulationsverdacht | **Fail-safe.** Ohne `AUDIT_HMAC_KEY` schreibt der Dienst weiter Version 2; das Logging fällt nie wegen fehlender Konfiguration aus. **Bekannte Grenze.** Der Anker schützt gegen reinen Datenbank-Schreibzugriff. Wer Schlüssel *und* Datenbank kontrolliert, kann die Kette konsistent neu rechnen. Eine Off-Site-Notarisierung (regelmäßiger Export/Versiegelung des Kettenkopfes außerhalb des Systems) wäre die nächste Stufe. **Achtung – zwei Endpunkte mit Nebenwirkung:** `POST /api/audit-logs/rehash` berechnet alle Hashes neu; danach meldet die Prüfung überall „gültig", aber eine bestehende Fälschung würde mitbesiegelt. `POST /api/audit-logs/cleanup` löscht nach Aufbewahrungsregeln, jede gelöschte Zeile erzeugt eine Lücke. Beide laufen nur manuell und schreiben einen Marker ins Log.
Cloud-Metadata-Endpoints (`169.254.169.254`, `metadata.google.internal` etc.) sind UNABHÄNGIG vom Flag **immer** geblockt – das ist Mindestschutz gegen AWS/GCP/Azure-IMDS-Diebstahl. Plus: - **Reverse-Proxy** (Nginx/Plesk) so konfigurieren, dass `X-Forwarded-For` hart auf die echte Client-IP gesetzt wird (nicht nur angefügt) – sonst Rate-Limit-Bypass möglich. - **Frontend + API müssen über dieselbe Origin laufen.** Die Auth nutzt einen httpOnly-Refresh-Cookie mit `SameSite=Strict; Path=/api/auth` – wenn Frontend und API auf getrennten Origins liegen (z.B. `crm.example.de` vs. `api.example.de`), schickt der Browser das Cookie cross-site nicht mit und der `/auth/refresh`-Endpoint kann den User nicht mehr nachladen (= alle 15 min Re-Login). Beim NPM-Setup landen Frontend und API automatisch auf derselben Domain via Proxy-Path. - **Admin-Initial-Passwort ändern**: das beim Seed generierte Zufalls-Passwort (siehe [Erster Login](#erster-login)) im Profil überschreiben – oder gleich per `SEED_ADMIN_PASSWORD` in der `.env` vor dem ersten Start setzen. - **Manuelle Test-Checkliste** aus [docs/TESTING.md](docs/TESTING.md) einmal komplett durchklicken. - **Monitoring konfigurieren**: Einstellungen → Sicherheits-Monitoring → Alert-E-Mail hinterlegen, Test-Alert senden, Digest aktivieren. - Vollständige Hardening-Story + restliche Trade-offs: [docs/SECURITY-HARDENING.md](docs/SECURITY-HARDENING.md) ### ⚠️ Wichtig: gzip für `/api/*` am Reverse-Proxy deaktivieren (BREACH-Schutz) Wenn ein TLS-Reverse-Proxy (Nginx Proxy Manager, Caddy, eigener Nginx, …) HTTPS terminiert und Antworten gzip-komprimiert, ist die **BREACH-Attacke** (CVE-2013-3587) theoretisch möglich: aus der gzip-komprimierten Response-Größe könnten unter ungünstigen Umständen Secrets erraten werden. Auch wenn unsere JWT-basierte SPA das Risiko praktisch klein hält (keine reflektierten Secrets im Response-Body), geht ein Penetration-Test mit testssl trotzdem auf „medium – Ausnutzbar: Ja". **Lösung:** gzip-Komprimierung nur für statische Frontend-Assets erlauben, für `/api/*` deaktivieren. Statische Bundles bleiben damit performant ausgeliefert, JSON-API-Responses werden ohne Kompression gesendet → BREACH ist dort kein Einfallstor mehr. **Nginx Proxy Manager (NPM):** 1. Proxy-Hosts → den CRM-Host → **Edit** 2. Tab **Custom Locations** → **„Add location"** 3. **Define location:** `/api/` 4. **Scheme:** `http`, **Forward Hostname/IP:** wie im Haupt-Host (z.B. `172.0.2.39`), **Forward Port:** `3010` 5. Zahnrad rechts an der Location → erweiterte Config eintragen: ```nginx gzip off; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; # Information-Disclosure-Header weg (Pentest-Hygiene): more_clear_headers Server X-Served-By; ``` 6. **Save** (Location), **Save** (Proxy-Host) > Der `more_clear_headers`-Befehl kommt aus dem `headers-more`-Modul, das > bei NPM standardmäßig dabei ist. Damit verschwinden die Banner > `Server: openresty` und `x-served-by: …` aus den Responses – Pentest- > Tools können den eingesetzten Webserver nicht mehr direkt aus dem Header > ablesen. Wer das auch auf der Hauptlocation will, kann denselben Eintrag > zusätzlich im **Advanced**-Tab des Proxy-Hosts setzen. **Plain Nginx** (falls eigener Nginx statt NPM): ```nginx location /api/ { gzip off; proxy_pass http://backend:3010; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; more_clear_headers Server X-Served-By; # braucht headers-more-Modul } # Optional global im server { … }-Block: server_tokens off; ``` **Verifikation:** ```bash # 1) gzip ist für /api/ deaktiviert (sollte leer sein) curl -sI -H 'Accept-Encoding: gzip' https://kundencenter.deine-domain.de/api/health \ | grep -i content-encoding # 2) Server-/x-served-by-Banner sind weg (sollte leer sein) curl -sI https://kundencenter.deine-domain.de/api/health \ | grep -iE '^(server|x-served-by):' ``` #### Was mit gzip auf `/` (SPA-HTML) ist Pentest-Tools wie `testssl` melden BREACH **trotzdem weiter** für die Root-URL `/`, weil die SPA-`index.html` bewusst weiter gzip-komprimiert ausgeliefert wird (Performance: 50 KB → ~10 KB). Bei OpenCRM ist der Angriff dort nicht ausnutzbar: - Die `/`-Response ist die statische `index.html` aus dem Vite-Build - Sie reflektiert **keinen user-controlled Input** - Sie enthält **keine Secrets** (JWT-Access ist im `Authorization`-Header, Refresh-Token im httpOnly-Cookie – beides nicht im HTML-Body) Ohne Secret-im-Body und ohne Input-Reflektion hat BREACH keinen Hebel. ##### Wer den Audit-Marker trotzdem weg haben will Wichtig: nicht einfach eine Custom-Location für `/` mit `gzip off` anlegen – das wäre ein **prefix-Match** und würde **alle** Pfade außer `/api/*` betreffen, also auch `/assets/*.{js,css}`. Das JS-Bundle käme dann unkomprimiert (~500 KB statt ~150 KB) → spürbarer Performance-Verlust für nichts. Sauber ist eine **exact-Match-Location** (`location = /`) – die fängt nur die Root-URL ohne weitere Pfad-Komponente: **Variante A** – Custom Location im NPM-UI (falls `= /` im „Define location"-Feld akzeptiert wird): | Feld | Wert | |---|---| | Define location | `= /` | | Scheme | `http` | | Forward Hostname/IP | wie im Haupt-Host | | Forward Port | `3010` | Im Zahnrad-Edit der Location: ```nginx gzip off; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; # Information-Disclosure-Header weg (Pentest-Hygiene): more_clear_headers Server X-Served-By; ``` **Variante B** – wenn das NPM-UI das `=` nicht akzeptiert, dieselbe Logik im **Advanced**-Tab des Proxy-Hosts: ```nginx location = / { gzip off; proxy_pass $forward_scheme://$server:$port; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; more_clear_headers Server X-Served-By; } ``` Verifikation – `/` ohne gzip, `/assets/*` aber weiter mit: ```bash # Root: kein Content-Encoding mehr curl -sI -H 'Accept-Encoding: gzip' https://kundencenter.deine-domain.de/ \ | grep -i content-encoding # /assets/.js: weiterhin gzip (Performance bleibt erhalten) JS=$(curl -s https://kundencenter.deine-domain.de/ | grep -oE 'assets/index-[A-Za-z0-9_-]+\.js' | head -1) curl -sI -H 'Accept-Encoding: gzip' "https://kundencenter.deine-domain.de/$JS" \ | grep -i content-encoding ``` Kostet 40 KB extra pro Tab-Reload – aber dafür ist auch der letzte BREACH-Marker weg und Pentest-Reports landen auf 0×MEDIUM. ## Developer-Tools aktivieren Die Developer-Tools (Datenbankstruktur, ER-Diagramm) sind standardmäßig für Admins verfügbar. Falls der Menüpunkt nicht erscheint: 1. Einmalig im Browser-Console ausführen: ```javascript fetch('/api/developer/setup', { method: 'POST' }) ``` 2. Ausloggen und neu einloggen Alternativ können Developer-Rechte pro Benutzer vergeben werden: - Benutzer bearbeiten > "Entwicklerzugriff" aktivieren ## Email-Provisionierung Das System unterstützt die automatische Erstellung von E-Mail-Weiterleitungen auf Hosting-Servern für Stressfrei-Wechseln Adressen. ### Unterstützte Provider - **Plesk** (implementiert) - **cPanel** (vorbereitet) - **DirectAdmin** (vorbereitet) ### Konfiguration 1. **Einstellungen** → **Email-Provisionierung** öffnen 2. Neuen Provider hinzufügen: - **Name**: Bezeichnung (z.B. "Plesk Hauptserver") - **Typ**: Plesk/cPanel/DirectAdmin - **API-URL**: Server-URL (z.B. `https://server.de:8443`) - **API-Key** _(empfohlen bei Plesk)_: Key aus Plesk (siehe unten), alternativ Benutzername/Passwort - **Benutzername/Passwort**: Nur wenn kein API-Key vorhanden - **Domain**: E-Mail-Domain (z.B. `stressfrei-wechseln.de`) - **Standard-Weiterleitung**: Zusätzliche Weiterleitungsadresse (optional) 3. Provider als "Standard" und "Aktiv" markieren 4. Verbindung testen ### Plesk: API-Key anlegen Der API-Key ist die empfohlene Authentifizierungsmethode (sicherer als Passwort, kann pro Anwendung vergeben und widerrufen werden). **Variante 1: Über die Plesk-Oberfläche (einfachster Weg)** 1. In Plesk als Admin einloggen 2. Oben rechts auf den **eigenen Namen** → **"Mein Profil"** (oder direkt URL `/admin/my-profile/`) 3. Tab **"API-Token"** oder **"API-Schlüssel"** öffnen 4. **"API-Schlüssel erstellen"** (bzw. "Add API Key") 5. Beschreibung vergeben (z.B. "OpenCRM") 6. Den angezeigten Schlüssel **sofort kopieren** – er wird nur einmal angezeigt! 7. Im CRM bei "API-Key" einfügen > **Hinweis:** Bei manchen Plesk-Versionen ist die Option unter > **Tools & Einstellungen** → **API-Schlüssel** oder **Werkzeuge & Einstellungen** → > **API-Tokens** zu finden. Wenn der Menüpunkt fehlt, muss ggf. die **REST API** > Extension installiert werden (siehe Variante 2). **Variante 2: Über die Kommandozeile (SSH als root)** Falls der API-Key-Button in Plesk nicht vorhanden ist, lässt er sich auch per SSH erstellen: ```bash # API-Key generieren (läuft nicht ab) # WICHTIG: -ip-address weglassen, wenn der Key von beliebigen IPs genutzt werden soll! plesk bin secret_key --create -description "OpenCRM" # Alternativ mit IP-Einschränkung (nur Zugriffe von dieser IP sind erlaubt): plesk bin secret_key --create -ip-address -description "OpenCRM" ``` > **Achtung:** `-ip-address 0.0.0.0` funktioniert **nicht** wie bei anderen Tools! > Plesk prüft exakt gegen die eingetragene IP. Für "alle IPs erlauben" muss der > `-ip-address`-Parameter komplett weggelassen werden. Der Befehl gibt den Key direkt zurück. Diesen kopieren und im CRM eintragen. **Alle API-Keys anzeigen:** ```bash plesk bin secret_key --list ``` **API-Key löschen:** ```bash plesk bin secret_key --delete ``` ### Plesk: REST API aktivieren (falls nicht vorhanden) Bei älteren Plesk-Versionen oder Custom-Installationen kann es sein, dass die REST API fehlt. Dann: 1. **Tools & Einstellungen** → **Updates** → **Erweiterungen hinzufügen/entfernen** 2. Nach **"REST API"** suchen und installieren 3. Plesk-Neustart (meist nicht nötig, aber zur Sicherheit) ### Plesk: Firewall-Hinweis Der CRM-Server muss den **Plesk-Port 8443** (Standard) erreichen können. Bei Plesk-Firewall: 1. **Tools & Einstellungen** → **Firewall** 2. **"Plesk-Dienst – Panel"** (Port 8443) für die IP des CRM-Servers erlauben Bei reiner Linux-Firewall (ufw/firewalld): ```bash # Beispiel ufw ufw allow from to any port 8443 ``` ### Verwendung Beim Anlegen einer Stressfrei-Wechseln Adresse im Kundenbereich erscheint die Checkbox **"Beim E-Mail-Provider anlegen"**, wenn: - Ein aktiver Standard-Provider konfiguriert ist - Der Kunde eine E-Mail-Adresse hat Bei aktivierter Checkbox wird automatisch: 1. Geprüft, ob die E-Mail-Adresse bereits existiert 2. Falls nicht: E-Mail-Adresse beim Provider angelegt mit Weiterleitung an: - Kunden-E-Mail-Adresse - Standard-Weiterleitungsadresse (falls konfiguriert) ## Befehle ### Backend ```bash npm run dev # Entwicklungsserver starten npm run build # Produktions-Build erstellen npm run db:studio # Prisma Studio (Datenbank-GUI) npm run db:migrate # Neue Migration erstellen npm run db:seed # Seed erneut ausführen ``` ### Frontend ```bash npm run dev # Entwicklungsserver starten npm run build # Produktions-Build erstellen npm run preview # Build-Vorschau ``` ### Docker (Entwicklung) ```bash docker-compose up -d # Container starten docker-compose down # Container stoppen docker-compose down -v # Container stoppen + Daten löschen docker-compose logs -f # Logs anzeigen ``` ### Betrieb mit eigenem SSL (optionaler Caddy-Dienst) Es gibt **eine** `docker-compose.yml` und **eine** `.env` im Projektstamm. Standardmässig bringt der Stack **keinen** Reverse-Proxy mit – die Annahme ist, dass du bereits einen hast (nginx, Nginx Proxy Manager, Traefik) und OpenCRM dahinter hängst. Hast du **keinen**, kannst du Caddy als optionalen Dienst dazuschalten. Er holt und erneuert das SSL-Zertifikat automatisch: Dafür gibt es einen Schalter in der `.env` – nichts muss auskommentiert werden: ```env COMPOSE_PROFILES=caddy # leer = kein Caddy (Standard) DOMAIN=crm.example.de HTTPS_ENABLED=true ``` ```bash docker-compose up -d # startet jetzt zusätzlich Caddy ``` Solange `COMPOSE_PROFILES` leer ist, wird der Caddy-Dienst **gar nicht erst angelegt** – der Stack verhält sich exakt wie ohne ihn. Der Schalter wirkt auch bei `down`, `logs` und `ps`, im Gegensatz zum Flag `--profile caddy`, das man bei Folgebefehlen leicht vergisst. **Was dabei zu beachten ist:** - Die Zertifikate liegen unter `./data/caddy`. Diesen Ordner **nicht löschen**, sonst werden bei jedem Start neue Zertifikate angefordert und du läufst in die Mengenbegrenzung von Let's Encrypt. - Die Domain muss vorher per DNS auf den Server zeigen, sonst schlägt die Zertifikatsausstellung fehl. - `HTTPS_ENABLED=true` setzen – das aktiviert das `Secure`-Attribut am Refresh-Cookie und die korrekte Proxy-Erkennung. - Der direkte App-Port (`OPENCRM_PORT`, Standard 3010) bleibt weiterhin offen. Wenn nur Caddy von aussen erreichbar sein soll, schliesse ihn per Firewall oder setze ihn in der `.env` auf einen Wert, den du nicht freigibst. Die Konfiguration liegt in der `Caddyfile` im Projektstamm. Sie liefert Sicherheits-Header mit und komprimiert bewusst **nicht** die API-Antworten (BREACH-Schutz, siehe Abschnitt zum Reverse-Proxy oben). ## Projektstruktur ``` opencrm/ ├── backend/ │ ├── src/ │ │ ├── controllers/ # Request-Handler │ │ │ ├── auditLog.controller.ts # Audit-Log API │ │ │ └── gdpr.controller.ts # DSGVO API │ │ ├── middleware/ # Auth, Validation │ │ │ ├── audit.ts # Automatisches API-Logging │ │ │ └── auditContext.ts # Before/After Context │ │ ├── routes/ # API-Endpunkte │ │ │ ├── auditLog.routes.ts # Audit-Log Routes │ │ │ └── gdpr.routes.ts # DSGVO Routes │ │ ├── services/ # Business-Logik │ │ │ ├── audit.service.ts # Hash-Kette, Logging │ │ │ ├── consent.service.ts # Einwilligungen │ │ │ └── gdpr.service.ts # Export, Löschung │ │ ├── lib/ │ │ │ └── prisma.ts # Prisma mit Audit-Middleware │ │ ├── types/ # TypeScript-Typen │ │ └── index.ts # Server-Einstiegspunkt │ ├── prisma/ │ │ ├── schema.prisma # Datenbank-Schema │ │ └── seed.ts # Seed-Daten │ ├── uploads/ # Hochgeladene Dokumente │ │ ├── bank-cards/ # Bankkarten-Dokumente │ │ ├── documents/ # Ausweis-Scans │ │ ├── invoices/ # Rechnungsdokumente (Strom/Gas) │ │ ├── gdpr/ # DSGVO-Löschnachweise │ │ ├── business-registrations/ # Gewerbeanmeldungen │ │ ├── commercial-registers/ # Handelsregisterauszüge │ │ ├── privacy-policies/ # Datenschutzerklärungen │ │ ├── cancellation-letters/ # Kündigungsschreiben │ │ ├── cancellation-confirmations/ # Kündigungsbestätigungen │ │ └── cancellation-*-options/ # Kündigungsdokumente Optionen │ └── package.json ├── frontend/ │ ├── src/ │ │ ├── components/ # UI-Komponenten │ │ ├── pages/ # Seiten │ │ │ └── settings/ │ │ │ ├── AuditLogs.tsx # Audit-Protokoll │ │ │ └── GDPRDashboard.tsx # DSGVO-Dashboard │ │ ├── hooks/ # Custom Hooks │ │ ├── services/ # API-Client │ │ ├── types/ # TypeScript-Typen │ │ └── App.tsx # Haupt-Komponente │ └── package.json ├── Caddyfile # Optionaler Reverse-Proxy (nur mit --profile caddy) │ ├── Dockerfile # Multi-Stage Build │ ├── docker-compose.yml # Produktion (MariaDB, App, Caddy) │ ├── Caddyfile # Reverse-Proxy mit SSL │ └── entrypoint.sh # Container-Startup ├── docker-compose.yml # MariaDB-Container (Entwicklung) └── README.md ``` ## Berechtigungen | Rolle | Kunden | Verträge | Benutzer | Plattformen | Audit/DSGVO | Developer | |-------|--------|----------|----------|-------------|-------------|-----------| | Admin | CRUD | CRUD | CRUD | CRUD | Vollzugriff | Optional | | Mitarbeiter | CRUD | CRUD | - | Lesen | Lesen | - | | Mitarbeiter (Lesen) | Lesen | Lesen | - | Lesen | - | - | | Kunde | Eigene | Eigene | - | - | - | - | ## Troubleshooting ### Datenbank-Verbindungsfehler 1. Prüfe ob Container läuft: `docker ps` 2. Prüfe die DATABASE_URL in `.env` 3. Warte nach Container-Start ca. 10 Sekunden ### Prisma-Fehler ```bash # Prisma Client neu generieren npx prisma generate # Schema zur Datenbank pushen (ohne Migration) npx prisma db push ``` ### Port bereits belegt - Backend: `PORT` in `.env` ändern - Frontend: In `vite.config.ts` anpassen ### Developer-Menü fehlt ```bash # In der Browser-Console: fetch('/api/developer/setup', { method: 'POST' }).then(r => r.json()).then(console.log) # Danach ausloggen und neu einloggen ``` ## Vertragstypen ### Standard-Vertragstypen Folgende Vertragstypen werden bei Installation/Factory-Reset automatisch angelegt: | Code | Name | Icon | Farbe | |------|------|------|-------| | ELECTRICITY | Strom | Zap | #FFC107 | | GAS | Gas | Flame | #FF5722 | | DSL | DSL | Wifi | #2196F3 | | FIBER | Glasfaser | Cable | #9C27B0 | | CABLE | Kabel Internet (Coax) | Cable | #00BCD4 | | MOBILE | Mobilfunk | Smartphone | #4CAF50 | | TV | TV | Tv | #E91E63 | | CAR_INSURANCE | KFZ-Versicherung | Car | #607D8B | > **Hinweis:** Vertragstypen können nur von Benutzern mit **Entwicklerzugriff** geändert werden, da Änderungen auch Anpassungen an den Formularen erfordern. ### Vertragstyp-spezifische Felder Je nach Vertragstyp werden unterschiedliche Felder im Formular angezeigt: #### Strom & Gas (ELECTRICITY, GAS) - Zähler-Auswahl (gefiltert auf die Lieferadresse des Vertrags) - Jahresverbrauch (kWh/m³) – bei Folgeverträgen mit Schätzwert aus dem Vorvertrag und 1-Klick-Übernahme - Grundpreis, Arbeitspreis (HT/NT bei Zweitarif) - Sofort-Bonus, Neukunden-Bonus - Vorversorger, Kundennummer beim Vorversorger - Folgezähler-Workflow (Zählerwechsel): Wechseldatum + optionaler Endstand des alten Zählers, der automatisch als Zählerstand erfasst wird und in die Verbrauchsberechnung einfließt #### Internet (DSL, CABLE, FIBER) - Download/Upload (Mbit/s) - Router Modell, Seriennummer - Installationsdatum - Benutzername, Passwort - Rufnummern mit SIP-Zugangsdaten | Vertragstyp | Zusatzfeld | |-------------|------------| | **Glasfaser (FIBER)** | Home-ID | #### Mobilfunk (MOBILE) - Datenvolumen (GB) - Inklusiv-Minuten, Inklusiv-SMS - Gerät-Modell, IMEI - Multisim-Checkbox - SIM-Karten (dynamisch erweiterbar): - Rufnummer, SIM-Kartennummer (ICCID) - PIN, PUK (verschlüsselt) - Multisim-Flag, Hauptkarte-Flag - Kartennutzer (optional) – tatsächlicher Nutzer bei Firmen-/ Familienverträgen, kann vom Vertragsinhaber abweichen > **Hinweis Multisim:** Nicht buchbar bei Klarmobil, Congstar, Otelo. Benötigt Freenet oder vergleichbar. #### TV - Receiver Modell - Smartcard-Nummer - Paket/Angebot #### KFZ-Versicherung (CAR_INSURANCE) - Kennzeichen, HSN, TSN, FIN/VIN - Fahrzeugtyp, Erstzulassung - SF-Klasse (Schadenfreiheitsklasse) - Versicherungsart (Haftpflicht/Teilkasko/Vollkasko) - Selbstbeteiligungen (Teilkasko, Vollkasko) - Versicherungsscheinnummer - Vorversicherer ### Standard-Anbieter Folgende Anbieter werden bei Installation/Factory-Reset automatisch angelegt: | Anbieter | Portal-URL | |----------|------------| | Vodafone | https://www.vodafone.de/meinvodafone/account/login | | Klarmobil | https://www.klarmobil.de/login | | Otelo | https://www.otelo.de/mein-otelo/login | | Congstar | https://www.congstar.de/login/ | | Telekom | https://www.telekom.de/kundencenter/startseite | | O2 | https://www.o2online.de/ecare/selfcare | | 1&1 | https://control-center.1und1.de/ | ### Anbieter-spezifische Felder Einige Felder werden nur bei bestimmten Anbietern angezeigt: | Anbieter | Vertragstyp | Zusatzfeld | |----------|-------------|------------| | **Vodafone** | DSL, Kabel Internet | Aktivierungscode | > **Hinweis Multisim:** Bei Klarmobil, Congstar und Otelo ist Multisim **nicht** buchbar. Dafür wird Freenet oder ein vergleichbarer Anbieter benötigt. ## Rechnungsverwaltung (Energieverträge) Für Strom- und Gas-Verträge können Rechnungen verwaltet werden, um den Abrechnungsstatus zu tracken. ### Rechnungstypen | Typ | Beschreibung | |-----|--------------| | **Zwischenrechnung** (INTERIM) | Reguläre Jahresrechnung während der Vertragslaufzeit | | **Schlussrechnung** (FINAL) | Endabrechnung nach Vertragskündigung/Deaktivierung | | **Nicht verfügbar** (NOT_AVAILABLE) | Rechnung ist nicht mehr zu bekommen (z.B. Anbieter existiert nicht mehr) | ### Funktionen - **Rechnungen hinzufügen/bearbeiten/löschen** in der Vertragsdetailansicht - **Dokument-Upload** (PDF) - Pflicht, außer bei Typ "Nicht verfügbar" - **Statusanzeige**: Grünes Badge bei Schlussrechnung, Orange bei fehlender Schlussrechnung - **E-Mail-Anhänge als Rechnung speichern**: Direkt aus dem E-Mail-Client ### E-Mail-Integration Bei E-Mails, die einem Energievertrag zugeordnet sind: 1. Toggle zwischen "Als Dokument" und "Als Rechnung" im Speichern-Dialog 2. Rechnungsdatum und Typ auswählen 3. Anhang wird automatisch als Rechnungsdokument gespeichert ## Vertrags-Cockpit Dashboard zur Überwachung offener Aufgaben und fehlender Dokumente. ### Kategorien | Kategorie | Prüfungen | |-----------|-----------| | **Fehlende Dokumente** | Kündigungsschreiben, Kündigungsbestätigung (wenn Kündigung markiert) | | **Fehlende Rechnungen** | Schluss-/Zwischenrechnungen für Energieverträge | | **Ablaufende Dokumente** | Ausweise, Bankkarten (nächste 30 Tage) | ### Rechnungsprüfung für Energieverträge #### Schlussrechnung (gekündigte/deaktivierte Verträge) | Status | Prüfung | |--------|---------| | **CANCELLED** / **DEACTIVATED** | Schlussrechnung oder "Nicht verfügbar" erforderlich | #### Zwischenrechnung (laufende Verträge) | Bedingung | Warnung | |-----------|---------| | Vertrag > 12 Monate alt, keine Rechnung | "Zwischenrechnung fehlt" | | Letzte Rechnung > 12 Monate her | "Zwischenrechnung überfällig" | > **Hinweis Status-Logik:** > - **EXPIRED** = Laufzeit abgelaufen, aber Vertrag läuft ohne Kündigung weiter → Zwischenrechnung prüfen > - **CANCELLED** = Aktive Kündigung → Schlussrechnung prüfen > - **DEACTIVATED** = Manuell beendet → Schlussrechnung prüfen ### Vertrag zurückstellen (Snooze) Wenn ein Vertrag EXPIRED ist (Laufzeit vorbei, läuft aber weiter), erscheint er im Cockpit. Manchmal ist Bleiben günstiger als Wechseln - aber der Vertrag soll nicht dauerhaft im Cockpit erscheinen. **Lösung:** Vertrag temporär zurückstellen mit Datum für erneute Prüfung. #### Snooze aktivieren Im Cockpit hat jeder Vertrag einen Snooze-Button (Glocke mit Uhr). Optionen: - **+3 Monate** - **+6 Monate** (Empfohlen) - **+12 Monate** - **Eigenes Datum** (Datepicker) #### Unterdrückte Warnungen (bei aktivem Snooze) | Warnung | Beschreibung | |---------|--------------| | Kündigungsfrist | Frist läuft ab | | Vertrag läuft ab | Enddatum naht | | Kündigungsschreiben fehlt | Bei markierter Kündigung | | Kündigungsbestätigung fehlt | Bei markierter Kündigung | #### NICHT unterdrückte Warnungen Diese Warnungen werden auch bei aktivem Snooze angezeigt: | Warnung | Beschreibung | |---------|--------------| | Schlussrechnung fehlt | Beendeter Vertrag ohne Endabrechnung | | Zwischenrechnung fehlt/überfällig | Laufender Vertrag ohne Jahresrechnung | | Zählerstand fehlt | Fehlende Zählerstände bei Energieverträgen | | Zugangsdaten fehlen | Portal-Passwort, SIP-Daten, etc. | #### Snooze aufheben **Im Cockpit:** Bei Verträgen in der Kategorie "Erneute Prüfung fällig" erscheint ein "Snooze aufheben"-Button. **In Vertragsdetails:** Bei zurückgestellten Verträgen erscheint ein bernsteinfarbenes Badge "Zurückgestellt bis [Datum]" mit X-Button zum Aufheben. #### Erneute Prüfung fällig Wenn das Snooze-Datum in der Vergangenheit liegt, erscheint der Vertrag in der neuen Kategorie "Erneute Prüfung fällig" mit Angabe, seit wie vielen Tagen die Prüfung fällig ist. ### Status-Info Modal An verschiedenen Stellen (Vertragsformular, Vertragsdetails, Vertragsübersicht, Kundenansicht) zeigt ein ℹ-Icon neben dem Status eine Erklärung aller Vertragsstatus: | Status | Bedeutung | |--------|-----------| | **Entwurf** | Vertrag wird noch vorbereitet | | **Ausstehend** | Wartet auf Aktivierung | | **Aktiv** | Vertrag läuft normal | | **Abgelaufen** | Laufzeit vorbei, läuft aber ohne Kündigung weiter | | **Gekündigt** | Aktive Kündigung eingereicht, Vertrag endet | | **Deaktiviert** | Manuell beendet/archiviert | ## E-Mail-Client Ein vollständig integrierter E-Mail-Client pro Kunde mit IMAP-Empfang und SMTP-Versand. ### Funktionen #### E-Mails lesen & verwalten - **E-Mail-Tab in Kundenansicht** mit Ordnern: Posteingang, Gesendet, Papierkorb - **Mehrere E-Mail-Konten** pro Kunde (Dropdown zur Auswahl) - **E-Mail-Detailansicht** mit HTML/Text-Body, Absender, Empfänger, CC, Datum - **Gelesen/Ungelesen** markieren - **Favoriten** (Stern) für wichtige E-Mails - **Papierkorb** mit Wiederherstellen und endgültigem Löschen #### E-Mails schreiben - **Neue E-Mail verfassen** mit An, CC, Betreff, Text - **Antworten** mit zitiertem Originaltext - **Dateianhänge** (max. 10 MB pro Datei, 25 MB gesamt) - **SMTP-Versand** über die gewählte StressfreiEmail-Adresse #### Vertragszuordnung - **E-Mails zu Verträgen zuordnen** für bessere Nachverfolgung - **E-Mail-Tab in Vertragsansicht** zeigt nur zugeordnete E-Mails - **Automatische Zuordnung** bei Versand aus Vertragskontext - **Manuelle Zuordnung** über Suchfeld und Vertragsauswahl #### Anhänge - **Anhangsliste** in E-Mail-Detail - **Download** einzelner Anhänge - **Inline-Ansicht** (im Browser öffnen) ### Technische Details #### Backend-Services | Service | Beschreibung | |---------|--------------| | `imapService.ts` | IMAP-Client (ImapFlow) für E-Mail-Empfang | | `smtpService.ts` | SMTP-Client (Nodemailer) für E-Mail-Versand | | `cachedEmail.service.ts` | E-Mail-Caching, Synchronisation, Zuordnung | #### API-Endpunkte ``` GET /api/customers/:id/emails # E-Mails für Kunde GET /api/contracts/:id/emails # E-Mails für Vertrag GET /api/emails/:id # Einzelne E-Mail mit Body POST /api/stressfrei-emails/:id/sync # IMAP-Synchronisation POST /api/stressfrei-emails/:id/send # E-Mail senden POST /api/emails/:id/assign # Vertrag zuordnen DELETE /api/emails/:id/assign # Zuordnung aufheben PATCH /api/emails/:id/read # Gelesen/Ungelesen POST /api/emails/:id/star # Favorit umschalten DELETE /api/emails/:id # In Papierkorb POST /api/emails/:id/restore # Aus Papierkorb wiederherstellen DELETE /api/emails/:id/permanent # Endgültig löschen GET /api/emails/:id/attachments/:filename # Anhang herunterladen # Rechnungen (Invoices) GET /api/invoices/ecd/:ecdId # Rechnungen für EnergyContractDetails POST /api/invoices/ecd/:ecdId # Rechnung hinzufügen PUT /api/invoices/ecd/:ecdId/:invoiceId # Rechnung bearbeiten DELETE /api/invoices/ecd/:ecdId/:invoiceId # Rechnung löschen POST /api/invoices/:invoiceId/upload # Rechnungsdokument hochladen # Cockpit GET /api/contracts/cockpit # Offene Aufgaben abrufen ``` #### Datenbank-Modell ```prisma model CachedEmail { id Int @id @default(autoincrement()) stressfreiEmailId Int stressfreiEmail StressfreiEmail @relation(...) folder String // INBOX, SENT messageId String // RFC 5322 Message-ID uid Int // IMAP UID subject String? fromAddress String fromName String? toAddresses String @db.Text // JSON Array ccAddresses String? @db.Text receivedAt DateTime textBody String? @db.LongText htmlBody String? @db.LongText hasAttachments Boolean @default(false) attachmentNames String? @db.Text // JSON Array contractId Int? // Vertragszuordnung assignedAt DateTime? assignedBy Int? isAutoAssigned Boolean @default(false) isRead Boolean @default(false) isStarred Boolean @default(false) isDeleted Boolean @default(false) deletedAt DateTime? @@unique([stressfreiEmailId, messageId, folder]) } ``` #### Sicherheit - **Passwort-Verschlüsselung**: AES-256-GCM für Mailbox-Passwörter - **Passwort-Reset**: Neues Passwort generieren und beim Provider setzen - **Verschlüsselungsmodi**: SSL, STARTTLS, oder unverschlüsselt - **Selbstsignierte Zertifikate**: Konfigurierbar pro Provider #### Berechtigungen | Aktion | Berechtigung | |--------|--------------| | E-Mails lesen | `customers:read` | | E-Mails senden, markieren | `customers:update` | | Anhänge in Dokumente speichern | `customers:update` | | Vertrag zuordnen | `contracts:update` | | Löschen, Papierkorb | `emails:delete` | ### Frontend-Komponenten | Komponente | Beschreibung | |------------|--------------| | `EmailClientTab.tsx` | Haupt-Tab mit Konto-Auswahl und Ordnern | | `EmailList.tsx` | E-Mail-Liste mit Aktionen | | `EmailDetail.tsx` | E-Mail-Ansicht mit Anhängen | | `ComposeEmailModal.tsx` | Neue E-Mail / Antworten | | `TrashEmailList.tsx` | Papierkorb-Verwaltung | | `AssignToContractModal.tsx` | Vertragszuordnung | | `ContractEmailsSection.tsx` | E-Mails in Vertragsansicht | | `SaveAttachmentModal.tsx` | Anhänge in Dokumentfelder speichern | | `SaveEmailAsPdfModal.tsx` | E-Mail als PDF in Dokumentfelder speichern | | `InvoicesSection.tsx` | Rechnungsverwaltung in Vertragsdetails | ### Anhänge als Dokumente speichern E-Mail-Anhänge können direkt in Dokumentfelder des CRM gespeichert werden. Über den blauen Speichern-Button (💾) neben jedem Anhang öffnet sich ein Modal mit allen verfügbaren Zielen. #### Verfügbare Ziele | Kategorie | Dokumentfelder | |-----------|----------------| | **Kunde** | Datenschutzerklärung | | **Kunde (Gewerbe)** | + Gewerbeanmeldung, Handelsregisterauszug | | **Ausweisdokumente** | Dokumentscan (pro Ausweis) | | **Bankkarten** | Kartenscan (pro Karte) | | **Vertrag** | Kündigungsschreiben, Kündigungsbestätigung, Kündigungsschreiben (Optionen), Kündigungsbestätigung (Optionen) | > **Hinweis:** Vertragsdokumente sind nur verfügbar, wenn die E-Mail einem Vertrag zugeordnet ist. #### Dynamische Konfiguration Die Dokumentziele werden zentral in `backend/src/config/documentTargets.config.ts` konfiguriert. Neue Dokumentfelder werden automatisch im Modal angezeigt, ohne Frontend-Änderungen. ```typescript // Beispiel: Neues Feld hinzufügen { key: 'newDocument', label: 'Neues Dokument', field: 'newDocumentPath', // Prisma-Feld condition: null, // oder 'BUSINESS' für Geschäftskunden directory: 'new-documents' // Upload-Verzeichnis } ``` #### Warnung bei Überschreiben Wenn bereits ein Dokument im Zielfeld vorhanden ist, wird eine Warnung angezeigt. Das vorhandene Dokument wird beim Speichern automatisch ersetzt und die alte Datei gelöscht. ### Vertragszuordnung aufheben (X-Button) Der X-Button zum Aufheben der Vertragszuordnung erscheint nur bei **manuell zugeordneten** E-Mails. E-Mails, die direkt aus dem Vertragskontext gesendet wurden (`isAutoAssigned = true`), bleiben dauerhaft mit dem Vertrag verknüpft. | Ansicht | Vertrags-Badge | X-Button sichtbar | |---------|----------------|-------------------| | **Kundenakte - Posteingang** | ✅ | ✅ immer | | **Kundenakte - Gesendet** | ✅ | nur manuell zugeordnet | | **Kundenakte - Papierkorb** | ✅ | je nach Original-Ordner | | **Vertrag - Posteingang** | ✅ | ✅ immer | | **Vertrag - Gesendet** | ✅ | nur manuell zugeordnet | | **Vertrag - Papierkorb** | ✅ | je nach Original-Ordner | | **Detail-Ansicht (beide)** | ✅ | nur manuell zugeordnet | > **Hinweis:** Bei gesendeten E-Mails gilt: > - `isAutoAssigned = true`: E-Mail wurde direkt aus dem Vertragskontext gesendet → X-Button ausgeblendet > - `isAutoAssigned = false`: E-Mail wurde manuell dem Vertrag zugeordnet → X-Button sichtbar ### Vertragsbaum in Kundenansicht In der Kundendetailansicht werden Verträge als **Baumstruktur** mit Vorgänger-Verknüpfung dargestellt: ``` ▼ GAS-ML781A4FYXU │ Gas │ ACTIVE │ 01.01.2025 - 31.12.2026 └─ GAS-ML24GKR...│ Gas │ EXPIRED│ 05.05.2023 - 05.05.2025 (Vorgänger) └─ GAS-OLD123 │ Gas │ EXPIRED│ 01.01.2021 - 04.05.2023 (Vorgänger) ▶ MOB-ML77W560A73 │ Mobil│ DRAFT │ 02.01.2024 - 02.01.2026 ``` **Funktionsweise:** - **Aktuellste Verträge oben** - Verträge ohne Nachfolger werden als Wurzelknoten angezeigt - **Standardmäßig eingeklappt** - Klick auf ▶ zeigt die Vorgängerkette - **Vorgänger eingerückt** - Mit grauem Rand und "(Vorgänger)" Label - **Verknüpfung über `previousContractId`** - Wird beim Erstellen eines Folgevertrags automatisch gesetzt > **Hinweis:** In der Hauptvertragsliste (`/contracts`) wird weiterhin die flache Ansicht ohne Baumstruktur verwendet. ## DSGVO-Compliance & Audit-Logging Umfassendes Audit-Logging-System mit DSGVO-Compliance-Features. ### Audit-Protokoll Automatische Protokollierung aller API-Zugriffe mit: | Feld | Beschreibung | |------|--------------| | **Benutzer** | User-ID, E-Mail, Rolle | | **Aktion** | CREATE, READ, UPDATE, DELETE, EXPORT, ANONYMIZE, LOGIN, LOGOUT | | **Ressource** | Tabelle + ID + lesbare Bezeichnung | | **Kontext** | Endpoint, HTTP-Methode, IP-Adresse, User-Agent | | **Änderungen** | Vorher/Nachher-Werte (bei Updates) | | **Sensitivität** | LOW, MEDIUM, HIGH, CRITICAL | | **Integrität** | SHA-256 Hash-Kette für Manipulationsschutz | #### Sensitivitätsstufen | Stufe | Ressourcen | |-------|------------| | **LOW** | Einstellungen, Plattformen, Tarife | | **MEDIUM** | Verträge, Provider | | **HIGH** | Kundendaten, Benutzerdaten | | **CRITICAL** | Authentifizierung, Bankdaten, Ausweisdokumente | #### Zugriff - **Einstellungen** → **Audit-Protokoll** - Filter nach: Datum, Benutzer, Aktion, Ressource, Sensitivität - Detail-Ansicht mit Vorher/Nachher-Diff - Export als JSON - Integritätsprüfung (Hash-Kette verifizieren) ### DSGVO-Dashboard Zentrale Verwaltung für DSGVO-Anfragen unter **Einstellungen** → **DSGVO-Dashboard**. #### Dashboard-Statistiken - Offene Löschanfragen - Abgeschlossene Löschungen (letzte 30 Tage) - Datenexporte (letzte 30 Tage) - Aktive Einwilligungen #### Einwilligungsverwaltung (Consents) | Consent-Typ | Beschreibung | |-------------|--------------| | **DATA_PROCESSING** | Grundlegende Datenverarbeitung | | **MARKETING_EMAIL** | E-Mail-Marketing | | **MARKETING_PHONE** | Telefonmarketing | | **DATA_SHARING_PARTNER** | Datenweitergabe an Partner | Einwilligungen können pro Kunde im Tab "Einwilligungen" verwaltet werden. #### Löschanfragen (Art. 17) Workflow für DSGVO-Löschanfragen: | Status | Beschreibung | |--------|--------------| | **PENDING** | Anfrage eingegangen | | **IN_PROGRESS** | Wird bearbeitet | | **COMPLETED** | Vollständig gelöscht/anonymisiert | | **PARTIALLY_COMPLETED** | Teildaten behalten (z.B. aktive Verträge) | | **REJECTED** | Abgelehnt mit Begründung | **Anonymisierung statt Löschung:** - Kundendaten werden anonymisiert (nicht gelöscht) - Aktive Verträge werden beibehalten - PDF-Löschnachweis wird generiert #### Datenexport (Art. 15) Kunden können alle gespeicherten Daten als JSON exportieren: - Stammdaten - Adressen, Bankdaten, Ausweise - Verträge mit Details - Zähler und Ablesungen - Einwilligungen - Zugriffsprotokolle ### API-Endpunkte ``` # Audit-Logs GET /api/audit-logs # Logs mit Filtern GET /api/audit-logs/:id # Einzelnes Log GET /api/audit-logs/customer/:id # Logs für Kunde GET /api/audit-logs/export # Export (JSON/CSV) POST /api/audit-logs/verify # Hash-Kette prüfen GET /api/audit-logs/retention-policies # Aufbewahrungsfristen PUT /api/audit-logs/retention-policies/:id POST /api/audit-logs/cleanup # Manuelle Bereinigung # DSGVO GET /api/gdpr/dashboard # Dashboard-Statistiken GET /api/gdpr/customer/:id/export # Kundendaten-Export GET /api/gdpr/deletions # Löschanfragen POST /api/gdpr/deletions # Löschanfrage erstellen PUT /api/gdpr/deletions/:id/process # Löschanfrage bearbeiten GET /api/gdpr/customer/:id/consents # Einwilligungen abrufen PUT /api/gdpr/customer/:id/consents/:type # Einwilligung ändern GET /api/gdpr/consents/overview # Consent-Übersicht ``` ### Aufbewahrungsfristen | Ressource | Frist | Rechtsgrundlage | |-----------|-------|-----------------| | Standard | 10 Jahre | AO §147, HGB §257 | | Authentifizierung | 2 Jahre | Sicherheit | | Kundendaten (HIGH) | 10 Jahre | Steuerrecht | | Verträge | 10 Jahre | Steuerrecht | | Allgemein (LOW) | 3 Jahre | Verjährung | ### Berechtigungen | Aktion | Berechtigung | |--------|--------------| | Audit-Logs lesen | `audit:read` | | Audit-Logs exportieren | `audit:export` | | Audit-Administration | `audit:admin` | | DSGVO-Export | `gdpr:export` | | Löschanfrage erstellen | `gdpr:delete` | | DSGVO-Administration | `gdpr:admin` | ### Technische Details #### Hash-Kette Jeder Audit-Log-Eintrag enthält einen SHA-256-Hash über: - Alle Felder des Eintrags - Hash des vorherigen Eintrags Dies ermöglicht die Erkennung von Manipulationen. #### Sensitive Daten Folgende Felder werden in Audit-Logs gefiltert: - `password`, `passwordHash` - `portalPasswordHash`, `portalPasswordEncrypted` - `emailPasswordEncrypted`, `internetPasswordEncrypted` - `sipPasswordEncrypted`, `pin`, `puk`, `apiKey` #### Performance - Logging erfolgt asynchron (`setImmediate`) - API-Response wird nicht blockiert - Before/After-Werte über Prisma Middleware ## Factory-Defaults: Stammdaten-Kataloge teilen Das **Factory-Defaults**-System erlaubt den Export und Import von Stammdaten-Katalogen (Anbieter, Tarife, PDF-Auftragsvorlagen, HTML-Standardtexte) zwischen verschiedenen OpenCRM-Installationen. Es ist bewusst **streng abgegrenzt** zu Datenbank-Backups: ### Abgrenzung | | Factory-Defaults | Datenbank-Backup | |---|---|---| | Anbieter, Tarife, Kündigungsfristen, Laufzeiten, Kategorien | ✅ | ✅ | | PDF-Auftragsvorlagen (inkl. Dateien + Feldzuordnungen) | ✅ | ✅ | | HTML-Standardtexte: Datenschutzerklärung, Impressum, Vollmacht-Vorlage, Website-Datenschutz | ✅ | ✅ | | **Kundendaten, Verträge, Dokumente** | ❌ | ✅ | | **Emails, SMTP-/IMAP-Zugangsdaten** | ❌ | ✅ | | **Secrets, JWT, Encryption-Keys, User-Accounts** | ❌ | ✅ | | Zwischen verschiedenen Installationen teilbar | ✅ | ❌ (zu firmen-spezifisch) | > **Kurz:** Factory-Defaults = generische Stammdaten + rechtliche Standardtexte, > Backup = die komplette Instanz. ### Drei Wege, eine ZIP zu transportieren Es gibt drei Pfade, je nachdem wo die ZIP gerade liegen soll: | Wo | Pfad | Wann | |---|---|---| | **Laufende DB einer Instanz** | UI-Upload oder `./factory-import.sh` | Bestehende Live-Instanz updaten | | **Drop-Box im Repo** (`factory-exports/`) | `./factory-export.sh` legt ab, `./factory-import.sh` liest | Transfer zwischen dev und prod via `scp` | | **Werkseinstellung im Image** (`backend/factory-defaults/`) | `./factory-import.sh --save-as-builtin` oder manuell entpacken | Neue VMs sollen die Defaults beim allerersten Start mitbringen | Alle drei sind unabhängig, **alle drei zusammen** decken den typischen Workflow ab. ### Export **Variante A – UI:** 1. **Einstellungen** → **Factory-Defaults** öffnen 2. Button **„Factory-Defaults exportieren"** klicken 3. ZIP wird als `factory-defaults-YYYY-MM-DD.zip` heruntergeladen **Variante B – CLI (für scp-Transfers):** ```bash ./factory-export.sh # → factory-exports/factory-defaults-…zip OPENCRM_URL=https://crm.prod.example.de \ OPENCRM_EMAIL=admin@example.de ./factory-export.sh # gegen Prod-Instanz ``` Ohne `OPENCRM_PASSWORD` wird das Passwort interaktiv abgefragt. Der Zielordner `factory-exports/` ist gitignored – die ZIPs landen also nicht ins Repo. **ZIP-Struktur:** ``` factory-defaults-2026-05-07-1949.zip ├── manifest.json # Version + Datum + Counts ├── providers/providers.json ├── contract-meta/ │ ├── cancellation-periods.json │ ├── contract-durations.json │ └── contract-categories.json ├── pdf-templates/ │ ├── pdf-templates.json │ └── *.pdf # Die eigentlichen PDF-Dateien └── app-settings/ └── app-settings.json # HTML-Templates (Whitelist-only) ``` ### Import **Variante A – UI:** 1. **Einstellungen** → **Factory-Defaults** 2. Bereich **Import** → **„ZIP hochladen"** → Datei wählen 3. Erfolgs-Box zeigt Counts pro Kategorie **Variante B – CLI:** ```bash ./factory-import.sh # nimmt jüngste ZIP aus factory-exports/ ./factory-import.sh ./factory-exports/foo.zip # explizite ZIP ./factory-import.sh --save-as-builtin # zusätzlich ins Image-Default ./factory-import.sh --save-as-builtin ./foo.zip # entpacken (siehe unten) ``` Konfigurierbar per ENV: `OPENCRM_URL`, `OPENCRM_EMAIL`, `OPENCRM_PASSWORD`. **Variante C – Container-Bare-Metal (für Migration / mehrere ZIPs zusammenführen):** ```bash # Inhalt der ZIP nach backend/factory-defaults/ entpacken (Unterordner beibehalten) cd backend && npm run seed:defaults ``` **Beispiel-Output:** ``` ✓ Anbieter: 10 ✓ Tarife: 4 ✓ Kündigungsfristen: 18 ✓ Laufzeiten: 18 ✓ Vertragskategorien: 8 ✓ PDF-Vorlagen: 2 ✓ HTML-Templates: 2 ``` ### `--save-as-builtin`: ZIP zur Werkseinstellung machen Mit `--save-as-builtin` entpackt `factory-import.sh` die ZIP nach **erfolgreichem DB-Import** zusätzlich in `backend/factory-defaults/`. Beim nächsten `docker-compose up --build` landen die Defaults im Image. Frisch hochgezogene VMs bringen sie dann beim ersten Start automatisch mit (Auto-Seed-Pfad im Container-Entrypoint). ```bash # typischer Sync prod → dev → Image-Default ssh prod './factory-export.sh' scp prod:opencrm/factory-exports/factory-defaults-*.zip factory-exports/ ./factory-import.sh --save-as-builtin docker-compose up -d --build # neuer Build, neue VMs starten direkt mit Defaults ``` Der Inhalt von `backend/factory-defaults/` wird beim `--save-as-builtin` vorher geleert (außer `README.md` und `.gitkeep`), damit nichts Veraltetes liegen bleibt. ### Mehrere ZIPs kombinieren (CLI-only, Variante C) `backend/factory-defaults/` darf mehrere `*.json` pro Unterordner haben – `npm run seed:defaults` merged sie automatisch: ``` backend/factory-defaults/ providers/ verivox.json # 40 Anbieter aus Verivox-Paket check24.json # 30 Anbieter aus Check24-Paket eigene.json # 5 eigene Anbieter ``` Bei gleichem Unique-Key gewinnt der zuletzt gelesene Eintrag. Der UI-/HTTP-Import nimmt nur eine ZIP entgegen – für Merges nutze `npm run seed:defaults`. ### Idempotenz Alle Pfade nutzen Prisma `upsert`: - **Neue Einträge** werden angelegt - **Bestehende Einträge** (per unique Key: `name` / `code` / `key`) werden aktualisiert - Nichts wird gelöscht Du kannst Imports also beliebig oft hintereinander ausführen, ohne Datenverlust oder Duplikate. ### PDF-Dateien Beim Import werden PDF-Vorlagen aus dem ZIP nach `uploads/pdf-templates/` kopiert (mit eindeutigem Suffix) und die `templatePath`-Spalte entsprechend gesetzt. Beim Re-Import wird die alte Datei in `uploads/` entsorgt und durch die neue ersetzt. ### AppSettings-Whitelist Beim Import werden nur die Keys mit AppSetting-Schreibzugriff gewährt, die auch exportiert werden – aktuell: - `privacyPolicyHtml` - `imprintHtml` - `authorizationTemplateHtml` - `websitePrivacyPolicyHtml` Andere Keys (SMTP, JWT, etc.) werden mit einer Warnung ignoriert. Whitelist ist in [`backend/src/services/factoryDefaults.service.ts`](backend/src/services/factoryDefaults.service.ts) zentral gepflegt. ### Auto-Seed beim Erst-Deploy Bei einer **frischen** Installation (leere DB) spielt der Container-Entrypoint nach dem Prisma-Seed automatisch das Built-in-Verzeichnis ein: ``` [entrypoint] DB ist leer (User-Count=0) – Auto-Seed wird ausgeführt [entrypoint] Spiele eingebaute Factory-Defaults ein… ✓ Anbieter: 10, Tarife: 4 … ``` Bei bestehenden Installs passiert das **nicht** – nur frische DBs. ### Berechtigungen | Aktion | Berechtigung | |--------|--------------| | Factory-Defaults Vorschau | `settings:read` | | Factory-Defaults Export (UI/CLI) | `settings:update` | | Factory-Defaults Import (UI/CLI) | `settings:update` | | Werkseinstellungen ändern (`--save-as-builtin` / `npm run seed:defaults`) | Server-Zugang (SSH/Shell) | ### Typische Einsatzzwecke - **Neue VM aufsetzen**: ZIP einmalig nach `backend/factory-defaults/` entpacken (oder per `--save-as-builtin`), dann `docker-compose up --build` – die Werkseinstellungen sind beim ersten Start automatisch drin. - **Prod-Stand zurück nach dev synchronisieren**: `./factory-export.sh` auf prod, `scp` ins dev, `./factory-import.sh --save-as-builtin` lokal – damit ist sowohl die dev-DB aktuell als auch der nächste Image-Build. - **Vorlagen-Paket teilen**: Eine ZIP mit nur PDF-Vorlagen weitergeben (andere Ordner aus der ZIP entfernen vor dem Entpacken). - **Anbieter-Paket teilen**: ZIP mit nur `providers/` weitergeben - **Versionskontrolle**: Die entpackten JSON-Dateien unter Versionskontrolle stellen (außerhalb von `backend/factory-defaults/`, da der Ordner gitignored ist) ## Changelog ### 1.1.0 (2026-05-01) **Production-readiness** – die Version, die wirklich öffentlich gehen darf. - 🛡 **Security-Hardening**: 10 Runden statisches + dynamisches Audit, vollständig dokumentiert in [docs/SECURITY-HARDENING.md](docs/SECURITY-HARDENING.md) (CORS/Helmet/JWT, IDOR-Schutz an 30+ Endpoints, Mass-Assignment-Whitelists, Zip-Slip, Path-Traversal, Login-Timing-Side-Channel, XFF-Rate-Limit-Bypass, Customer-Liste-Leak, SSRF + DNS-Rebinding, Per-File-Ownership statt freiem `/api/uploads`, JWT-Logout, Audit-Log-Hash-Chain). - 🚨 **Sicherheits-Monitoring**: neue `SecurityEvent`-Tabelle + Hooks an Login, Logout, Rate-Limit-Hit, IDOR-Abwehr, SSRF-Block, Password-Reset, JWT-Reject. Threshold-Detection (Brute-Force, IDOR-Probing, SSRF-Probing) erzeugt CRITICAL-Events. **Sofort-E-Mail-Alerts** für CRITICAL + **stündlicher Digest** für HIGH/MEDIUM. UI in Einstellungen → Monitoring mit Filter, Pagination, Log-leeren (mit optionalem Tage-Filter) und Test-Alert-Button. - 🔄 **Auto-Vertragsstatus**: - Lieferbestätigung-Upload → `DRAFT` → `ACTIVE` + `startDate` - Kündigungsbestätigung-Upload → `ACTIVE` → `CANCELLED` + `cancellationConfirmationDate` (mit Datums-Modal beim Upload) - Nightly-Cron 02:00: alle `ACTIVE`-Verträge mit `endDate < heute` → `EXPIRED` - 🔐 **Lazy bcrypt-Rehash**: Bestandshashes mit Cost 10 werden beim nächsten Login transparent auf Cost 12 geupgradet. - 🚪 **Logout-Endpoint** `POST /api/auth/logout`: invalidiert JWTs serverseitig über `tokenInvalidatedAt`. - 📦 **`npm audit fix`**: 8 transitive Vulnerabilities gefixt (lodash, path-to-regexp, undici, minimatch). ### 1.0.0 Erste Release-Version. - Kunden-, Vertrags-, Adress-, Bankkarten-, Ausweis- und Zählerverwaltung - Energie-/Telekommunikations-/KFZ-Verträge mit typspezifischen Details - Vertrags-Cockpit mit Rechnungsprüfung - E-Mail-Client mit Anhang-Verwaltung - DSGVO-Compliance: Audit-Log, Einwilligungen, Datenexport, Löschanfragen - PDF-Auftragsvorlagen-System mit visueller Feldzuordnung - Factory-Defaults für Stammdaten-Kataloge - Mandantenfähigkeit über `customerEmailLabel` pro Provider - Passwort-Reset-Flow + Rate-Limiting + Auto-Geburtstagsgrüße ## Lizenz MIT