KI-Einstellungen: Schluessel und Modell in der Oberflaeche

This commit is contained in:
2026-09-14 16:06:37 +02:00
parent 91fc8d1819
commit 96ab707bfc
+132 -132
View File
@@ -1,132 +1,132 @@
# 01 Architektur # 01 Architektur
## Bausteine ## Bausteine
Drei Teile; keiner spricht direkt mit einem anderen außer über HTTP bzw. SQL: Drei Teile; keiner spricht direkt mit einem anderen außer über HTTP bzw. SQL:
| Teil | Technik | Wo | | Teil | Technik | Wo |
| --- | --- | --- | | --- | --- | --- |
| Frontend | React 19, TypeScript, Tailwind 4, Vite 6 | statisches Build in `/var/www/icon_CRM/dist` | | Frontend | React 19, TypeScript, Tailwind 4, Vite 6 | statisches Build in `/var/www/icon_CRM/dist` |
| Backend | Express 4, mysql2, JWT, bcrypt | `/var/www/icon_CRM/server`, `127.0.0.1:5007` | | Backend | Express 4, mysql2, JWT, bcrypt | `/var/www/icon_CRM/server`, `127.0.0.1:5007` |
| Datenbank | MariaDB | `icon_CRM` | | Datenbank | MariaDB | `icon_CRM` |
Der Browser spricht **nie** direkt mit MariaDB – deshalb überhaupt ein Backend. Der Browser spricht **nie** direkt mit MariaDB – deshalb überhaupt ein Backend.
Es bindet ausschließlich auf `127.0.0.1` und ist von außen nur über den Es bindet ausschließlich auf `127.0.0.1` und ist von außen nur über den
Apache-Reverse-Proxy erreichbar. Apache-Reverse-Proxy erreichbar.
Das ersetzt den vorherigen Aufbau: dort lag der Supabase-Anon-Key im Das ersetzt den vorherigen Aufbau: dort lag der Supabase-Anon-Key im
Browser-Bundle und hatte vollen Lesezugriff auf alle Tabellen, `icon_users` Browser-Bundle und hatte vollen Lesezugriff auf alle Tabellen, `icon_users`
samt Passwort-Hashes eingeschlossen. samt Passwort-Hashes eingeschlossen.
## Topologie ## Topologie
```mermaid ```mermaid
flowchart LR flowchart LR
U(("Browser")) -->|"HTTPS"| A["Apache :443<br/>icrm.icon-experts.de"] U(("Browser")) -->|"HTTPS"| A["Apache :443<br/>icrm.icon-experts.de"]
A -->|"/"| D["dist/<br/>SPA, FallbackResource"] A -->|"/"| D["dist/<br/>SPA, FallbackResource"]
A -->|"/api"| E["Express :5007"] A -->|"/api"| E["Express :5007"]
E --> C[("icon_CRM")] E --> C[("icon_CRM")]
W(("Besucher")) -->|"HTTPS"| B["Apache :443<br/>icon-experts.de"] W(("Besucher")) -->|"HTTPS"| B["Apache :443<br/>icon-experts.de"]
B -->|"/"| WD["icon_website/dist"] B -->|"/"| WD["icon_website/dist"]
B -->|"/api/public"| E B -->|"/api/public"| E
``` ```
Beide Vhosts liegen auf demselben Rechner. Die Website reicht **nur** Beide Vhosts liegen auf demselben Rechner. Die Website reicht **nur**
`/api/public` durch – Anmeldung, Kontakte und Leads sind von dort aus `/api/public` durch – Anmeldung, Kontakte und Leads sind von dort aus
unerreichbar. Der Umweg über den eigenen Vhost ist Absicht: so bleibt der unerreichbar. Der Umweg über den eigenen Vhost ist Absicht: so bleibt der
Aufruf gleicher Herkunft und die Content-Security-Policy der Website kommt Aufruf gleicher Herkunft und die Content-Security-Policy der Website kommt
weiter mit `connect-src 'self'` aus. weiter mit `connect-src 'self'` aus.
## Ausgehende Verbindungen ## Ausgehende Verbindungen
Das Backend spricht von sich aus mit drei fremden Diensten. Alle sind optional – Das Backend spricht von sich aus mit drei fremden Diensten. Alle sind optional –
fehlen die Zugangsdaten oder ist das Ziel nicht erreichbar, läuft die Anwendung fehlen die Zugangsdaten oder ist das Ziel nicht erreichbar, läuft die Anwendung
ohne den jeweiligen Teil weiter. ohne den jeweiligen Teil weiter.
| Ziel | Wofür | Ausgelöst von | | Ziel | Wofür | Ausgelöst von |
| --- | --- | --- | | --- | --- | --- |
| `api.openai.com` | CV-Auslesung, Schreibhilfe, erweiterte Suche | Benutzer, jeweils auf Klick | | `api.openai.com` | CV-Auslesung, Schreibhilfe, erweiterte Suche | Benutzer, jeweils auf Klick |
| `pcrm.perinco.de` | Kontakte des perinco CRM lesen | Schalter „perinco" in den Kontakten | | `pcrm.perinco.de` | Kontakte des perinco CRM lesen | Schalter „perinco" in den Kontakten |
| `linkedin.com`, `media.licdn.com` | Profilfoto zu einem Kontakt holen | Knopf im Kontaktformular | | `linkedin.com`, `media.licdn.com` | Profilfoto zu einem Kontakt holen | Knopf im Kontaktformular |
Das Diktat taucht in dieser Liste **nicht** auf: es lädt sein Modell direkt aus Das Diktat taucht in dieser Liste **nicht** auf: es lädt sein Modell direkt aus
dem Browser vom Hugging-Face-CDN, das Backend ist daran nicht beteiligt (siehe dem Browser vom Hugging-Face-CDN, das Backend ist daran nicht beteiligt (siehe
[04 Diktat](04-diktat.md)). [04 Diktat](04-diktat.md)).
## Anfrageweg mit Anmeldung ## Anfrageweg mit Anmeldung
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
actor B as Browser actor B as Browser
participant A as Apache participant A as Apache
participant E as Express participant E as Express
participant M as MariaDB participant M as MariaDB
B->>A: POST /api/auth/login B->>A: POST /api/auth/login
A->>E: weiterleiten A->>E: weiterleiten
E->>M: SELECT icon_users WHERE username OR email E->>M: SELECT icon_users WHERE username OR email
M-->>E: Zeile mit Hash M-->>E: Zeile mit Hash
E->>E: bcrypt.compare (SHA-256 des Altbestands wird dabei umgeschrieben) E->>E: bcrypt.compare (SHA-256 des Altbestands wird dabei umgeschrieben)
E-->>B: { token (JWT, 12 h), user } E-->>B: { token (JWT, 12 h), user }
Note over B: Token im localStorage Note over B: Token im localStorage
B->>A: GET /api/contacts<br/>Authorization: Bearer ... B->>A: GET /api/contacts<br/>Authorization: Bearer ...
A->>E: weiterleiten A->>E: weiterleiten
E->>E: requireAuth: JWT pruefen E->>E: requireAuth: JWT pruefen
E->>M: SELECT icon_users WHERE id (noch aktiv?) E->>M: SELECT icon_users WHERE id (noch aktiv?)
E->>M: SELECT contacts JOIN companies E->>M: SELECT contacts JOIN companies
M-->>E: Zeilen M-->>E: Zeilen
E-->>B: JSON E-->>B: JSON
``` ```
Bei `401` löscht das Frontend das Token und springt auf den Anmeldeschirm Bei `401` löscht das Frontend das Token und springt auf den Anmeldeschirm
(`setUnauthorizedHandler` in `src/lib/api.ts`). Das Token wird bei jeder Anfrage (`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, neu gegen die Datenbank geprüft – ein deaktivierter Benutzer ist sofort draußen,
ohne auf den Ablauf des Tokens zu warten. ohne auf den Ablauf des Tokens zu warten.
**Passwörter aus dem Altbestand:** im alten CRM lagen sie als ungesalzenes **Passwörter aus dem Altbestand:** im alten CRM lagen sie als ungesalzenes
SHA-256. Diese Hashes sind an ihrer Form erkennbar (64 Hexziffern) und werden SHA-256. Diese Hashes sind an ihrer Form erkennbar (64 Hexziffern) und werden
beim ersten erfolgreichen Login auf bcrypt umgeschrieben. Neu vergeben wird beim ersten erfolgreichen Login auf bcrypt umgeschrieben. Neu vergeben wird
ausschließlich bcrypt. ausschließlich bcrypt.
## Rollen ## Rollen
```mermaid ```mermaid
flowchart TD flowchart TD
L["Anmeldung"] --> R{"Rolle"} L["Anmeldung"] --> R{"Rolle"}
R -->|admin| A["Dashboard, Leads, Kontakte, Unternehmen, Ausschreibungen,<br/>Laufende Projekte, Historie<br/>Einstellungen: Nutzerverwaltung, Stammdaten, Mein Konto, Diktat"] R -->|admin| A["Dashboard, Leads, Kontakte, Unternehmen, Ausschreibungen,<br/>Laufende Projekte, Historie<br/>Einstellungen: Nutzerverwaltung, Stammdaten, KI, Mein Konto, Diktat"]
R -->|user| U["Dashboard, Leads, Kontakte, Unternehmen, Ausschreibungen,<br/>Laufende Projekte, Historie<br/>Einstellungen: Mein Konto, Diktat"] R -->|user| U["Dashboard, Leads, Kontakte, Unternehmen, Ausschreibungen,<br/>Laufende Projekte, Historie<br/>Einstellungen: Mein Konto, Diktat"]
``` ```
Die Prüfung sitzt **doppelt**: das Menü blendet aus, was nicht erlaubt ist, und Die Prüfung sitzt **doppelt**: das Menü blendet aus, was nicht erlaubt ist, und
das Backend weist zusätzlich mit `403` ab (`requireAdmin`). Ausnahme ist das Backend weist zusätzlich mit `403` ab (`requireAdmin`). Ausnahme ist
`GET /api/users/directory` – die Benutzerliste brauchen alle Rollen für `GET /api/users/directory` – die Benutzerliste brauchen alle Rollen für
Erwähnungen, Zuständigkeiten und die Farben in der Historie. Passwort-Hashes Erwähnungen, Zuständigkeiten und die Farben in der Historie. Passwort-Hashes
liefert der Endpunkt nie. liefert der Endpunkt nie.
Ein Sicherheitsnetz im Backend verhindert, dass der letzte aktive Administrator Ein Sicherheitsnetz im Backend verhindert, dass der letzte aktive Administrator
degradiert, deaktiviert oder gelöscht wird. degradiert, deaktiviert oder gelöscht wird.
Der Reiter *Diktat* ist bewusst für jede Rolle sichtbar: jeder richtet die Der Reiter *Diktat* ist bewusst für jede Rolle sichtbar: jeder richtet die
Spracherkennung auf seinem eigenen Gerät ein, mit seinem eigenen Download. Spracherkennung auf seinem eigenen Gerät ein, mit seinem eigenen Download.
## Belegte Ports ## Belegte Ports
| Port | Dienst | | Port | Dienst |
| --- | --- | | --- | --- |
| 5007 | ICON CRM Backend (nur `127.0.0.1`) | | 5007 | ICON CRM Backend (nur `127.0.0.1`) |
| 3101 | Vite-Dev-Server (nur lokal beim Entwickeln) | | 3101 | Vite-Dev-Server (nur lokal beim Entwickeln) |
| 80/443 | Apache: `icrm.icon-experts.de`, `icon-experts.de`, `www.icon-experts.de` | | 80/443 | Apache: `icrm.icon-experts.de`, `icon-experts.de`, `www.icon-experts.de` |
## Was wo liegt ## Was wo liegt
``` ```
/var/www/icon_CRM/dist SPA (Build) /var/www/icon_CRM/dist SPA (Build)
/var/www/icon_CRM/server Backend samt .env (chmod 600) /var/www/icon_CRM/server Backend samt .env (chmod 600)
/var/www/icon_CRM/uploads hochgeladene Lebenslaeufe /var/www/icon_CRM/uploads hochgeladene Lebenslaeufe
/var/www/icon_website/dist ICON Website /var/www/icon_website/dist ICON Website
/etc/systemd/system/icon-crm-backend.service /etc/systemd/system/icon-crm-backend.service
/etc/apache2/sites-available/icrm.icon-experts.de*.conf /etc/apache2/sites-available/icrm.icon-experts.de*.conf
/etc/apache2/sites-available/icon-experts.de*.conf /etc/apache2/sites-available/icon-experts.de*.conf
``` ```