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

7.8 KiB

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.

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"]
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 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.

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.

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:

cd /var/www/perinco_CRM/server && npx playwright install chromium

Erststart des Scanners

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:

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:

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.