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