duffyduckandClaude Opus 5 b6b6f7c0a7 Gegenbuch: Rewind-Waechter statt Probe-Push, Tor vor dem Anhaengen (R179 a/b)
(a) Probe-Push verworfen. Er haette nur den geprobten Ref beurteilt, die eigene
Push-Identitaet gemessen statt die des Angreifers (Bypass-Rechte fuer Admins
gehen genau dann auseinander, wenn es zaehlt), nur einen Zeitpunkt abgedeckt -
und einen zerstoerungsfreien Force-Push gibt es nicht: die bestaetigende
Beobachtung waere derselbe Vorgang wie der Schaden.

Stattdessen ein Fast-Forward-Waechter: Der beobachtete Remote-Kopf wird
ausserhalb des Klons festgehalten; beim naechsten Lauf muss der neue Kopf ein
Nachfahre des alten sein. Das erkennt das Ereignis statt die Regel abzufragen
und wirkt unabhaengig von serverseitigem Schutz. Ein belegter Fast-Forward
gilt als Nachweis und blendet den Rewind-Vorbehalt aus.

(b) Code 3 als Tor vor dem Anhaengen statt als Status danach: Der Schreiblauf
signiert mit dem neuen Checkpoint zugleich ueber den Bestand darunter - ist die
Basis ungeklaert, waere das Anhaengen selbst das Waschmittel. Grundlage nicht
feststellbar -> nichts anhaengen, exit 3. Erster Lauf -> Basislinie, ehrlich
gemeldet, exit 3. Anhaengen geklappt, Push gescheitert -> exit 3 mit "erstellt,
aber NICHT verankert". Nur Anhaengen + Push + belegte Verankerung -> exit 0.

Verifiziert: Basislinie exit 3; Folgelauf exit 0 ohne Vorbehalt; Rewind aus
einem frischen Auditoren-Klon ohne MIN_SEQ und ohne Zusicherung -> Alarm exit 2
(bisher stilles Gruen); kaputtes Push-Ziel -> "erstellt, aber nicht verankert"
exit 3, Folgelauf haelt den ungepushten Commit fail-closed an.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 22:29:20 +02:00
2026-03-25 15:25:34 +01:00

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 DRAFTACTIVE (mit Vertragsbeginn), Kündigungsbestätigung-Upload setzt ACTIVECANCELLED (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
  • 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:

git clone <repository-url>
cd opencrm
cp .env.example .env       # Werte anpassen, Secrets rotieren!
docker compose up -d

Browser:

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:

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

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:
    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=<min. 25 Zeichen> 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=<min. 25 Zeichen> 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.

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_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:

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:

  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 (<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_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:

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-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) im Profil überschreiben oder gleich per SEED_ADMIN_PASSWORD in der .env vor 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):

  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:
    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):

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 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:

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:

  1. Einmalig im Browser-Console ausführen:
    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. EinstellungenEmail-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 & EinstellungenAPI-Schlüssel oder Werkzeuge & EinstellungenAPI-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.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:

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:

  1. Tools & EinstellungenUpdatesErweiterungen 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 & EinstellungenFirewall
  2. "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:

  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

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=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

# 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

# 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

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 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

  • EinstellungenAudit-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 EinstellungenDSGVO-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. EinstellungenFactory-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):

./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. EinstellungenFactory-Defaults
  2. Bereich Import„ZIP hochladen" → Datei wählen
  3. 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:

  • privacyPolicyHtml
  • imprintHtml
  • authorizationTemplateHtml
  • websitePrivacyPolicyHtml

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), 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 (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 → DRAFTACTIVE + startDate
    • Kündigungsbestätigung-Upload → ACTIVECANCELLED + cancellationConfirmationDate (mit Datums-Modal beim Upload)
    • Nightly-Cron 02:00: alle ACTIVE-Verträge mit endDate < heuteEXPIRED
  • 🔐 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

S
Description
No description provided
Readme
60 MiB
Languages
TypeScript 97.3%
JavaScript 1.6%
Shell 0.9%
Dockerfile 0.2%