Drei Verbesserungen an den Kontakten: * Beim Anlegen ist der angemeldete Benutzer voreingestellt zustaendig. Bisher stand dort der erste Benutzer der Liste - fast immer falsch. * Wird eine LinkedIn-Adresse hinterlegt, holt das CRM das oeffentliche Profilfoto und setzt es als Kontaktfoto: beim Verlassen des Feldes einmal je Adresse, solange kein Foto gesetzt ist, und auf Klick ueber den Knopf daneben. Der Abruf laeuft ueber das Backend (server/src/linkedinPhoto.js, POST /api/leads/linkedin-photo), weil ihn die Same-Origin-Policy im Browser verhindert. Gelesen wird die Open-Graph-Vorschau der Profilseite; Herkunft, Groesse und Antwortzeit des Bildes werden geprueft, Platzhalter-Silhouetten abgewiesen. Schlaegt der Abruf fehl, steht der Grund unter dem Feld. * In der Netzwerkansicht hebt ein Klick auf freie Flaeche die Auswahl auf - das Diagramm sieht danach aus wie beim Wechsel in die Ansicht. Ein Ziehen des Hintergrunds (ab 4 px) laesst die Auswahl stehen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
119 lines
4.5 KiB
Markdown
119 lines
4.5 KiB
Markdown
# 01 Architektur
|
|
|
|
## Bausteine
|
|
|
|
Drei Teile, keiner davon spricht direkt mit einem anderen ausser über HTTP bzw.
|
|
SQL:
|
|
|
|
| Teil | Technik | Wo |
|
|
| --- | --- | --- |
|
|
| Frontend | React 19, TypeScript, Tailwind 4, Vite | statisches Build in `/var/www/perinco_CRM/dist` |
|
|
| Backend | Express 4, mysql2 (CRM), Sequelize (Scanner) | `/var/www/perinco_CRM/server`, `127.0.0.1:5006` |
|
|
| Datenbank | MariaDB 10.11 | `perinco_CRM` und `perinco_scanner` |
|
|
|
|
Der Browser spricht **nie** direkt mit MariaDB - deshalb überhaupt ein Backend.
|
|
Das Backend bindet ausschließlich auf `127.0.0.1` und ist von außen nur über
|
|
den Apache-Reverse-Proxy erreichbar.
|
|
|
|
## Topologie
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
U(("Browser")) -->|"HTTPS"| A["Apache :443<br/>pcrm.perinco.de"]
|
|
A -->|"/"| D["dist/<br/>SPA, FallbackResource"]
|
|
A -->|"/api"| E["Express :5006"]
|
|
E --> C[("perinco_CRM")]
|
|
E --> S[("perinco_scanner")]
|
|
```
|
|
|
|
Der Scanner im CRM hängt unter `/api/scanner` und braucht damit **keine eigene
|
|
Apache-Regel**: alles läuft über den bestehenden `/api`-Proxy. Dasselbe gilt für
|
|
die Postfachanbindung unter `/api/mail` (siehe
|
|
[11 Postfachanbindung](11-postfach.md)).
|
|
|
|
## Ausgehende Verbindungen
|
|
|
|
Das Backend spricht von sich aus mit vier 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 |
|
|
| --- | --- | --- |
|
|
| Projektbörsen | Scraping der Projektangebote | Scanner-Scheduler |
|
|
| `api.openai.com` | Bewertung Lebenslauf gegen Projekt | Scanner-Pipeline |
|
|
| `login.microsoftonline.com`, `graph.microsoft.com` | Postfach `crm@perinco.de` lesen | Mail-Scheduler |
|
|
| `linkedin.com`, `media.licdn.com` | Profilfoto zu einem Kontakt holen | Benutzer im Kontaktformular |
|
|
|
|
Der LinkedIn-Abruf ist der einzige, den ein Benutzer direkt auslöst: Er liest die
|
|
öffentliche Vorschau (`og:image`) einer Profilseite und lädt das Bild vom
|
|
LinkedIn-CDN. Keine Anmeldung, kein Zugriff auf geschützte Inhalte - schlägt der
|
|
Abruf fehl, bleibt es beim Hochladen von Hand.
|
|
|
|
Die Postfachanbindung schickt **keine** Mailinhalte an Dritte: Kontaktdaten
|
|
werden auf dem Server aus Graph-Feldern und Signaturen gelesen, ohne
|
|
Sprachmodell.
|
|
|
|
## 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 crm_users WHERE username
|
|
M-->>E: Zeile mit bcrypt-Hash
|
|
E->>E: bcrypt.compare
|
|
E-->>B: { token (JWT, 12 h), user }
|
|
Note over B: Token im localStorage
|
|
|
|
B->>A: GET /api/scanner/matches<br/>Authorization: Bearer ...
|
|
A->>E: weiterleiten
|
|
E->>E: requireAuth: JWT pruefen
|
|
E->>M: SELECT crm_users WHERE id (ist der noch aktiv?)
|
|
E->>M: SELECT matches JOIN agents, projects
|
|
M-->>E: Treffer
|
|
E-->>B: JSON
|
|
```
|
|
|
|
Bei `401` löscht das Frontend das Token und springt zurück 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.
|
|
|
|
## Rollen
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
L["Anmeldung"] --> R{"Rolle"}
|
|
R -->|admin| A["Dashboard, Akquise, Opportunities, Interviews, Kontakte, Mails, Scanner<br/>Einstellungen: Nutzerverwaltung, Mein Konto, Akquise, Postfach, Scanner, Whisper"]
|
|
R -->|user| U["Dashboard, Akquise, Opportunities, Interviews, Kontakte, Mails, Scanner<br/>Einstellungen: Mein Konto, Akquise, Whisper"]
|
|
```
|
|
|
|
Bei den Mails darf jede Rolle lesen und zuordnen - `crm@perinco.de` ist ein
|
|
gemeinsames Vertriebspostfach. Einrichten, Verbindungstest und manueller Abgleich
|
|
sind Administratoren vorbehalten.
|
|
|
|
Die Rollenprüfung sitzt **doppelt**: das Menü blendet aus, was nicht erlaubt
|
|
ist, und das Backend weist Zugriffe zusätzlich mit `403` ab (`requireAdmin`).
|
|
Ausnahme ist `GET /api/users/directory` - die Benutzerliste brauchen alle Rollen
|
|
für Netzwerkdiagramm, Zuständigkeiten, @-Mentions, Teilnehmerauswahl und
|
|
Terminfarben. Passwort-Hashes liefert der Endpunkt nie.
|
|
|
|
Ein Sicherheitsnetz im Backend verhindert, dass der letzte aktive Administrator
|
|
degradiert, deaktiviert oder gelöscht wird.
|
|
|
|
## Belegte Ports auf dem Server
|
|
|
|
Der Rechner hostet weitere Dienste (n8n, Docker, ~15 Apache-Vhosts). Vor jeder
|
|
Portbelegung `ss -tlnp` prüfen. Relevant hier:
|
|
|
|
| Port | Dienst |
|
|
| --- | --- |
|
|
| 5006 | perinco CRM Backend (nur `127.0.0.1`) |
|
|
| 3000 | weitere Vite-App |
|
|
| 5678 | n8n |
|