Files
perinco-CRM/docs/01-architektur.md
T
Sascha WolfandClaude Opus 5.5 e35c7518db feat(ki): Einstellungen -> KI mit OpenAI-Schluessel und Modellwahl
Neuer Admin-Reiter "KI": zeigt die letzten vier Zeichen des hinterlegten
OpenAI-Schluessels samt Quelle, nimmt einen neuen an (vorher gegen
/v1/models geprueft) und laesst je Einsatzzweck das Modell per Dropdown
waehlen - Scanner-Bewertung, Abgleich "bereits beworben", erweiterte Suche.
Die Auswahl kommt live von OpenAI, Vorgaben sind die bisher fest
verdrahteten Modelle. Reasoning-Modelle bekommen reasoning_effort statt
temperature, damit ein Wechsel keinen 400er ausloest.

Schluesselfeld aus dem Scanner-Reiter entfernt; Doku in docs/14-ki.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 10:31:55 +02:00

127 lines
5.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 01 Architektur
## Bausteine
Drei Teile, keiner davon spricht direkt mit einem anderen ausser über HTTP bzw.
SQL:
| Teil | Technik | Wo |
| --- | --- | --- |
| Frontend | React 19, TypeScript, Tailwind 4, Vite | statisches Build in `/var/www/perinco_CRM/dist` |
| Backend | Express 4, mysql2 (CRM), Sequelize (Scanner) | `/var/www/perinco_CRM/server`, `127.0.0.1:5006` |
| Datenbank | MariaDB 10.11 | `perinco_CRM` und `perinco_scanner` |
Der Browser spricht **nie** direkt mit MariaDB - deshalb überhaupt ein Backend.
Das Backend bindet ausschließlich auf `127.0.0.1` und ist von außen nur über
den Apache-Reverse-Proxy erreichbar. Das CRM hört auf `pcrm.perinco.de` und
seit 16.09.2026 auch auf `crm.perinco.de` (`ServerAlias` im selben Vhost, beide
Namen im selben Let’s-Encrypt-Zertifikat).
## Topologie
```mermaid
flowchart LR
U(("Browser")) -->|"HTTPS"| A["Apache :443<br/>pcrm.perinco.de<br/>crm.perinco.de"]
A -->|"/"| D["dist/<br/>SPA, FallbackResource"]
A -->|"/api"| E["Express :5006"]
E --> C[("perinco_CRM")]
E --> S[("perinco_scanner")]
```
Der Scanner im CRM hängt unter `/api/scanner` und braucht damit **keine eigene
Apache-Regel**: alles läuft über den bestehenden `/api`-Proxy. Dasselbe gilt für
die Postfachanbindung unter `/api/mail` (siehe
[11 Postfachanbindung](11-postfach.md)).
## Ausgehende Verbindungen
Das Backend spricht von sich aus mit vier fremden Diensten. Alle sind
optional - fehlen die Zugangsdaten oder ist das Ziel nicht erreichbar, läuft die
Anwendung ohne den jeweiligen Teil weiter.
| Ziel | Wofür | Ausgelöst von |
| --- | --- | --- |
| Projektbörsen | Scraping der Projektangebote | Scanner-Scheduler |
| `api.openai.com` | Bewertung Lebenslauf gegen Projekt | Scanner-Pipeline |
| `login.microsoftonline.com`, `graph.microsoft.com` | Postfach `crm@perinco.de` lesen | Mail-Scheduler |
| `linkedin.com`, `media.licdn.com` | Profilfoto zu einem Kontakt holen | Benutzer im Kontaktformular |
Der LinkedIn-Abruf ist der einzige, den ein Benutzer direkt auslöst: Er liest die
öffentliche Vorschau (`og:image`) einer Profilseite und lädt das Bild vom
LinkedIn-CDN. Keine Anmeldung, kein Zugriff auf geschützte Inhalte - schlägt der
Abruf fehl, bleibt es beim Hochladen von Hand.
Die Postfachanbindung schickt **keine** Mailinhalte an Dritte: Kontaktdaten
werden auf dem Server aus Graph-Feldern und Signaturen gelesen, ohne
Sprachmodell.
## Anfrageweg mit Anmeldung
```mermaid
sequenceDiagram
actor B as Browser
participant A as Apache
participant E as Express
participant M as MariaDB
B->>A: POST /api/auth/login
A->>E: weiterleiten
E->>M: SELECT crm_users WHERE username
M-->>E: Zeile mit bcrypt-Hash
E->>E: bcrypt.compare
E-->>B: { token (JWT, 12 h), user }
Note over B: Token im localStorage
B->>A: GET /api/scanner/matches<br/>Authorization: Bearer ...
A->>E: weiterleiten
E->>E: requireAuth: JWT pruefen
E->>M: SELECT crm_users WHERE id (ist der noch aktiv?)
E->>M: SELECT matches JOIN agents, projects
M-->>E: Treffer
E-->>B: JSON
```
Bei `401` löscht das Frontend das Token und springt zurück auf den
Anmeldeschirm (`setUnauthorizedHandler` in `src/lib/api.ts`). Das Token wird bei
jeder Anfrage neu gegen die Datenbank geprüft - ein deaktivierter Benutzer ist
sofort draußen, ohne auf den Ablauf des Tokens zu warten.
Der zweite Weg zu demselben Token ist die Anmeldung über Microsoft Entra: statt
`bcrypt.compare` steht dort der OIDC-Ablauf mit Microsoft, danach geht es
identisch weiter - dasselbe JWT, dieselbe Prüfung bei jeder Anfrage. Wer kein
CRM-Konto hat, kommt auch mit gültigem Microsoft-Konto nicht hinein.
Einzelheiten: [13 Anmeldung über Microsoft](13-anmeldung-sso.md).
## Rollen
```mermaid
flowchart TD
L["Anmeldung"] --> R{"Rolle"}
R -->|admin| A["Dashboard, Akquise, Opportunities, Interviews, Kontakte, Mails, Scanner<br/>Einstellungen: Nutzerverwaltung, Mein Konto, Akquise, Postfach, Anmeldung, Scanner, KI, Whisper"]
R -->|user| U["Dashboard, Akquise, Opportunities, Interviews, Kontakte, Mails, Scanner<br/>Einstellungen: Mein Konto, Akquise, Whisper"]
```
Bei den Mails darf jede Rolle lesen und zuordnen - `crm@perinco.de` ist ein
gemeinsames Vertriebspostfach. Einrichten, Verbindungstest und manueller Abgleich
sind Administratoren vorbehalten.
Die Rollenprüfung sitzt **doppelt**: das Menü blendet aus, was nicht erlaubt
ist, und das Backend weist Zugriffe zusätzlich mit `403` ab (`requireAdmin`).
Ausnahme ist `GET /api/users/directory` - die Benutzerliste brauchen alle Rollen
für Netzwerkdiagramm, Zuständigkeiten, @-Mentions, Teilnehmerauswahl und
Terminfarben. Passwort-Hashes liefert der Endpunkt nie.
Ein Sicherheitsnetz im Backend verhindert, dass der letzte aktive Administrator
degradiert, deaktiviert oder gelöscht wird.
## Belegte Ports auf dem Server
Der Rechner hostet weitere Dienste (n8n, Docker, ~15 Apache-Vhosts). Vor jeder
Portbelegung `ss -tlnp` prüfen. Relevant hier:
| Port | Dienst |
| --- | --- |
| 5006 | perinco CRM Backend (nur `127.0.0.1`) |
| 3000 | weitere Vite-App |
| 5678 | n8n |