# 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, KI, 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
```