Files
perinco-CRM/docs/06-betrieb.md
T
Sascha WolfandClaude Opus 5 c8eaaa1947 feat(auth): Client-Secret in den Einstellungen pflegen, Warnung vor Ablauf
Das Secret der App-Registrierung laeuft in Entra ab. Bisher hiess das: jemand
mit SSH-Zugang aendert die .env und startet den Dienst neu - und niemand sieht
vorher, wann es soweit ist. Laeuft es aus, stehen Anmeldung UND
Postfachabgleich still, denn beide haengen an derselben Registrierung.

Neu unter Einstellungen > Anmeldung: Secret eintragen, Ablaufdatum hinterlegen,
Restlaufzeit ablesen. Ab 30 Tagen vor dem Ablauf bekommen Administratoren ueber
jeder Ansicht ein Hinweisband, das direkt dorthin fuehrt; nach dem Ablauf wird
es rot. Der Hinweis reist mit /api/badges mit, das die App ohnehin bei jedem
Laden abruft.

Bewusst so entschieden:
  - Das Secret muss dafuer in die Datenbank, entgegen der bisherigen Regel
    "keine Geheimnisse in crm_settings". Es liegt deshalb AES-256-GCM-
    verschluesselt dort; der Schluessel kommt aus CRM_ENCRYPTION_KEY, ersatz-
    weise JWT_SECRET, und steht damit nie im selben Dump.
  - Vor dem Speichern holt das CRM mit dem eingegebenen Wert ein Token bei
    Microsoft. Ein Tippfehler wird gar nicht erst uebernommen.
  - Gelesen wird Datenbank vor .env; der .env-Wert bleibt Rueckfallebene, und
    "Hinterlegtes Secret verwerfen" schaltet dorthin zurueck.
  - Passt der Schluessel nicht mehr, meldet die Oberflaeche das ausdruecklich,
    statt still auf die .env zurueckzufallen - sonst sucht man den Fehler an
    der falschen Stelle.
  - Das Ablaufdatum wird von Hand eingetragen. Aus Entra lesen braeuchte
    Application.Read.All, die die App bewusst nicht hat.

MAIL_CONFIGURED und SSO_CONFIGURED sind dadurch von Konstanten zu den
Funktionen mailConfigured() und ssoConfigured() geworden - die Pruefung muss
jetzt in die Datenbank sehen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 08:38:28 +02:00

172 lines
7.8 KiB
Markdown

