Files
ICON-CRM/docs/01-architektur.md

133 lines
5.2 KiB
Markdown
Raw Permalink 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 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<br/>icrm + crm.icon-experts.de"]
A -->|"/"| D["dist/<br/>SPA, FallbackResource"]
A -->|"/api"| E["Express :5007"]
E --> C[("icon_CRM")]
W(("Besucher")) -->|"HTTPS"| B["Apache :443<br/>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<br/>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,<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"]
```
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` (+ Alias `crm.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
```