POST /audit-logs/rehash rechnet die Kette mit dem HMAC-Schluessel neu und macht
sie damit wieder stimmig - auch wenn sie vorher berechtigte Beanstandungen
enthielt. Der Anker schuetzt gegen einen DB-Schreiber ohne Schluessel, nicht
gegen einen Admin mit audit:admin. Der bisherige Marker hielt nur fest, DASS
rehasht wurde, nicht WAS dabei verschwand.
Fix 1: Vor dem Rehash wird verifyIntegrity() erhoben und samt Kettenkopf im
Marker gesichert - Anzahl geprueft, Listen der manipulierten Zeilen, der
Ketten-Luecken, der Luecken ohne dokumentierte Loeschung, der nicht pruefbaren.
Dazu ausloesender Benutzer und IP statt pauschal "system". Der Marker entsteht
nach dem Rehash, ist Teil der neuen Kette und signiert.
Fix 2: rehash verlangt {"confirm":"REHASH"}, cleanup verlangt
{"confirm":"CLEANUP"}. Beide wurden bei blinder Methoden-Erkundung per POST
unbeabsichtigt ausgeloest; ein tastender Aufruf laeuft jetzt in 400.
Verifiziert: blinder POST -> 400 ohne Wirkung; mit Bestaetigung laeuft der
Rehash und der Marker enthaelt Ausloeser, Vorbefund (1 manipuliert, 7 Luecken
mit exakten IDs) und Kettenkopf.
Hinweis: Der Test hat auf der DEV-Datenbank real rehasht, die dortigen
historischen Beanstandungen sind damit geglaettet. Staging/Prod unberuehrt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
OpenCRM
Web-basiertes CRM-System für Kundenverwaltung mit Verträgen (Energie, Telekommunikation, KFZ-Versicherung).
Version: 1.1.0 (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 setztACTIVE→CANCELLED(mit Datum), nightly-Cron setztACTIVE-Verträge mit abgelaufenemendDateaufEXPIRED - 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
- 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/expresszur 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:
git clone <repository-url>
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=truein der.envsetzen,docker compose up -dausführen, dann wieder auffalse.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: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.enveinträ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
git clone <repository-url>
cd opencrm
cp .env.example .env # Konfiguration anpassen
2. MariaDB-Container starten
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
cd backend
# Dependencies installieren
npm install
# .env-Datei erstellen (falls noch nicht vorhanden)
cp .env.example .env
Die .env-Datei sollte folgende Werte enthalten:
# 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
# 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
cd ../frontend
# Dependencies installieren
npm install
Anwendung starten
Backend starten (Terminal 1)
cd backend
npm run dev
Das Backend läuft auf http://localhost:3001
Frontend starten (Terminal 2)
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:
Beispiel-Ausgabe:
docker logs opencrm-app 2>&1 | grep -A 5 "Initial-Passwort"Wer das Log nicht mehr findet (z.B. Container schon länger gerollt), setzt======================================================== Admin-User: admin@admin.com Initial-Passwort: <28-zeichen-zufalls-passwort> ⚠️ Dieses Passwort wird hier EINMAL ausgegeben! Bitte sofort nach dem ersten Login ändern. ========================================================SEED_ADMIN_PASSWORD=<min. 25 Zeichen>in der.envund startet einmalig mitRUN_SEED=trueneu – der Seed ist idempotent und überschreibt das Admin-Passwort. DanachRUN_SEEDwieder auffalse.
Tipp: Wer von Anfang an ein eigenes Admin-Passwort haben möchte, setzt
SEED_ADMIN_PASSWORD=<min. 25 Zeichen>bevor der erstedocker compose up -dläuft. Der Wert wird dann statt des Zufalls- passworts verwendet (siehe.env.example).
Wichtig: Vor dem ersten Production-Deployment Secrets rotieren – siehe 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 upsertet 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:
# 1) In der .env / docker-compose environment setzen:
SEED_ADMIN_PASSWORD=<ein dir bekanntes Passwort, MIND. 25 Zeichen>
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 / <SEED_ADMIN_PASSWORD>
# 4) Danach RUN_SEED wieder auf false (sonst re-seedet jeder Restart – harmlos,
# aber unnötig). SEED_ADMIN_PASSWORD kann gesetzt bleiben.
⚠️
SEED_ADMIN_PASSWORDmuss ≥ 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:
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:
# 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
# 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:
- Schlüssel erzeugen (
openssl rand -hex 32), in die.enveintragen - Backend neu starten
- 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
.envverlorenging), 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 (<hier-eigenen-wert-eintragen>) 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:
AUDIT_HMAC_KEY=<neuer Schlüssel>
AUDIT_HMAC_KEY_OLD=<bisheriger Schlüssel>
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_OLDnicht 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:
AUDIT_HMAC_KEY=<aktueller Schlüssel>
AUDIT_HMAC_KEY_OLD=<vorheriger>,<davor>,<ganz alter>
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:
curl -X POST https://crm.deine-domain.de/api/audit-logs/verify \
-H "Authorization: Bearer <Access-Token>"
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-Forhart 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.devs.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) im Profil überschreiben – oder gleich
per
SEED_ADMIN_PASSWORDin der.envvor dem ersten Start setzen. - Manuelle Test-Checkliste aus 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
⚠️ 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):
- Proxy-Hosts → den CRM-Host → Edit
- Tab Custom Locations → „Add location"
- Define location:
/api/ - Scheme:
http, Forward Hostname/IP: wie im Haupt-Host (z.B.172.0.2.39), Forward Port:3010 - Zahnrad rechts an der Location → erweiterte Config eintragen:
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; - Save (Location), Save (Proxy-Host)
Der
more_clear_headers-Befehl kommt aus demheaders-more-Modul, das bei NPM standardmäßig dabei ist. Damit verschwinden die BannerServer: openrestyundx-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):
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:
# 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 statischeindex.htmlaus 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:
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:
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:
# Root: kein Content-Encoding mehr
curl -sI -H 'Accept-Encoding: gzip' https://kundencenter.deine-domain.de/ \
| grep -i content-encoding
# /assets/<file>.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:
- Einmalig im Browser-Console ausführen:
fetch('/api/developer/setup', { method: 'POST' }) - 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
- Einstellungen → Email-Provisionierung öffnen
- 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)
- Provider als "Standard" und "Aktiv" markieren
- 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)
- In Plesk als Admin einloggen
- Oben rechts auf den eigenen Namen → "Mein Profil" (oder direkt URL
/admin/my-profile/) - Tab "API-Token" oder "API-Schlüssel" öffnen
- "API-Schlüssel erstellen" (bzw. "Add API Key")
- Beschreibung vergeben (z.B. "OpenCRM")
- Den angezeigten Schlüssel sofort kopieren – er wird nur einmal angezeigt!
- 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:
# 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 <IP-DES-CRM-SERVERS> -description "OpenCRM"
Achtung:
-ip-address 0.0.0.0funktioniert 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:
plesk bin secret_key --list
API-Key löschen:
plesk bin secret_key --delete <KEY>
Plesk: REST API aktivieren (falls nicht vorhanden)
Bei älteren Plesk-Versionen oder Custom-Installationen kann es sein, dass die REST API fehlt. Dann:
- Tools & Einstellungen → Updates → Erweiterungen hinzufügen/entfernen
- Nach "REST API" suchen und installieren
- 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:
- Tools & Einstellungen → Firewall
- "Plesk-Dienst – Panel" (Port 8443) für die IP des CRM-Servers erlauben
Bei reiner Linux-Firewall (ufw/firewalld):
# Beispiel ufw
ufw allow from <CRM-SERVER-IP> 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:
- Geprüft, ob die E-Mail-Adresse bereits existiert
- Falls nicht: E-Mail-Adresse beim Provider angelegt mit Weiterleitung an:
- Kunden-E-Mail-Adresse
- Standard-Weiterleitungsadresse (falls konfiguriert)
Befehle
Backend
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
npm run dev # Entwicklungsserver starten
npm run build # Produktions-Build erstellen
npm run preview # Build-Vorschau
Docker (Entwicklung)
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:
COMPOSE_PROFILES=caddy # leer = kein Caddy (Standard)
DOMAIN=crm.example.de
HTTPS_ENABLED=true
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=truesetzen – das aktiviert dasSecure-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.envauf 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
- Prüfe ob Container läuft:
docker ps - Prüfe die DATABASE_URL in
.env - Warte nach Container-Start ca. 10 Sekunden
Prisma-Fehler
# Prisma Client neu generieren
npx prisma generate
# Schema zur Datenbank pushen (ohne Migration)
npx prisma db push
Port bereits belegt
- Backend:
PORTin.envändern - Frontend: In
vite.config.tsanpassen
Developer-Menü fehlt
# 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:
- Toggle zwischen "Als Dokument" und "Als Rechnung" im Speichern-Dialog
- Rechnungsdatum und Typ auswählen
- 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
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.
// 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 ausgeblendetisAutoAssigned = 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,passwordHashportalPasswordHash,portalPasswordEncryptedemailPasswordEncrypted,internetPasswordEncryptedsipPasswordEncrypted,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:
- Einstellungen → Factory-Defaults öffnen
- Button „Factory-Defaults exportieren" klicken
- ZIP wird als
factory-defaults-YYYY-MM-DD.zipheruntergeladen
Variante B – CLI (für scp-Transfers):
./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:
- Einstellungen → Factory-Defaults
- Bereich Import → „ZIP hochladen" → Datei wählen
- Erfolgs-Box zeigt Counts pro Kategorie
Variante B – CLI:
./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):
# 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).
# 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:
privacyPolicyHtmlimprintHtmlauthorizationTemplateHtmlwebsitePrivacyPolicyHtml
Andere Keys (SMTP, JWT, etc.) werden mit einer Warnung ignoriert. Whitelist ist
in 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), danndocker-compose up --build– die Werkseinstellungen sind beim ersten Start automatisch drin. - Prod-Stand zurück nach dev synchronisieren:
./factory-export.shauf prod,scpins dev,./factory-import.sh --save-as-builtinlokal – 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
(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 mitendDate < heute→EXPIRED
- Lieferbestätigung-Upload →
- 🔐 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 übertokenInvalidatedAt. - 📦
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
customerEmailLabelpro Provider - Passwort-Reset-Flow + Rate-Limiting + Auto-Geburtstagsgrüße
Lizenz
MIT