# 06 Betrieb
## Deployment
Gebaut wird **lokal**, auf den Server geht nur `dist/` plus Backend-Quellen. Der
Server hat wenig freien RAM und volles Swap - dort wird nicht gebaut.
```mermaid
flowchart LR
L["Arbeitsplatz"] -->|"npm run build"| D["dist/"]
D -->|pscp| S1["/var/www/perinco_CRM/dist"]
L -->|pscp| S2["/var/www/perinco_CRM/server/src"]
L -->|pscp| S3["server/package.json"]
S3 -.->|"nur mit -Deps"| NI["npm install --omit=dev"]
NI --> R["systemctl restart<br/>perinco-crm-backend"]
S2 --> R
R --> H["curl /api/health"]
```
```powershell
npm run build
powershell -File deploy/deploy.ps1 # dist + Backend
powershell -File deploy/deploy.ps1 -Deps # zusätzlich npm install
powershell -File deploy/deploy.ps1 -SkipBuild # ohne neuen Build
```
Das Passwort kommt aus `$env:PERINCO_CRM_SSH_PASSWORD` oder per `-Password`.
`deploy.ps1` lädt die systemd-Unit **nicht** mit hoch. Nach Änderungen an
`deploy/perinco-crm-backend.service` von Hand kopieren und `daemon-reload`.
## Dienste auf dem Server
| Unit | Verzeichnis | Port | Anmerkung |
| --- | --- | --- | --- |
| `perinco-crm-backend` | `/var/www/perinco_CRM/server` | 5006 | `MemoryMax=1536M` |
Die zweite Unit `project-scanner-backend` (Port 5005, `/var/www/Projekt Scanner/backend`)
ist mit dem [Rückbau der alten App](03-scanner.md#rückbau-der-alten-app---09092026)
am 09.09.2026 entfallen. Port 5005 ist seither frei.
`MemoryMax=1536M` statt der ursprünglichen 512M: ein Scan-Lauf startet Chromium
als Kindprozess in derselben cgroup. Mit 512M würde der OOM-Killer das Backend
mitten im Scrapen beenden.
## Umgebungsvariablen
`server/.env`, Rechte `600`. Vorlage: `server/.env.example`.
| Variable | Bedeutung |
| --- | --- |
| `PORT`, `HOST` | `5006`, `127.0.0.1` - von außen nur über Apache |
| `DB_*` | Verbindung zu `perinco_CRM` |
| `JWT_SECRET`, `JWT_EXPIRES_IN` | Signatur der Sitzungen, Standard 12 h |
| `ADMIN_*` | wird beim ersten Start angelegt, nur wenn `crm_users` leer ist |
| `SCANNER_DB_NAME` | eigene Scanner-Datenbank, Standard `perinco_scanner` |
| `SCANNER_ENCRYPTION_KEY` | AES-Schlüssel für Portal-Passwörter |
| `SCANNER_UPLOAD_DIR` | CV-Verzeichnis, leer = `<server>/uploads` |
| `SCANNER_SCHEDULER` | Hintergrund-Scraper, Standard an, nur `false` schaltet ab |
| `SCANNER_SCHEDULER_OFFSET` | Minuten Versatz für den ersten Lauf, seit dem Rückbau der alten App 0 |
| `MS_TENANT_ID`, `MS_CLIENT_ID`, `MS_CLIENT_SECRET` | App-only-Zugriff auf Microsoft Graph |
| `MS_MAILBOX` | abzugleichendes Postfach, Standard `crm@perinco.de` |
| `MS_MAIL_FOLDER` | Ordner, Standard `inbox` |
| `MAIL_SYNC` | Postfachabgleich, Standard an, nur `false` schaltet ab |
| `MAIL_MAX_PAGES`, `MAIL_MAX_BODY_CHARS` | Bremsen für Erstabgleich und Textgröße |
| `MS_SSO_ENABLED` | Anmeldung über Microsoft, Standard aus, nur `true` schaltet ein |
| `MS_SSO_REDIRECT_URI` | Umleitungsadresse, zeichengenau wie in Entra eingetragen |
| `MS_SSO_ALLOWED_DOMAINS` | Positivliste von E-Mail-Domains, leer = keine zusätzliche Prüfung |
| `CRM_ENCRYPTION_KEY` | verschlüsselt das in der Oberfläche hinterlegte Client-Secret; fehlt er, dient `JWT_SECRET` als Schlüssel |
Die Scanner-Datenbank wird beim Start automatisch angelegt
(`CREATE DATABASE IF NOT EXISTS`), Tabellen kommen aus `sequelize.sync()`, Portale
und Standardeinstellungen werden geseedet.
Fehlen die `MS_*`-Werte, startet das Backend normal und protokolliert
`[Mail] Zugangsdaten fehlen`. Die Ansicht *Mails* und der Einstellungsreiter
*Postfach* nennen die fehlende Variable beim Namen. Einrichtung in Entra und
Exchange Online: [11 Postfachanbindung](11-postfach.md).
> Die `MS_*`-Werte werden **beim Start** gelesen. Nach einer Änderung an der
> `.env` muss der Dienst neu starten, ein Speichern in der Oberfläche genügt
> nicht - dort liegen nur Intervall, Schalter und Zeitraum (`crm_settings`).
Die `MS_SSO_*`-Werte gehören zur Anmeldung über Microsoft und nutzen dieselbe
App-Registrierung wie das Postfach. Ohne sie bleibt es bei Benutzername und
Passwort. Einrichtung: [13 Anmeldung über Microsoft](13-anmeldung-sso.md).
`MS_CLIENT_SECRET` ist seit dem 21.09.2026 nur noch die Rückfallebene: Vorrang
hat das Secret, das Administratoren unter *Einstellungen → Anmeldung* eintragen.
Es liegt verschlüsselt in `crm_settings`, wird bei jedem Bedarf frisch gelesen
und braucht deshalb **keinen** Neustart.
## Playwright
`playwright` ist auf **exakt 1.60.0** gepinnt. Der passende Chromium-Build
(`chromium-1223`) liegt in `/root/.cache/ms-playwright` - dieser Cache stammt aus
der Installation der alten App und hat deren Rückbau überlebt, weil beide Apps
ihn geteilt haben und `PLAYWRIGHT_BROWSERS_PATH` nirgends gesetzt ist. Ein `^` in
der Version würde einen zweiten Browser-Download ziehen; die Begründung steht als
`comments`-Feld in `server/package.json`.
> **Beim Aufräumen nicht anfassen:** `/root/.cache/ms-playwright` (641 MB) liegt
> ausserhalb beider Anwendungsverzeichnisse. Wer ihn mit löscht, nimmt dem
> Scanner den Browser - Oberfläche und Treffer bleiben, Scan-Lauf und
> Verbindungstest fallen aus.
Fehlt der Browser, funktioniert alles ausser Scan-Lauf und Verbindungstest:
```bash
cd /var/www/perinco_CRM/server && npx playwright install chromium
```
## Erststart des Scanners
```bash
bash /var/www/perinco_CRM/deploy/scanner-setup.sh # .env ergänzen, Schlüssel erzeugen
systemctl restart perinco-crm-backend
```
Datenbank und Tabellen legt der Start selbst an, Portale und
Standardeinstellungen werden geseedet. Agenten, Zugangsdaten und Cookies kommen
danach über die Oberfläche. Das frühere `npm run scanner:import` holte diese
Daten aus der Datenbank der alten App - beides ist mit dem Rückbau am 09.09.2026
entfallen.
## Störungssuche
| Symptom | Blick darauf |
| --- | --- |
| Anmeldung schlägt fehl | `journalctl -u perinco-crm-backend -n 50`, `JWT_SECRET` gesetzt? |
| Scanner-Reiter leer, 500er | `[Scanner] Datenbank nicht erreichbar` im Journal |
| Kein Treffer trotz Scan | `scanning_active`, aktive Agenten mit CV, OpenAI-Key gesetzt |
| Portal `expired` | Cookies neu importieren (Cookie-Editor) |
| `Target page ... has been closed` | Dienst wurde mitten im Scrapen neu gestartet |
| Backend stirbt beim Scrapen | `MemoryMax` zu klein, `systemctl show ... -p MemoryCurrent` |
| Ton bleibt aus | Schwellenwert, Dashboard-Ansicht offen, einmal auf der Seite geklickt? |
| Keine Mails im Postfachreiter | `journalctl -u perinco-crm-backend \| grep '\[Mail\]'`, dann Verbindungstest in den Einstellungen |
| Mail-Abgleich meldet `ErrorAccessDenied` | Administratorzustimmung für `Mail.Read` fehlt, oder die ApplicationAccessPolicy erlaubt das Postfach nicht |
Nützliche Abfragen:
```bash
systemctl status perinco-crm-backend
journalctl -u perinco-crm-backend -f
curl -s http://127.0.0.1:5006/api/health
set -a; . /var/www/perinco_CRM/server/.env; set +a
mysql -u"$DB_USER" -p"$DB_PASSWORD" "$SCANNER_DB_NAME" -e \
"SELECT COUNT(*) FROM matches; SELECT key_name, value FROM settings"
# Zustand des Postfachabgleichs
mysql -u"$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" -e \
"SELECT mailbox, has_more, last_run_at, last_success_at, last_error FROM crm_mail_sync_state;
SELECT COUNT(*) AS mails FROM crm_mail_messages;
SELECT status, COUNT(*) FROM crm_mail_contact_candidates GROUP BY status"
```
## Sicherung
Beide Datenbanken einzeln, dazu die CV-Dateien:
```bash
set -a; . /var/www/perinco_CRM/server/.env; set +a
mysqldump -u"$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" > crm_$(date +%F).sql
mysqldump -u"$DB_USER" -p"$DB_PASSWORD" "$SCANNER_DB_NAME" > scanner_$(date +%F).sql
tar czf uploads_$(date +%F).tar.gz -C /var/www/perinco_CRM/server uploads
```
Die `.env` gehört **nicht** ins Repository, aber in die Sicherung - ohne
`SCANNER_ENCRYPTION_KEY` sind die Portal-Passwörter verloren. Genau das ist der
alten App passiert.