From e0a74a0c0347c4b5da96d34d064cf417cd984d05 Mon Sep 17 00:00:00 2001 From: Sascha Date: Mon, 14 Sep 2026 15:34:08 +0200 Subject: [PATCH] Dokumentation: docs/01-architektur.md --- docs/01-architektur.md | 132 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 docs/01-architektur.md diff --git a/docs/01-architektur.md b/docs/01-architektur.md new file mode 100644 index 0000000..d9a71aa --- /dev/null +++ b/docs/01-architektur.md @@ -0,0 +1,132 @@ +# 01 Architektur + +## Bausteine + +Drei Teile; keiner spricht direkt mit einem anderen außer über HTTP bzw. SQL: + +| Teil | Technik | Wo | +| --- | --- | --- | +| 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` | +| Datenbank | MariaDB | `icon_CRM` | + +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 +Apache-Reverse-Proxy erreichbar. + +Das ersetzt den vorherigen Aufbau: dort lag der Supabase-Anon-Key im +Browser-Bundle und hatte vollen Lesezugriff auf alle Tabellen, `icon_users` +samt Passwort-Hashes eingeschlossen. + +## Topologie + +```mermaid +flowchart LR + U(("Browser")) -->|"HTTPS"| A["Apache :443
icrm.icon-experts.de"] + A -->|"/"| D["dist/
SPA, FallbackResource"] + A -->|"/api"| E["Express :5007"] + E --> C[("icon_CRM")] + + W(("Besucher")) -->|"HTTPS"| B["Apache :443
icon-experts.de"] + B -->|"/"| WD["icon_website/dist"] + B -->|"/api/public"| E +``` + +Beide Vhosts liegen auf demselben Rechner. Die Website reicht **nur** +`/api/public` durch – Anmeldung, Kontakte und Leads sind von dort aus +unerreichbar. Der Umweg über den eigenen Vhost ist Absicht: so bleibt der +Aufruf gleicher Herkunft und die Content-Security-Policy der Website kommt +weiter mit `connect-src 'self'` aus. + +## Ausgehende Verbindungen + +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 +ohne den jeweiligen Teil weiter. + +| Ziel | Wofür | Ausgelöst von | +| --- | --- | --- | +| `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 | +| `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 +dem Browser vom Hugging-Face-CDN, das Backend ist daran nicht beteiligt (siehe +[04 Diktat](04-diktat.md)). + +## 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 icon_users WHERE username OR email + M-->>E: Zeile mit Hash + E->>E: bcrypt.compare (SHA-256 des Altbestands wird dabei umgeschrieben) + E-->>B: { token (JWT, 12 h), user } + Note over B: Token im localStorage + + B->>A: GET /api/contacts
Authorization: Bearer ... + A->>E: weiterleiten + E->>E: requireAuth: JWT pruefen + E->>M: SELECT icon_users WHERE id (noch aktiv?) + E->>M: SELECT contacts JOIN companies + M-->>E: Zeilen + E-->>B: JSON +``` + +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 +neu gegen die Datenbank geprüft – ein deaktivierter Benutzer ist sofort draußen, +ohne auf den Ablauf des Tokens zu warten. + +**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 +beim ersten erfolgreichen Login auf bcrypt umgeschrieben. Neu vergeben wird +ausschließlich bcrypt. + +## Rollen + +```mermaid +flowchart TD + L["Anmeldung"] --> R{"Rolle"} + R -->|admin| A["Dashboard, Leads, Kontakte, Unternehmen, Ausschreibungen,
Laufende Projekte, Historie
Einstellungen: Nutzerverwaltung, Stammdaten, Mein Konto, Diktat"] + R -->|user| U["Dashboard, Leads, Kontakte, Unternehmen, Ausschreibungen,
Laufende Projekte, Historie
Einstellungen: Mein Konto, Diktat"] +``` + +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 +`GET /api/users/directory` – die Benutzerliste brauchen alle Rollen für +Erwähnungen, Zuständigkeiten und die Farben in der Historie. Passwort-Hashes +liefert der Endpunkt nie. + +Ein Sicherheitsnetz im Backend verhindert, dass der letzte aktive Administrator +degradiert, deaktiviert oder gelöscht wird. + +Der Reiter *Diktat* ist bewusst für jede Rolle sichtbar: jeder richtet die +Spracherkennung auf seinem eigenen Gerät ein, mit seinem eigenen Download. + +## Belegte Ports + +| Port | Dienst | +| --- | --- | +| 5007 | ICON CRM Backend (nur `127.0.0.1`) | +| 3101 | Vite-Dev-Server (nur lokal beim Entwickeln) | +| 80/443 | Apache: `icrm.icon-experts.de`, `icon-experts.de`, `www.icon-experts.de` | + +## Was wo liegt + +``` +/var/www/icon_CRM/dist SPA (Build) +/var/www/icon_CRM/server Backend samt .env (chmod 600) +/var/www/icon_CRM/uploads hochgeladene Lebenslaeufe +/var/www/icon_website/dist ICON Website +/etc/systemd/system/icon-crm-backend.service +/etc/apache2/sites-available/icrm.icon-experts.de*.conf +/etc/apache2/sites-available/icon-experts.de*.conf +```