# OpenCRM

Web-basiertes CRM-System für Kundenverwaltung mit Verträgen (Energie, Telekommunikation, KFZ-Versicherung).

**Version: 1.1.0** ([Changelog](#changelog))

## Features

- **Kundenverwaltung**: Privat- und Geschäftskunden mit Stammdaten
- **Adressen**: Mehrere Liefer-/Melde- und Rechnungsadressen pro Kunde
- **Bankkarten**: Mit Ablaufdatum, Aktiv-Status und Dokument-Upload (PDF)
- **Ausweise**: Personalausweis, Reisepass, etc. mit Ablaufdatum und Dokument-Upload (PDF)
- **Zähler**: Strom-/Gaszähler mit Zählerstandhistorie,
  Pflicht-Zuordnung zur Lieferadresse und Folgezähler-Kette
  (Auto-Propagation auf alle Verträge mit dem Vorgänger)
- **Rechnungen**: Rechnungsverwaltung für Energieverträge mit Dokumenten-Upload
- **Vertrags-Cockpit**: Dashboard zur Überwachung offener Aufgaben (fehlende Dokumente, Rechnungen)
- **Auto-Vertragsstatus**: Lieferbestätigung-Upload setzt `DRAFT` → `ACTIVE` (mit Vertragsbeginn),
  Kündigungsbestätigung-Upload setzt `ACTIVE` → `CANCELLED` (mit Datum),
  nightly-Cron setzt `ACTIVE`-Verträge mit abgelaufenem `endDate` auf `EXPIRED`
- **Verträge**:
  - Energie (Strom, Gas)
  - Telekommunikation (DSL, Glasfaser, Mobilfunk, TV)
  - KFZ-Versicherung
- **Folgeverträge**: Automatische Datenübernahme bei Anbieterwechsel
- **Vertriebsplattformen**: Verwaltbar über WebUI
- **Email-Provisionierung**: Automatische E-Mail-Weiterleitung bei Plesk/cPanel/DirectAdmin
- **Berechtigungssystem**: Admin, Mitarbeiter, Nur-Lesen, Kundenportal
- **Verschlüsselte Zugangsdaten**: Portal-Passwörter AES-256-GCM verschlüsselt
- **DSGVO-Compliance**: Audit-Logging mit Hash-Chain-Integritätsprüfung,
  Einwilligungsverwaltung, Datenexport, Löschanfragen
- **Sicherheits-Monitoring**: Realtime-Logging von Login-Fehlversuchen, IDOR-Abwehr,
  SSRF-Blocks, JWT-Manipulation; Threshold-Detection (Brute-Force, IDOR-Probing) mit
  Sofort-E-Mail-Alerts und stündlichem Digest – siehe Einstellungen → Monitoring
- **Production-Hardening**: 10 dokumentierte Hardening-Runden inkl. CORS, Helmet,
  IDOR-Schutz, Rate-Limiting, SSRF/DNS-Rebinding-Block, Per-File-Ownership-Check, mehr
  in [docs/SECURITY-HARDENING.md](docs/SECURITY-HARDENING.md)
- **Developer-Tools**: Datenbank-Browser und interaktives ER-Diagramm

## Tech Stack

- **Frontend**: React 18, TypeScript, Tailwind CSS, React Query
- **Backend**: Node.js, Express 4.x, TypeScript
- **Datenbank**: MariaDB
- **ORM**: Prisma
- **Auth**: JWT-Access-Token (Memory, 15 min) + Refresh-Token im httpOnly-Cookie
  (7 Tage). Rollen-basierte Zugriffskontrolle. XSS klaut maximal einen
  15-min-Access-Token, der Refresh-Cookie ist JS-unzugänglich.

> **Hinweis zu Express 5:** Das Projekt verwendet bewusst Express 4.x (nicht 5.x). Express 5 ist seit Jahren in der Beta-Phase und noch nicht offiziell stable. Bei der Installation darauf achten, dass `@types/express` zur Express-Version passt:
> - Express 4.x → `@types/express@^4.17.x`
> - Express 5.x → `@types/express@^5.x` (erst bei offiziellem Release empfohlen)

## Quick-Start mit Docker (empfohlen)

Komplettes Setup mit MariaDB + OpenCRM + Adminer (DB-UI) in 3 Befehlen:

```bash
git clone <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=true` in der `.env` setzen,
> `docker compose up -d` ausführen, dann wieder auf `false`.
>
> Der initiale Admin-User ist **`admin@admin.com`**. Das Passwort wird
> beim Seed zufällig generiert (28 Zeichen) und genau einmal in den
> Container-Logs ausgegeben:
> ```bash
> docker logs opencrm-app 2>&1 | grep -A 5 "Initial-Passwort"
> ```
> Alternativ kannst du **vor dem ersten Start** ein eigenes Passwort
> setzen, indem du `SEED_ADMIN_PASSWORD=…` (mindestens 25 Zeichen, sonst
> wird's ignoriert) in der `.env` einträgst. Nach dem ersten Login
> sollte das Passwort über *Einstellungen → Mein Profil* geändert
> werden.

## Voraussetzungen

- Docker & Docker Compose v2
- Für Backend-Entwicklung außerhalb von Docker: Node.js 20+ und npm

## Installation für Entwicklung (ohne Container)

### 1. Repository klonen

```bash
git clone <repository-url>
cd opencrm
cp .env.example .env       # Konfiguration anpassen
```

### 2. MariaDB-Container starten

```bash
docker compose up -d db
```

Das startet nur die Datenbank (mit Daten in `./data/db/`).
Konfiguration kommt aus `./.env`:

- **Port:** wie in `DB_PORT` (Standard: 3306, intern auf 127.0.0.1)
- **Datenbank/User/Passwort:** wie in `DB_*`-Variablen

Warte ca. 10 Sekunden bis die Datenbank bereit ist.

### 3. Backend einrichten

```bash
cd backend

# Dependencies installieren
npm install

# .env-Datei erstellen (falls noch nicht vorhanden)
cp .env.example .env
```

Die `.env`-Datei sollte folgende Werte enthalten:

```env
# Database
DATABASE_URL="mysql://root:rootpassword@localhost:3306/opencrm"

# JWT – Access-/Refresh-Token-Pattern (SPA-Standard)
#   Access-Token (Bearer-Header, nur im Browser-Memory, kurzlebig)
#   Refresh-Token (httpOnly-Cookie, lang)
# Beide werden mit JWT_SECRET signiert; Refresh wird nur am
# /api/auth/refresh-Endpoint akzeptiert (type-Claim).
JWT_SECRET="change-this-to-a-very-long-random-secret-in-production"
JWT_EXPIRES_IN="15m"          # Access-Token-Lifetime (Default: 15m)
JWT_REFRESH_EXPIRES_IN="7d"   # Refresh-Token-Lifetime (Default: 7d)

# Encryption (for portal credentials) - generate with: openssl rand -hex 32
ENCRYPTION_KEY="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"

# Server
PORT=3001
NODE_ENV=development
```

### 4. Datenbank initialisieren

```bash
# Prisma Client generieren und Migrationen ausführen
npx prisma migrate dev

# Seed-Daten einspielen (Admin-User, Rollen, Berechtigungen)
npm run db:seed
```

### 5. Frontend einrichten

```bash
cd ../frontend

# Dependencies installieren
npm install
```

## Anwendung starten

### Backend starten (Terminal 1)

```bash
cd backend
npm run dev
```

Das Backend läuft auf `http://localhost:3001`

### Frontend starten (Terminal 2)

```bash
cd frontend
npm run dev
```

Das Frontend läuft auf `http://localhost:5173`

## Erster Login

Nach dem Seed sind folgende Zugangsdaten verfügbar:

- **E-Mail:** `admin@admin.com`
- **Passwort:** wird beim Seed zufällig generiert (28 Zeichen) und einmal
  in den Container-Logs ausgegeben:
  ```bash
  docker logs opencrm-app 2>&1 | grep -A 5 "Initial-Passwort"
  ```
  Beispiel-Ausgabe:
  ```
  ========================================================
    Admin-User: admin@admin.com
    Initial-Passwort: <28-zeichen-zufalls-passwort>
    ⚠️  Dieses Passwort wird hier EINMAL ausgegeben!
    Bitte sofort nach dem ersten Login ändern.
  ========================================================
  ```
  Wer das Log nicht mehr findet (z.B. Container schon länger gerollt),
  setzt `SEED_ADMIN_PASSWORD=<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](#production-deployment).

### Zugangsdaten auf bestehender DB neu setzen (Reseed)

Nützlich z.B. auf **Staging/Test nach einem DB-Reset**, wenn Admin-/Portal-
Passwörter randomisiert sind und man wieder **bekannte** Zugangsdaten braucht
(etwa für einen Pentester).

**Der Seed ist datenerhaltend:** Er `upsert`et nur die Rollen und den
Admin-User (`admin@admin.com`) und synchronisiert die Rollen-Berechtigungen.
**Kundendaten, Verträge, Bankkarten usw. werden NICHT angefasst** (das einzige
`deleteMany` betrifft ausschließlich Rollen↔Permission-Zuordnungen). Ein
erneuter Seed setzt also faktisch nur das Admin-Passwort neu.

**Admin-Passwort deterministisch neu setzen:**

```bash
# 1) In der .env / docker-compose environment setzen:
SEED_ADMIN_PASSWORD=<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`:

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

### Deployment-Modus: On-Prem vs. Cloud

OpenCRM ist primär als **On-Prem-Anwendung** designed (eigener Server / VM,
hinter Reverse-Proxy). Für **Cloud-Deployments** (öffentlich erreichbares
Backend, Shared-Infrastructure, Hyperscaler) gibt es einen zusätzlichen
SSRF-Schalter:

```env
# Cloud-Deploy: zusätzlich alle privaten IP-Ranges für Provider-/SMTP-
# Test-Connection blockieren (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12,
# 192.168.0.0/16, ::1, fc00::/7, localhost). Default false, weil
# On-Prem-Setups oft Plesk/Dovecot auf 127.0.0.1 brauchen.
SSRF_BLOCK_PRIVATE_IPS=true
```

Cloud-Metadata-Endpoints (`169.254.169.254`, `metadata.google.internal` etc.)
sind UNABHÄNGIG vom Flag **immer** geblockt – das ist Mindestschutz gegen
AWS/GCP/Azure-IMDS-Diebstahl.

Plus:

- **Reverse-Proxy** (Nginx/Plesk) so konfigurieren, dass `X-Forwarded-For` hart auf
  die echte Client-IP gesetzt wird (nicht nur angefügt) – sonst Rate-Limit-Bypass möglich.
- **Frontend + API müssen über dieselbe Origin laufen.** Die Auth nutzt einen
  httpOnly-Refresh-Cookie mit `SameSite=Strict; Path=/api/auth` – wenn Frontend
  und API auf getrennten Origins liegen (z.B. `crm.example.de` vs.
  `api.example.de`), schickt der Browser das Cookie cross-site nicht mit
  und der `/auth/refresh`-Endpoint kann den User nicht mehr nachladen
  (= alle 15 min Re-Login). Beim NPM-Setup landen Frontend und API automatisch
  auf derselben Domain via Proxy-Path.
- **Admin-Initial-Passwort ändern**: das beim Seed generierte Zufalls-Passwort
  (siehe [Erster Login](#erster-login)) im Profil überschreiben – oder gleich
  per `SEED_ADMIN_PASSWORD` in der `.env` vor dem ersten Start setzen.
- **Manuelle Test-Checkliste** aus [docs/TESTING.md](docs/TESTING.md) einmal komplett
  durchklicken.
- **Monitoring konfigurieren**: Einstellungen → Sicherheits-Monitoring → Alert-E-Mail
  hinterlegen, Test-Alert senden, Digest aktivieren.
- Vollständige Hardening-Story + restliche Trade-offs:
  [docs/SECURITY-HARDENING.md](docs/SECURITY-HARDENING.md)

### ⚠️ Wichtig: gzip für `/api/*` am Reverse-Proxy deaktivieren (BREACH-Schutz)

Wenn ein TLS-Reverse-Proxy (Nginx Proxy Manager, Caddy, eigener Nginx, …) HTTPS
terminiert und Antworten gzip-komprimiert, ist die **BREACH-Attacke** (CVE-2013-3587)
theoretisch möglich: aus der gzip-komprimierten Response-Größe könnten unter
ungünstigen Umständen Secrets erraten werden. Auch wenn unsere JWT-basierte SPA
das Risiko praktisch klein hält (keine reflektierten Secrets im Response-Body),
geht ein Penetration-Test mit testssl trotzdem auf „medium – Ausnutzbar: Ja".

**Lösung:** gzip-Komprimierung nur für statische Frontend-Assets erlauben, für
`/api/*` deaktivieren. Statische Bundles bleiben damit performant ausgeliefert,
JSON-API-Responses werden ohne Kompression gesendet → BREACH ist dort kein
Einfallstor mehr.

**Nginx Proxy Manager (NPM):**
1. Proxy-Hosts → den CRM-Host → **Edit**
2. Tab **Custom Locations** → **„Add location"**
3. **Define location:** `/api/`
4. **Scheme:** `http`, **Forward Hostname/IP:** wie im Haupt-Host
   (z.B. `172.0.2.39`), **Forward Port:** `3010`
5. Zahnrad rechts an der Location → erweiterte Config eintragen:
   ```nginx
   gzip off;
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   proxy_set_header X-Real-IP $remote_addr;
   # Information-Disclosure-Header weg (Pentest-Hygiene):
   more_clear_headers Server X-Served-By;
   ```
6. **Save** (Location), **Save** (Proxy-Host)

> Der `more_clear_headers`-Befehl kommt aus dem `headers-more`-Modul, das
> bei NPM standardmäßig dabei ist. Damit verschwinden die Banner
> `Server: openresty` und `x-served-by: …` aus den Responses – Pentest-
> Tools können den eingesetzten Webserver nicht mehr direkt aus dem Header
> ablesen. Wer das auch auf der Hauptlocation will, kann denselben Eintrag
> zusätzlich im **Advanced**-Tab des Proxy-Hosts setzen.

**Plain Nginx** (falls eigener Nginx statt NPM):
```nginx
location /api/ {
    gzip off;
    proxy_pass http://backend:3010;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
    more_clear_headers Server X-Served-By;   # braucht headers-more-Modul
}
# Optional global im server { … }-Block:
server_tokens off;
```

**Verifikation:**
```bash
# 1) gzip ist für /api/ deaktiviert (sollte leer sein)
curl -sI -H 'Accept-Encoding: gzip' https://kundencenter.deine-domain.de/api/health \
  | grep -i content-encoding

# 2) Server-/x-served-by-Banner sind weg (sollte leer sein)
curl -sI https://kundencenter.deine-domain.de/api/health \
  | grep -iE '^(server|x-served-by):'
```

#### Was mit gzip auf `/` (SPA-HTML) ist

Pentest-Tools wie `testssl` melden BREACH **trotzdem weiter** für die
Root-URL `/`, weil die SPA-`index.html` bewusst weiter gzip-komprimiert
ausgeliefert wird (Performance: 50 KB → ~10 KB). Bei OpenCRM ist der
Angriff dort nicht ausnutzbar:

- Die `/`-Response ist die statische `index.html` aus dem Vite-Build
- Sie reflektiert **keinen user-controlled Input**
- Sie enthält **keine Secrets** (JWT-Access ist im `Authorization`-Header,
  Refresh-Token im httpOnly-Cookie – beides nicht im HTML-Body)

Ohne Secret-im-Body und ohne Input-Reflektion hat BREACH keinen Hebel.

##### Wer den Audit-Marker trotzdem weg haben will

Wichtig: nicht einfach eine Custom-Location für `/` mit `gzip off`
anlegen – das wäre ein **prefix-Match** und würde **alle** Pfade
außer `/api/*` betreffen, also auch `/assets/*.{js,css}`. Das JS-Bundle
käme dann unkomprimiert (~500 KB statt ~150 KB) → spürbarer
Performance-Verlust für nichts.

Sauber ist eine **exact-Match-Location** (`location = /`) – die fängt
nur die Root-URL ohne weitere Pfad-Komponente:

**Variante A** – Custom Location im NPM-UI (falls `= /` im
„Define location"-Feld akzeptiert wird):

| Feld | Wert |
|---|---|
| Define location | `= /` |
| Scheme | `http` |
| Forward Hostname/IP | wie im Haupt-Host |
| Forward Port | `3010` |

Im Zahnrad-Edit der Location:
```nginx
gzip off;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
# Information-Disclosure-Header weg (Pentest-Hygiene):
more_clear_headers Server X-Served-By;
```

**Variante B** – wenn das NPM-UI das `=` nicht akzeptiert, dieselbe
Logik im **Advanced**-Tab des Proxy-Hosts:
```nginx
location = / {
    gzip off;
    proxy_pass $forward_scheme://$server:$port;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
    more_clear_headers Server X-Served-By;
}
```

Verifikation – `/` ohne gzip, `/assets/*` aber weiter mit:
```bash
# Root: kein Content-Encoding mehr
curl -sI -H 'Accept-Encoding: gzip' https://kundencenter.deine-domain.de/ \
  | grep -i content-encoding

# /assets/<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:
   ```javascript
   fetch('/api/developer/setup', { method: 'POST' })
   ```
2. Ausloggen und neu einloggen

Alternativ können Developer-Rechte pro Benutzer vergeben werden:
- Benutzer bearbeiten > "Entwicklerzugriff" aktivieren

## Email-Provisionierung

Das System unterstützt die automatische Erstellung von E-Mail-Weiterleitungen auf Hosting-Servern für Stressfrei-Wechseln Adressen.

### Unterstützte Provider

- **Plesk** (implementiert)
- **cPanel** (vorbereitet)
- **DirectAdmin** (vorbereitet)

### Konfiguration

1. **Einstellungen** → **Email-Provisionierung** öffnen
2. Neuen Provider hinzufügen:
   - **Name**: Bezeichnung (z.B. "Plesk Hauptserver")
   - **Typ**: Plesk/cPanel/DirectAdmin
   - **API-URL**: Server-URL (z.B. `https://server.de:8443`)
   - **API-Key** _(empfohlen bei Plesk)_: Key aus Plesk (siehe unten), alternativ Benutzername/Passwort
   - **Benutzername/Passwort**: Nur wenn kein API-Key vorhanden
   - **Domain**: E-Mail-Domain (z.B. `stressfrei-wechseln.de`)
   - **Standard-Weiterleitung**: Zusätzliche Weiterleitungsadresse (optional)
3. Provider als "Standard" und "Aktiv" markieren
4. Verbindung testen

### Plesk: API-Key anlegen

Der API-Key ist die empfohlene Authentifizierungsmethode (sicherer als Passwort, kann pro
Anwendung vergeben und widerrufen werden).

**Variante 1: Über die Plesk-Oberfläche (einfachster Weg)**

1. In Plesk als Admin einloggen
2. Oben rechts auf den **eigenen Namen** → **"Mein Profil"** (oder direkt URL `/admin/my-profile/`)
3. Tab **"API-Token"** oder **"API-Schlüssel"** öffnen
4. **"API-Schlüssel erstellen"** (bzw. "Add API Key")
5. Beschreibung vergeben (z.B. "OpenCRM")
6. Den angezeigten Schlüssel **sofort kopieren** – er wird nur einmal angezeigt!
7. Im CRM bei "API-Key" einfügen

> **Hinweis:** Bei manchen Plesk-Versionen ist die Option unter
> **Tools & Einstellungen** → **API-Schlüssel** oder **Werkzeuge & Einstellungen** →
> **API-Tokens** zu finden. Wenn der Menüpunkt fehlt, muss ggf. die **REST API**
> Extension installiert werden (siehe Variante 2).

**Variante 2: Über die Kommandozeile (SSH als root)**

Falls der API-Key-Button in Plesk nicht vorhanden ist, lässt er sich auch per SSH erstellen:

```bash
# API-Key generieren (läuft nicht ab)
# WICHTIG: -ip-address weglassen, wenn der Key von beliebigen IPs genutzt werden soll!
plesk bin secret_key --create -description "OpenCRM"

# Alternativ mit IP-Einschränkung (nur Zugriffe von dieser IP sind erlaubt):
plesk bin secret_key --create -ip-address <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:**
```bash
plesk bin secret_key --list
```

**API-Key löschen:**
```bash
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 & Einstellungen** → **Updates** → **Erweiterungen hinzufügen/entfernen**
2. Nach **"REST API"** suchen und installieren
3. Plesk-Neustart (meist nicht nötig, aber zur Sicherheit)

### Plesk: Firewall-Hinweis

Der CRM-Server muss den **Plesk-Port 8443** (Standard) erreichen können. Bei Plesk-Firewall:

1. **Tools & Einstellungen** → **Firewall**
2. **"Plesk-Dienst – Panel"** (Port 8443) für die IP des CRM-Servers erlauben

Bei reiner Linux-Firewall (ufw/firewalld):
```bash
# Beispiel ufw
ufw allow from <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

```bash
npm run dev          # Entwicklungsserver starten
npm run build        # Produktions-Build erstellen
npm run db:studio    # Prisma Studio (Datenbank-GUI)
npm run db:migrate   # Neue Migration erstellen
npm run db:seed      # Seed erneut ausführen
```

### Frontend

```bash
npm run dev      # Entwicklungsserver starten
npm run build    # Produktions-Build erstellen
npm run preview  # Build-Vorschau
```

### Docker (Entwicklung)

```bash
docker-compose up -d       # Container starten
docker-compose down        # Container stoppen
docker-compose down -v     # Container stoppen + Daten löschen
docker-compose logs -f     # Logs anzeigen
```

### Docker (Produktion)

Im `docker/` Verzeichnis liegt ein komplettes Produktions-Setup:

```bash
cd docker

# Image bauen
docker-compose build

# Container starten
docker-compose up -d

# Logs anzeigen
docker-compose logs -f app
```

**Komponenten:**
- **MariaDB 10.11**: Datenbank
- **App**: Backend + Frontend in einem Container
- **Caddy**: Reverse-Proxy mit automatischem SSL

**Umgebungsvariablen (`docker/.env`):**
```env
MYSQL_ROOT_PASSWORD=sicheres-root-passwort
MYSQL_DATABASE=opencrm
MYSQL_USER=opencrm
MYSQL_PASSWORD=sicheres-passwort
JWT_SECRET=sehr-langer-zufaelliger-string
ENCRYPTION_KEY=64-zeichen-hex-string
DOMAIN=crm.example.com
RUN_SEED=true  # Nur beim ersten Start
```

## 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
├── docker/                  # Docker-Deployment
│   ├── Dockerfile          # Multi-Stage Build
│   ├── docker-compose.yml  # Produktion (MariaDB, App, Caddy)
│   ├── Caddyfile           # Reverse-Proxy mit SSL
│   └── entrypoint.sh       # Container-Startup
├── docker-compose.yml      # MariaDB-Container (Entwicklung)
└── README.md
```

## Berechtigungen

| Rolle | Kunden | Verträge | Benutzer | Plattformen | Audit/DSGVO | Developer |
|-------|--------|----------|----------|-------------|-------------|-----------|
| Admin | CRUD | CRUD | CRUD | CRUD | Vollzugriff | Optional |
| Mitarbeiter | CRUD | CRUD | - | Lesen | Lesen | - |
| Mitarbeiter (Lesen) | Lesen | Lesen | - | Lesen | - | - |
| Kunde | Eigene | Eigene | - | - | - | - |

## Troubleshooting

### Datenbank-Verbindungsfehler

1. Prüfe ob Container läuft: `docker ps`
2. Prüfe die DATABASE_URL in `.env`
3. Warte nach Container-Start ca. 10 Sekunden

### Prisma-Fehler

```bash
# Prisma Client neu generieren
npx prisma generate

# Schema zur Datenbank pushen (ohne Migration)
npx prisma db push
```

### Port bereits belegt

- Backend: `PORT` in `.env` ändern
- Frontend: In `vite.config.ts` anpassen

### Developer-Menü fehlt

```bash
# In der Browser-Console:
fetch('/api/developer/setup', { method: 'POST' }).then(r => r.json()).then(console.log)
# Danach ausloggen und neu einloggen
```

## Vertragstypen

### Standard-Vertragstypen

Folgende Vertragstypen werden bei Installation/Factory-Reset automatisch angelegt:

| Code | Name | Icon | Farbe |
|------|------|------|-------|
| ELECTRICITY | Strom | Zap | #FFC107 |
| GAS | Gas | Flame | #FF5722 |
| DSL | DSL | Wifi | #2196F3 |
| FIBER | Glasfaser | Cable | #9C27B0 |
| CABLE | Kabel Internet (Coax) | Cable | #00BCD4 |
| MOBILE | Mobilfunk | Smartphone | #4CAF50 |
| TV | TV | Tv | #E91E63 |
| CAR_INSURANCE | KFZ-Versicherung | Car | #607D8B |

> **Hinweis:** Vertragstypen können nur von Benutzern mit **Entwicklerzugriff** geändert werden, da Änderungen auch Anpassungen an den Formularen erfordern.

### Vertragstyp-spezifische Felder

Je nach Vertragstyp werden unterschiedliche Felder im Formular angezeigt:

#### Strom & Gas (ELECTRICITY, GAS)

- Zähler-Auswahl (gefiltert auf die Lieferadresse des Vertrags)
- Jahresverbrauch (kWh/m³) – bei Folgeverträgen mit Schätzwert
  aus dem Vorvertrag und 1-Klick-Übernahme
- Grundpreis, Arbeitspreis (HT/NT bei Zweitarif)
- Sofort-Bonus, Neukunden-Bonus
- Vorversorger, Kundennummer beim Vorversorger
- Folgezähler-Workflow (Zählerwechsel): Wechseldatum + optionaler
  Endstand des alten Zählers, der automatisch als Zählerstand
  erfasst wird und in die Verbrauchsberechnung einfließt

#### Internet (DSL, CABLE, FIBER)

- Download/Upload (Mbit/s)
- Router Modell, Seriennummer
- Installationsdatum
- Benutzername, Passwort
- Rufnummern mit SIP-Zugangsdaten

| Vertragstyp | Zusatzfeld |
|-------------|------------|
| **Glasfaser (FIBER)** | Home-ID |

#### Mobilfunk (MOBILE)

- Datenvolumen (GB)
- Inklusiv-Minuten, Inklusiv-SMS
- Gerät-Modell, IMEI
- Multisim-Checkbox
- SIM-Karten (dynamisch erweiterbar):
  - Rufnummer, SIM-Kartennummer (ICCID)
  - PIN, PUK (verschlüsselt)
  - Multisim-Flag, Hauptkarte-Flag
  - Kartennutzer (optional) – tatsächlicher Nutzer bei Firmen-/
    Familienverträgen, kann vom Vertragsinhaber abweichen

> **Hinweis Multisim:** Nicht buchbar bei Klarmobil, Congstar, Otelo. Benötigt Freenet oder vergleichbar.

#### TV

- Receiver Modell
- Smartcard-Nummer
- Paket/Angebot

#### KFZ-Versicherung (CAR_INSURANCE)

- Kennzeichen, HSN, TSN, FIN/VIN
- Fahrzeugtyp, Erstzulassung
- SF-Klasse (Schadenfreiheitsklasse)
- Versicherungsart (Haftpflicht/Teilkasko/Vollkasko)
- Selbstbeteiligungen (Teilkasko, Vollkasko)
- Versicherungsscheinnummer
- Vorversicherer

### Standard-Anbieter

Folgende Anbieter werden bei Installation/Factory-Reset automatisch angelegt:

| Anbieter | Portal-URL |
|----------|------------|
| Vodafone | https://www.vodafone.de/meinvodafone/account/login |
| Klarmobil | https://www.klarmobil.de/login |
| Otelo | https://www.otelo.de/mein-otelo/login |
| Congstar | https://www.congstar.de/login/ |
| Telekom | https://www.telekom.de/kundencenter/startseite |
| O2 | https://www.o2online.de/ecare/selfcare |
| 1&1 | https://control-center.1und1.de/ |

### Anbieter-spezifische Felder

Einige Felder werden nur bei bestimmten Anbietern angezeigt:

| Anbieter | Vertragstyp | Zusatzfeld |
|----------|-------------|------------|
| **Vodafone** | DSL, Kabel Internet | Aktivierungscode |

> **Hinweis Multisim:** Bei Klarmobil, Congstar und Otelo ist Multisim **nicht** buchbar. Dafür wird Freenet oder ein vergleichbarer Anbieter benötigt.

## Rechnungsverwaltung (Energieverträge)

Für Strom- und Gas-Verträge können Rechnungen verwaltet werden, um den Abrechnungsstatus zu tracken.

### Rechnungstypen

| Typ | Beschreibung |
|-----|--------------|
| **Zwischenrechnung** (INTERIM) | Reguläre Jahresrechnung während der Vertragslaufzeit |
| **Schlussrechnung** (FINAL) | Endabrechnung nach Vertragskündigung/Deaktivierung |
| **Nicht verfügbar** (NOT_AVAILABLE) | Rechnung ist nicht mehr zu bekommen (z.B. Anbieter existiert nicht mehr) |

### Funktionen

- **Rechnungen hinzufügen/bearbeiten/löschen** in der Vertragsdetailansicht
- **Dokument-Upload** (PDF) - Pflicht, außer bei Typ "Nicht verfügbar"
- **Statusanzeige**: Grünes Badge bei Schlussrechnung, Orange bei fehlender Schlussrechnung
- **E-Mail-Anhänge als Rechnung speichern**: Direkt aus dem E-Mail-Client

### E-Mail-Integration

Bei E-Mails, die einem Energievertrag zugeordnet sind:
1. Toggle zwischen "Als Dokument" und "Als Rechnung" im Speichern-Dialog
2. Rechnungsdatum und Typ auswählen
3. Anhang wird automatisch als Rechnungsdokument gespeichert

## Vertrags-Cockpit

Dashboard zur Überwachung offener Aufgaben und fehlender Dokumente.

### Kategorien

| Kategorie | Prüfungen |
|-----------|-----------|
| **Fehlende Dokumente** | Kündigungsschreiben, Kündigungsbestätigung (wenn Kündigung markiert) |
| **Fehlende Rechnungen** | Schluss-/Zwischenrechnungen für Energieverträge |
| **Ablaufende Dokumente** | Ausweise, Bankkarten (nächste 30 Tage) |

### Rechnungsprüfung für Energieverträge

#### Schlussrechnung (gekündigte/deaktivierte Verträge)

| Status | Prüfung |
|--------|---------|
| **CANCELLED** / **DEACTIVATED** | Schlussrechnung oder "Nicht verfügbar" erforderlich |

#### Zwischenrechnung (laufende Verträge)

| Bedingung | Warnung |
|-----------|---------|
| Vertrag > 12 Monate alt, keine Rechnung | "Zwischenrechnung fehlt" |
| Letzte Rechnung > 12 Monate her | "Zwischenrechnung überfällig" |

> **Hinweis Status-Logik:**
> - **EXPIRED** = Laufzeit abgelaufen, aber Vertrag läuft ohne Kündigung weiter → Zwischenrechnung prüfen
> - **CANCELLED** = Aktive Kündigung → Schlussrechnung prüfen
> - **DEACTIVATED** = Manuell beendet → Schlussrechnung prüfen

### Vertrag zurückstellen (Snooze)

Wenn ein Vertrag EXPIRED ist (Laufzeit vorbei, läuft aber weiter), erscheint er im Cockpit. Manchmal ist Bleiben günstiger als Wechseln - aber der Vertrag soll nicht dauerhaft im Cockpit erscheinen.

**Lösung:** Vertrag temporär zurückstellen mit Datum für erneute Prüfung.

#### Snooze aktivieren

Im Cockpit hat jeder Vertrag einen Snooze-Button (Glocke mit Uhr). Optionen:
- **+3 Monate**
- **+6 Monate** (Empfohlen)
- **+12 Monate**
- **Eigenes Datum** (Datepicker)

#### Unterdrückte Warnungen (bei aktivem Snooze)

| Warnung | Beschreibung |
|---------|--------------|
| Kündigungsfrist | Frist läuft ab |
| Vertrag läuft ab | Enddatum naht |
| Kündigungsschreiben fehlt | Bei markierter Kündigung |
| Kündigungsbestätigung fehlt | Bei markierter Kündigung |

#### NICHT unterdrückte Warnungen

Diese Warnungen werden auch bei aktivem Snooze angezeigt:

| Warnung | Beschreibung |
|---------|--------------|
| Schlussrechnung fehlt | Beendeter Vertrag ohne Endabrechnung |
| Zwischenrechnung fehlt/überfällig | Laufender Vertrag ohne Jahresrechnung |
| Zählerstand fehlt | Fehlende Zählerstände bei Energieverträgen |
| Zugangsdaten fehlen | Portal-Passwort, SIP-Daten, etc. |

#### Snooze aufheben

**Im Cockpit:** Bei Verträgen in der Kategorie "Erneute Prüfung fällig" erscheint ein "Snooze aufheben"-Button.

**In Vertragsdetails:** Bei zurückgestellten Verträgen erscheint ein bernsteinfarbenes Badge "Zurückgestellt bis [Datum]" mit X-Button zum Aufheben.

#### Erneute Prüfung fällig

Wenn das Snooze-Datum in der Vergangenheit liegt, erscheint der Vertrag in der neuen Kategorie "Erneute Prüfung fällig" mit Angabe, seit wie vielen Tagen die Prüfung fällig ist.

### Status-Info Modal

An verschiedenen Stellen (Vertragsformular, Vertragsdetails, Vertragsübersicht, Kundenansicht) zeigt ein ℹ-Icon neben dem Status eine Erklärung aller Vertragsstatus:

| Status | Bedeutung |
|--------|-----------|
| **Entwurf** | Vertrag wird noch vorbereitet |
| **Ausstehend** | Wartet auf Aktivierung |
| **Aktiv** | Vertrag läuft normal |
| **Abgelaufen** | Laufzeit vorbei, läuft aber ohne Kündigung weiter |
| **Gekündigt** | Aktive Kündigung eingereicht, Vertrag endet |
| **Deaktiviert** | Manuell beendet/archiviert |

## E-Mail-Client

Ein vollständig integrierter E-Mail-Client pro Kunde mit IMAP-Empfang und SMTP-Versand.

### Funktionen

#### E-Mails lesen & verwalten
- **E-Mail-Tab in Kundenansicht** mit Ordnern: Posteingang, Gesendet, Papierkorb
- **Mehrere E-Mail-Konten** pro Kunde (Dropdown zur Auswahl)
- **E-Mail-Detailansicht** mit HTML/Text-Body, Absender, Empfänger, CC, Datum
- **Gelesen/Ungelesen** markieren
- **Favoriten** (Stern) für wichtige E-Mails
- **Papierkorb** mit Wiederherstellen und endgültigem Löschen

#### E-Mails schreiben
- **Neue E-Mail verfassen** mit An, CC, Betreff, Text
- **Antworten** mit zitiertem Originaltext
- **Dateianhänge** (max. 10 MB pro Datei, 25 MB gesamt)
- **SMTP-Versand** über die gewählte StressfreiEmail-Adresse

#### Vertragszuordnung
- **E-Mails zu Verträgen zuordnen** für bessere Nachverfolgung
- **E-Mail-Tab in Vertragsansicht** zeigt nur zugeordnete E-Mails
- **Automatische Zuordnung** bei Versand aus Vertragskontext
- **Manuelle Zuordnung** über Suchfeld und Vertragsauswahl

#### Anhänge
- **Anhangsliste** in E-Mail-Detail
- **Download** einzelner Anhänge
- **Inline-Ansicht** (im Browser öffnen)

### Technische Details

#### Backend-Services
| Service | Beschreibung |
|---------|--------------|
| `imapService.ts` | IMAP-Client (ImapFlow) für E-Mail-Empfang |
| `smtpService.ts` | SMTP-Client (Nodemailer) für E-Mail-Versand |
| `cachedEmail.service.ts` | E-Mail-Caching, Synchronisation, Zuordnung |

#### API-Endpunkte
```
GET  /api/customers/:id/emails              # E-Mails für Kunde
GET  /api/contracts/:id/emails              # E-Mails für Vertrag
GET  /api/emails/:id                        # Einzelne E-Mail mit Body
POST /api/stressfrei-emails/:id/sync        # IMAP-Synchronisation
POST /api/stressfrei-emails/:id/send        # E-Mail senden
POST /api/emails/:id/assign                 # Vertrag zuordnen
DELETE /api/emails/:id/assign               # Zuordnung aufheben
PATCH /api/emails/:id/read                  # Gelesen/Ungelesen
POST /api/emails/:id/star                   # Favorit umschalten
DELETE /api/emails/:id                      # In Papierkorb
POST /api/emails/:id/restore                # Aus Papierkorb wiederherstellen
DELETE /api/emails/:id/permanent            # Endgültig löschen
GET  /api/emails/:id/attachments/:filename  # Anhang herunterladen

# Rechnungen (Invoices)
GET    /api/invoices/ecd/:ecdId              # Rechnungen für EnergyContractDetails
POST   /api/invoices/ecd/:ecdId              # Rechnung hinzufügen
PUT    /api/invoices/ecd/:ecdId/:invoiceId   # Rechnung bearbeiten
DELETE /api/invoices/ecd/:ecdId/:invoiceId   # Rechnung löschen
POST   /api/invoices/:invoiceId/upload       # Rechnungsdokument hochladen

# Cockpit
GET    /api/contracts/cockpit                # Offene Aufgaben abrufen
```

#### Datenbank-Modell
```prisma
model CachedEmail {
  id                Int              @id @default(autoincrement())
  stressfreiEmailId Int
  stressfreiEmail   StressfreiEmail  @relation(...)
  folder            String           // INBOX, SENT
  messageId         String           // RFC 5322 Message-ID
  uid               Int              // IMAP UID
  subject           String?
  fromAddress       String
  fromName          String?
  toAddresses       String           @db.Text  // JSON Array
  ccAddresses       String?          @db.Text
  receivedAt        DateTime
  textBody          String?          @db.LongText
  htmlBody          String?          @db.LongText
  hasAttachments    Boolean          @default(false)
  attachmentNames   String?          @db.Text  // JSON Array
  contractId        Int?             // Vertragszuordnung
  assignedAt        DateTime?
  assignedBy        Int?
  isAutoAssigned    Boolean          @default(false)
  isRead            Boolean          @default(false)
  isStarred         Boolean          @default(false)
  isDeleted         Boolean          @default(false)
  deletedAt         DateTime?

  @@unique([stressfreiEmailId, messageId, folder])
}
```

#### Sicherheit
- **Passwort-Verschlüsselung**: AES-256-GCM für Mailbox-Passwörter
- **Passwort-Reset**: Neues Passwort generieren und beim Provider setzen
- **Verschlüsselungsmodi**: SSL, STARTTLS, oder unverschlüsselt
- **Selbstsignierte Zertifikate**: Konfigurierbar pro Provider

#### Berechtigungen
| Aktion | Berechtigung |
|--------|--------------|
| E-Mails lesen | `customers:read` |
| E-Mails senden, markieren | `customers:update` |
| Anhänge in Dokumente speichern | `customers:update` |
| Vertrag zuordnen | `contracts:update` |
| Löschen, Papierkorb | `emails:delete` |

### Frontend-Komponenten

| Komponente | Beschreibung |
|------------|--------------|
| `EmailClientTab.tsx` | Haupt-Tab mit Konto-Auswahl und Ordnern |
| `EmailList.tsx` | E-Mail-Liste mit Aktionen |
| `EmailDetail.tsx` | E-Mail-Ansicht mit Anhängen |
| `ComposeEmailModal.tsx` | Neue E-Mail / Antworten |
| `TrashEmailList.tsx` | Papierkorb-Verwaltung |
| `AssignToContractModal.tsx` | Vertragszuordnung |
| `ContractEmailsSection.tsx` | E-Mails in Vertragsansicht |
| `SaveAttachmentModal.tsx` | Anhänge in Dokumentfelder speichern |
| `SaveEmailAsPdfModal.tsx` | E-Mail als PDF in Dokumentfelder speichern |
| `InvoicesSection.tsx` | Rechnungsverwaltung in Vertragsdetails |

### Anhänge als Dokumente speichern

E-Mail-Anhänge können direkt in Dokumentfelder des CRM gespeichert werden. Über den blauen Speichern-Button (💾) neben jedem Anhang öffnet sich ein Modal mit allen verfügbaren Zielen.

#### Verfügbare Ziele

| Kategorie | Dokumentfelder |
|-----------|----------------|
| **Kunde** | Datenschutzerklärung |
| **Kunde (Gewerbe)** | + Gewerbeanmeldung, Handelsregisterauszug |
| **Ausweisdokumente** | Dokumentscan (pro Ausweis) |
| **Bankkarten** | Kartenscan (pro Karte) |
| **Vertrag** | Kündigungsschreiben, Kündigungsbestätigung, Kündigungsschreiben (Optionen), Kündigungsbestätigung (Optionen) |

> **Hinweis:** Vertragsdokumente sind nur verfügbar, wenn die E-Mail einem Vertrag zugeordnet ist.

#### Dynamische Konfiguration

Die Dokumentziele werden zentral in `backend/src/config/documentTargets.config.ts` konfiguriert. Neue Dokumentfelder werden automatisch im Modal angezeigt, ohne Frontend-Änderungen.

```typescript
// Beispiel: Neues Feld hinzufügen
{
  key: 'newDocument',
  label: 'Neues Dokument',
  field: 'newDocumentPath',      // Prisma-Feld
  condition: null,                // oder 'BUSINESS' für Geschäftskunden
  directory: 'new-documents'      // Upload-Verzeichnis
}
```

#### Warnung bei Überschreiben

Wenn bereits ein Dokument im Zielfeld vorhanden ist, wird eine Warnung angezeigt. Das vorhandene Dokument wird beim Speichern automatisch ersetzt und die alte Datei gelöscht.

### Vertragszuordnung aufheben (X-Button)

Der X-Button zum Aufheben der Vertragszuordnung erscheint nur bei **manuell zugeordneten** E-Mails. E-Mails, die direkt aus dem Vertragskontext gesendet wurden (`isAutoAssigned = true`), bleiben dauerhaft mit dem Vertrag verknüpft.

| Ansicht | Vertrags-Badge | X-Button sichtbar |
|---------|----------------|-------------------|
| **Kundenakte - Posteingang** | ✅ | ✅ immer |
| **Kundenakte - Gesendet** | ✅ | nur manuell zugeordnet |
| **Kundenakte - Papierkorb** | ✅ | je nach Original-Ordner |
| **Vertrag - Posteingang** | ✅ | ✅ immer |
| **Vertrag - Gesendet** | ✅ | nur manuell zugeordnet |
| **Vertrag - Papierkorb** | ✅ | je nach Original-Ordner |
| **Detail-Ansicht (beide)** | ✅ | nur manuell zugeordnet |

> **Hinweis:** Bei gesendeten E-Mails gilt:
> - `isAutoAssigned = true`: E-Mail wurde direkt aus dem Vertragskontext gesendet → X-Button ausgeblendet
> - `isAutoAssigned = false`: E-Mail wurde manuell dem Vertrag zugeordnet → X-Button sichtbar

### Vertragsbaum in Kundenansicht

In der Kundendetailansicht werden Verträge als **Baumstruktur** mit Vorgänger-Verknüpfung dargestellt:

```
▼ GAS-ML781A4FYXU  │ Gas │ ACTIVE │ 01.01.2025 - 31.12.2026
  └─ GAS-ML24GKR...│ Gas │ EXPIRED│ 05.05.2023 - 05.05.2025  (Vorgänger)
     └─ GAS-OLD123 │ Gas │ EXPIRED│ 01.01.2021 - 04.05.2023  (Vorgänger)

▶ MOB-ML77W560A73  │ Mobil│ DRAFT  │ 02.01.2024 - 02.01.2026
```

**Funktionsweise:**
- **Aktuellste Verträge oben** - Verträge ohne Nachfolger werden als Wurzelknoten angezeigt
- **Standardmäßig eingeklappt** - Klick auf ▶ zeigt die Vorgängerkette
- **Vorgänger eingerückt** - Mit grauem Rand und "(Vorgänger)" Label
- **Verknüpfung über `previousContractId`** - Wird beim Erstellen eines Folgevertrags automatisch gesetzt

> **Hinweis:** In der Hauptvertragsliste (`/contracts`) wird weiterhin die flache Ansicht ohne Baumstruktur verwendet.

## DSGVO-Compliance & Audit-Logging

Umfassendes Audit-Logging-System mit DSGVO-Compliance-Features.

### Audit-Protokoll

Automatische Protokollierung aller API-Zugriffe mit:

| Feld | Beschreibung |
|------|--------------|
| **Benutzer** | User-ID, E-Mail, Rolle |
| **Aktion** | CREATE, READ, UPDATE, DELETE, EXPORT, ANONYMIZE, LOGIN, LOGOUT |
| **Ressource** | Tabelle + ID + lesbare Bezeichnung |
| **Kontext** | Endpoint, HTTP-Methode, IP-Adresse, User-Agent |
| **Änderungen** | Vorher/Nachher-Werte (bei Updates) |
| **Sensitivität** | LOW, MEDIUM, HIGH, CRITICAL |
| **Integrität** | SHA-256 Hash-Kette für Manipulationsschutz |

#### Sensitivitätsstufen

| Stufe | Ressourcen |
|-------|------------|
| **LOW** | Einstellungen, Plattformen, Tarife |
| **MEDIUM** | Verträge, Provider |
| **HIGH** | Kundendaten, Benutzerdaten |
| **CRITICAL** | Authentifizierung, Bankdaten, Ausweisdokumente |

#### Zugriff

- **Einstellungen** → **Audit-Protokoll**
- Filter nach: Datum, Benutzer, Aktion, Ressource, Sensitivität
- Detail-Ansicht mit Vorher/Nachher-Diff
- Export als JSON
- Integritätsprüfung (Hash-Kette verifizieren)

### DSGVO-Dashboard

Zentrale Verwaltung für DSGVO-Anfragen unter **Einstellungen** → **DSGVO-Dashboard**.

#### Dashboard-Statistiken

- Offene Löschanfragen
- Abgeschlossene Löschungen (letzte 30 Tage)
- Datenexporte (letzte 30 Tage)
- Aktive Einwilligungen

#### Einwilligungsverwaltung (Consents)

| Consent-Typ | Beschreibung |
|-------------|--------------|
| **DATA_PROCESSING** | Grundlegende Datenverarbeitung |
| **MARKETING_EMAIL** | E-Mail-Marketing |
| **MARKETING_PHONE** | Telefonmarketing |
| **DATA_SHARING_PARTNER** | Datenweitergabe an Partner |

Einwilligungen können pro Kunde im Tab "Einwilligungen" verwaltet werden.

#### Löschanfragen (Art. 17)

Workflow für DSGVO-Löschanfragen:

| Status | Beschreibung |
|--------|--------------|
| **PENDING** | Anfrage eingegangen |
| **IN_PROGRESS** | Wird bearbeitet |
| **COMPLETED** | Vollständig gelöscht/anonymisiert |
| **PARTIALLY_COMPLETED** | Teildaten behalten (z.B. aktive Verträge) |
| **REJECTED** | Abgelehnt mit Begründung |

**Anonymisierung statt Löschung:**
- Kundendaten werden anonymisiert (nicht gelöscht)
- Aktive Verträge werden beibehalten
- PDF-Löschnachweis wird generiert

#### Datenexport (Art. 15)

Kunden können alle gespeicherten Daten als JSON exportieren:
- Stammdaten
- Adressen, Bankdaten, Ausweise
- Verträge mit Details
- Zähler und Ablesungen
- Einwilligungen
- Zugriffsprotokolle

### API-Endpunkte

```
# Audit-Logs
GET    /api/audit-logs                    # Logs mit Filtern
GET    /api/audit-logs/:id                # Einzelnes Log
GET    /api/audit-logs/customer/:id       # Logs für Kunde
GET    /api/audit-logs/export             # Export (JSON/CSV)
POST   /api/audit-logs/verify             # Hash-Kette prüfen
GET    /api/audit-logs/retention-policies # Aufbewahrungsfristen
PUT    /api/audit-logs/retention-policies/:id
POST   /api/audit-logs/cleanup            # Manuelle Bereinigung

# DSGVO
GET    /api/gdpr/dashboard                # Dashboard-Statistiken
GET    /api/gdpr/customer/:id/export      # Kundendaten-Export
GET    /api/gdpr/deletions                # Löschanfragen
POST   /api/gdpr/deletions                # Löschanfrage erstellen
PUT    /api/gdpr/deletions/:id/process    # Löschanfrage bearbeiten
GET    /api/gdpr/customer/:id/consents    # Einwilligungen abrufen
PUT    /api/gdpr/customer/:id/consents/:type  # Einwilligung ändern
GET    /api/gdpr/consents/overview        # Consent-Übersicht
```

### Aufbewahrungsfristen

| Ressource | Frist | Rechtsgrundlage |
|-----------|-------|-----------------|
| Standard | 10 Jahre | AO §147, HGB §257 |
| Authentifizierung | 2 Jahre | Sicherheit |
| Kundendaten (HIGH) | 10 Jahre | Steuerrecht |
| Verträge | 10 Jahre | Steuerrecht |
| Allgemein (LOW) | 3 Jahre | Verjährung |

### Berechtigungen

| Aktion | Berechtigung |
|--------|--------------|
| Audit-Logs lesen | `audit:read` |
| Audit-Logs exportieren | `audit:export` |
| Audit-Administration | `audit:admin` |
| DSGVO-Export | `gdpr:export` |
| Löschanfrage erstellen | `gdpr:delete` |
| DSGVO-Administration | `gdpr:admin` |

### Technische Details

#### Hash-Kette

Jeder Audit-Log-Eintrag enthält einen SHA-256-Hash über:
- Alle Felder des Eintrags
- Hash des vorherigen Eintrags

Dies ermöglicht die Erkennung von Manipulationen.

#### Sensitive Daten

Folgende Felder werden in Audit-Logs gefiltert:
- `password`, `passwordHash`
- `portalPasswordHash`, `portalPasswordEncrypted`
- `emailPasswordEncrypted`, `internetPasswordEncrypted`
- `sipPasswordEncrypted`, `pin`, `puk`, `apiKey`

#### Performance

- Logging erfolgt asynchron (`setImmediate`)
- API-Response wird nicht blockiert
- Before/After-Werte über Prisma Middleware

## Factory-Defaults: Stammdaten-Kataloge teilen

Das **Factory-Defaults**-System erlaubt den Export und Import von
Stammdaten-Katalogen (Anbieter, Tarife, PDF-Auftragsvorlagen, HTML-Standardtexte)
zwischen verschiedenen OpenCRM-Installationen. Es ist bewusst **streng abgegrenzt**
zu Datenbank-Backups:

### Abgrenzung

| | Factory-Defaults | Datenbank-Backup |
|---|---|---|
| Anbieter, Tarife, Kündigungsfristen, Laufzeiten, Kategorien | ✅ | ✅ |
| PDF-Auftragsvorlagen (inkl. Dateien + Feldzuordnungen) | ✅ | ✅ |
| HTML-Standardtexte: Datenschutzerklärung, Impressum, Vollmacht-Vorlage, Website-Datenschutz | ✅ | ✅ |
| **Kundendaten, Verträge, Dokumente** | ❌ | ✅ |
| **Emails, SMTP-/IMAP-Zugangsdaten** | ❌ | ✅ |
| **Secrets, JWT, Encryption-Keys, User-Accounts** | ❌ | ✅ |
| Zwischen verschiedenen Installationen teilbar | ✅ | ❌ (zu firmen-spezifisch) |

> **Kurz:** Factory-Defaults = generische Stammdaten + rechtliche Standardtexte,
> Backup = die komplette Instanz.

### Drei Wege, eine ZIP zu transportieren

Es gibt drei Pfade, je nachdem wo die ZIP gerade liegen soll:

| Wo | Pfad | Wann |
|---|---|---|
| **Laufende DB einer Instanz** | UI-Upload oder `./factory-import.sh` | Bestehende Live-Instanz updaten |
| **Drop-Box im Repo** (`factory-exports/`) | `./factory-export.sh` legt ab, `./factory-import.sh` liest | Transfer zwischen dev und prod via `scp` |
| **Werkseinstellung im Image** (`backend/factory-defaults/`) | `./factory-import.sh --save-as-builtin` oder manuell entpacken | Neue VMs sollen die Defaults beim allerersten Start mitbringen |

Alle drei sind unabhängig, **alle drei zusammen** decken den typischen Workflow ab.

### Export

**Variante A – UI:**
1. **Einstellungen** → **Factory-Defaults** öffnen
2. Button **„Factory-Defaults exportieren"** klicken
3. ZIP wird als `factory-defaults-YYYY-MM-DD.zip` heruntergeladen

**Variante B – CLI (für scp-Transfers):**
```bash
./factory-export.sh                                  # → factory-exports/factory-defaults-…zip
OPENCRM_URL=https://crm.prod.example.de \
  OPENCRM_EMAIL=admin@example.de ./factory-export.sh # gegen Prod-Instanz
```
Ohne `OPENCRM_PASSWORD` wird das Passwort interaktiv abgefragt. Der Zielordner
`factory-exports/` ist gitignored – die ZIPs landen also nicht ins Repo.

**ZIP-Struktur:**
```
factory-defaults-2026-05-07-1949.zip
├── manifest.json                      # Version + Datum + Counts
├── providers/providers.json
├── contract-meta/
│   ├── cancellation-periods.json
│   ├── contract-durations.json
│   └── contract-categories.json
├── pdf-templates/
│   ├── pdf-templates.json
│   └── *.pdf                          # Die eigentlichen PDF-Dateien
└── app-settings/
    └── app-settings.json              # HTML-Templates (Whitelist-only)
```

### Import

**Variante A – UI:**
1. **Einstellungen** → **Factory-Defaults**
2. Bereich **Import** → **„ZIP hochladen"** → Datei wählen
3. Erfolgs-Box zeigt Counts pro Kategorie

**Variante B – CLI:**
```bash
./factory-import.sh                                  # nimmt jüngste ZIP aus factory-exports/
./factory-import.sh ./factory-exports/foo.zip        # explizite ZIP
./factory-import.sh --save-as-builtin                # zusätzlich ins Image-Default
./factory-import.sh --save-as-builtin ./foo.zip      # entpacken (siehe unten)
```

Konfigurierbar per ENV: `OPENCRM_URL`, `OPENCRM_EMAIL`, `OPENCRM_PASSWORD`.

**Variante C – Container-Bare-Metal (für Migration / mehrere ZIPs zusammenführen):**
```bash
# Inhalt der ZIP nach backend/factory-defaults/ entpacken (Unterordner beibehalten)
cd backend && npm run seed:defaults
```

**Beispiel-Output:**
```
✓ Anbieter: 10
✓ Tarife: 4
✓ Kündigungsfristen: 18
✓ Laufzeiten: 18
✓ Vertragskategorien: 8
✓ PDF-Vorlagen: 2
✓ HTML-Templates: 2
```

### `--save-as-builtin`: ZIP zur Werkseinstellung machen

Mit `--save-as-builtin` entpackt `factory-import.sh` die ZIP nach **erfolgreichem
DB-Import** zusätzlich in `backend/factory-defaults/`. Beim nächsten
`docker-compose up --build` landen die Defaults im Image. Frisch hochgezogene
VMs bringen sie dann beim ersten Start automatisch mit (Auto-Seed-Pfad im
Container-Entrypoint).

```bash
# typischer Sync prod → dev → Image-Default
ssh prod './factory-export.sh'
scp prod:opencrm/factory-exports/factory-defaults-*.zip factory-exports/
./factory-import.sh --save-as-builtin
docker-compose up -d --build           # neuer Build, neue VMs starten direkt mit Defaults
```

Der Inhalt von `backend/factory-defaults/` wird beim `--save-as-builtin` vorher
geleert (außer `README.md` und `.gitkeep`), damit nichts Veraltetes liegen
bleibt.

### Mehrere ZIPs kombinieren (CLI-only, Variante C)

`backend/factory-defaults/` darf mehrere `*.json` pro Unterordner haben –
`npm run seed:defaults` merged sie automatisch:

```
backend/factory-defaults/
  providers/
    verivox.json       # 40 Anbieter aus Verivox-Paket
    check24.json       # 30 Anbieter aus Check24-Paket
    eigene.json        # 5 eigene Anbieter
```

Bei gleichem Unique-Key gewinnt der zuletzt gelesene Eintrag. Der UI-/HTTP-Import
nimmt nur eine ZIP entgegen – für Merges nutze `npm run seed:defaults`.

### Idempotenz

Alle Pfade nutzen Prisma `upsert`:
- **Neue Einträge** werden angelegt
- **Bestehende Einträge** (per unique Key: `name` / `code` / `key`) werden aktualisiert
- Nichts wird gelöscht

Du kannst Imports also beliebig oft hintereinander ausführen, ohne Datenverlust
oder Duplikate.

### PDF-Dateien

Beim Import werden PDF-Vorlagen aus dem ZIP nach `uploads/pdf-templates/`
kopiert (mit eindeutigem Suffix) und die `templatePath`-Spalte entsprechend
gesetzt. Beim Re-Import wird die alte Datei in `uploads/` entsorgt und durch
die neue ersetzt.

### AppSettings-Whitelist

Beim Import werden nur die Keys mit AppSetting-Schreibzugriff gewährt, die auch
exportiert werden – aktuell:

- `privacyPolicyHtml`
- `imprintHtml`
- `authorizationTemplateHtml`
- `websitePrivacyPolicyHtml`

Andere Keys (SMTP, JWT, etc.) werden mit einer Warnung ignoriert. Whitelist ist
in [`backend/src/services/factoryDefaults.service.ts`](backend/src/services/factoryDefaults.service.ts)
zentral gepflegt.

### Auto-Seed beim Erst-Deploy

Bei einer **frischen** Installation (leere DB) spielt der Container-Entrypoint
nach dem Prisma-Seed automatisch das Built-in-Verzeichnis ein:

```
[entrypoint] DB ist leer (User-Count=0) – Auto-Seed wird ausgeführt
[entrypoint] Spiele eingebaute Factory-Defaults ein…
  ✓ Anbieter: 10, Tarife: 4
  …
```

Bei bestehenden Installs passiert das **nicht** – nur frische DBs.

### Berechtigungen

| Aktion | Berechtigung |
|--------|--------------|
| Factory-Defaults Vorschau | `settings:read` |
| Factory-Defaults Export (UI/CLI) | `settings:update` |
| Factory-Defaults Import (UI/CLI) | `settings:update` |
| Werkseinstellungen ändern (`--save-as-builtin` / `npm run seed:defaults`) | Server-Zugang (SSH/Shell) |

### Typische Einsatzzwecke

- **Neue VM aufsetzen**: ZIP einmalig nach `backend/factory-defaults/` entpacken
  (oder per `--save-as-builtin`), dann `docker-compose up --build` – die
  Werkseinstellungen sind beim ersten Start automatisch drin.
- **Prod-Stand zurück nach dev synchronisieren**: `./factory-export.sh` auf prod,
  `scp` ins dev, `./factory-import.sh --save-as-builtin` lokal – damit ist
  sowohl die dev-DB aktuell als auch der nächste Image-Build.
- **Vorlagen-Paket teilen**: Eine ZIP mit nur PDF-Vorlagen weitergeben
  (andere Ordner aus der ZIP entfernen vor dem Entpacken).
- **Anbieter-Paket teilen**: ZIP mit nur `providers/` weitergeben
- **Versionskontrolle**: Die entpackten JSON-Dateien unter Versionskontrolle
  stellen (außerhalb von `backend/factory-defaults/`, da der Ordner gitignored ist)

## Changelog

### 1.1.0 (2026-05-01)

**Production-readiness** – die Version, die wirklich öffentlich gehen darf.

- 🛡 **Security-Hardening**: 10 Runden statisches + dynamisches Audit, vollständig
  dokumentiert in [docs/SECURITY-HARDENING.md](docs/SECURITY-HARDENING.md)
  (CORS/Helmet/JWT, IDOR-Schutz an 30+ Endpoints, Mass-Assignment-Whitelists,
  Zip-Slip, Path-Traversal, Login-Timing-Side-Channel, XFF-Rate-Limit-Bypass,
  Customer-Liste-Leak, SSRF + DNS-Rebinding, Per-File-Ownership statt
  freiem `/api/uploads`, JWT-Logout, Audit-Log-Hash-Chain).
- 🚨 **Sicherheits-Monitoring**: neue `SecurityEvent`-Tabelle + Hooks an Login,
  Logout, Rate-Limit-Hit, IDOR-Abwehr, SSRF-Block, Password-Reset, JWT-Reject.
  Threshold-Detection (Brute-Force, IDOR-Probing, SSRF-Probing) erzeugt
  CRITICAL-Events. **Sofort-E-Mail-Alerts** für CRITICAL + **stündlicher Digest**
  für HIGH/MEDIUM. UI in Einstellungen → Monitoring mit Filter, Pagination,
  Log-leeren (mit optionalem Tage-Filter) und Test-Alert-Button.
- 🔄 **Auto-Vertragsstatus**:
  - Lieferbestätigung-Upload → `DRAFT` → `ACTIVE` + `startDate`
  - Kündigungsbestätigung-Upload → `ACTIVE` → `CANCELLED` + `cancellationConfirmationDate`
    (mit Datums-Modal beim Upload)
  - Nightly-Cron 02:00: alle `ACTIVE`-Verträge mit `endDate < heute` → `EXPIRED`
- 🔐 **Lazy bcrypt-Rehash**: Bestandshashes mit Cost 10 werden beim nächsten
  Login transparent auf Cost 12 geupgradet.
- 🚪 **Logout-Endpoint** `POST /api/auth/logout`: invalidiert JWTs serverseitig
  über `tokenInvalidatedAt`.
- 📦 **`npm audit fix`**: 8 transitive Vulnerabilities gefixt (lodash,
  path-to-regexp, undici, minimatch).

### 1.0.0

Erste Release-Version.

- Kunden-, Vertrags-, Adress-, Bankkarten-, Ausweis- und Zählerverwaltung
- Energie-/Telekommunikations-/KFZ-Verträge mit typspezifischen Details
- Vertrags-Cockpit mit Rechnungsprüfung
- E-Mail-Client mit Anhang-Verwaltung
- DSGVO-Compliance: Audit-Log, Einwilligungen, Datenexport, Löschanfragen
- PDF-Auftragsvorlagen-System mit visueller Feldzuordnung
- Factory-Defaults für Stammdaten-Kataloge
- Mandantenfähigkeit über `customerEmailLabel` pro Provider
- Passwort-Reset-Flow + Rate-Limiting + Auto-Geburtstagsgrüße

## Lizenz

MIT
