133 lines
5.0 KiB
Markdown
133 lines
5.0 KiB
Markdown
# 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.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, 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`, `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
|
||
```
|