d8f46abe0b
- Old code archived on archive/legacy-v0 and feat/T1-auth - Main contains only requirements and analysis docs - UI Prototype: https://webspace.media-on.de/leocrm-prototype-x7k2p9/ - Ready for Phase 2: Architecture design
1007 lines
47 KiB
Markdown
1007 lines
47 KiB
Markdown
# LeoCRM — Extracted Architecture Details
|
|
|
|
**Zweck:** Implementierungs-Details, die aus der bereinigten `requirements.md` entfernt wurden. Für den Solution Architect als Referenz.
|
|
**Quelle:** Original `requirements.md` (git HEAD, 1956 Zeilen) vs. bereinigte Version (2105 Zeilen)
|
|
**Datum:** 2026-06-28
|
|
|
|
---
|
|
|
|
## 1. Tech-Stack (vollständige Spezifikation)
|
|
|
|
### Original Tech-Stack-Tabelle (entfernt)
|
|
|
|
| Komponente | Entscheidung | Bemerkung |
|
|
|------------|-------------|----------|
|
|
| Backend | FastAPI + SQLAlchemy 2.0 | Python 3.12 |
|
|
| Datenbank | PostgreSQL (empfohlen) | Siehe DB-Empfehlung unten |
|
|
| Frontend | React SPA | Client-side rendering, i18n (bestätigt durch Prototyp) |
|
|
| Deployment | Coolify (Docker) | Bestätigt |
|
|
| Linting | ruff + black | Python-Standard |
|
|
| Testing | pytest + Vitest/Jest | Backend + Frontend |
|
|
|
|
### DB-Empfehlung: PostgreSQL statt SQLite
|
|
|
|
Ursprünglich war SQLite spezifiziert. Bei 200.000 Kontakten + mehreren Usern + Concurrent Writes ist SQLite limitiert:
|
|
|
|
- **SQLite:** File-Level Locking, nur 1 Writer gleichzeitig, ~15 Updates/Sekunde bei Concurrent Writes (StackOverflow Benchmark)
|
|
- **PostgreSQL:** True Concurrent Writers, ~1.500 Updates/Sekunde, MVCC-Architektur (tableone.dev Benchmark)
|
|
- **Fazit:** Bei 200k Datensätzen + Multi-User + gleichzeitige Schreibzugriffe → PostgreSQL
|
|
|
|
### v0.1 Historischer Tech-Stack (archiviert)
|
|
- ~~Backend: FastAPI (Python 3.11+)~~ → Python 3.12
|
|
- ~~DB: SQLite (Datei `leocrm.db`)~~ → PostgreSQL 16
|
|
- ~~Templates: Jinja2 (HTML)~~ → React 18 SPA
|
|
- ~~Server: Uvicorn / Gunicorn~~ → Uvicorn (async)
|
|
- ~~Deployment: 1 Container Docker Compose~~ → Multi-Container (Backend + Frontend + PostgreSQL + OnlyOffice + Worker)
|
|
|
|
---
|
|
|
|
## 2. Domain Knowledge (entfernte Referenzen)
|
|
|
|
**Domain:** Customer Relationship Management (CRM)
|
|
|
|
Standard-Entitäten in CRMs (Referenz: Zoho CRM, HubSpot, Salesforce):
|
|
- **Accounts (Firmen):** Name, Adresse, Industrie, Employees, Revenue, Owner, Phone, Website, Billing/Shipping Address
|
|
- **Contacts (Kontaktpersonen):** Name, Email, Phone, Mobile, Title, Department, Mailing Address, Reports To, Description
|
|
- **Beziehungen:** N:M (ein Kontakt kann mehreren Firmen zugeordnet sein)
|
|
|
|
Quellen:
|
|
- Zoho CRM Standard Fields Accounts: https://help.zoho.com/portal/en/kb/crm/sales-force-automation/accounts/articles/standard-fields-accounts
|
|
- Zoho CRM Standard Fields Contacts: https://help.zoho.com/portal/en/kb/crm/sales-force-automation/contacts/articles/standard-fields-contacts
|
|
- Encore Business Solutions — 30 CRM Custom Account Fields
|
|
- Bitrix24 Standard fields in CRM
|
|
|
|
---
|
|
|
|
## 3. HTTP-Endpunkt-Spezifikationen
|
|
|
|
### F-AUTH-01: Login
|
|
- **Endpoint:** POST `/api/auth/login`
|
|
- **Akzeptiert:** E-Mail + Passwort
|
|
- **Response:** Session-Cookie (HttpOnly+Secure+SameSite=Strict) oder 401
|
|
|
|
### F-AUTH-02: Logout
|
|
- **Clientseitig:** Token wird entfernt
|
|
- **Server:** Token-Blacklist optional für v1
|
|
|
|
### F-AUTH-03: User-Verwaltung
|
|
- **Endpoint:** POST `/api/users`
|
|
- **Auth:** Admin-Token erforderlich
|
|
- **Response:** 201 (erstellt) oder 403 (ohne Admin-Token)
|
|
|
|
### F-AUTH-04: RBAC
|
|
- **Jeder API-Endpoint prüft Rolle:** falsche Rolle → 403; korrekte Rolle → 200/201
|
|
- **Beispiele:** POST `/api/users` → 403 (editor); POST `/api/companies` → 403 (viewer), GET `/api/companies` → 200 (viewer)
|
|
|
|
### F-AUTH-05: Passwort-Reset
|
|
- **Request:** POST `/api/auth/password-reset/request` → sendet E-Mail
|
|
- **Confirm:** POST `/api/auth/password-reset/confirm` mit Token + neuem Passwort → aktualisiert Passwort
|
|
|
|
### F-COMP-01: Firma anlegen
|
|
- **Endpoint:** POST `/api/companies`
|
|
- **Response:** 201 Created (valide Daten); 422 Validation Error (ohne Name)
|
|
|
|
### F-COMP-02: Firma anzeigen
|
|
- **Endpoint:** GET `/api/companies/{id}`
|
|
- **Response:** 200 mit Firmendaten + Kontakte-Array; 404 (nicht-existent)
|
|
|
|
### F-COMP-03: Firma bearbeiten
|
|
- **Endpoint:** PUT/PATCH `/api/companies/{id}`
|
|
- **Response:** 200 mit aktualisierten Daten; 422 (Validierungsfehler)
|
|
|
|
### F-COMP-04: Firma löschen (Soft-Delete)
|
|
- **Endpoint:** DELETE `/api/companies/{id}?cascade=true|false`
|
|
- **Response:** 200; Firma wird als `deleted_at = NOW()` markiert
|
|
- **Cascade:** bei cascade=true auch Kontakte soft-deleted
|
|
|
|
### F-COMP-05: Firmen-Liste mit Pagination
|
|
- **Endpoint:** GET `/api/companies?page=2&page_size=25&sort_by=name&sort_order=desc`
|
|
- **Response:** 200 mit `{items, total, page, page_size}`
|
|
|
|
### F-COMP-06: Firmen-Suche & Filter
|
|
- **Endpoint:** GET `/api/companies?search=Tech&industry=IT&country=Germany`
|
|
- **Response:** 200 mit gefilterten Ergebnissen
|
|
|
|
### F-COMP-07: Audit-Log
|
|
- **Tabelle:** `audit_log`
|
|
- **Mechanismus:** Middleware/Decorator loggt schreibende Aktionen
|
|
- **Endpoint:** GET `/api/audit-log` (Admin, paginiert)
|
|
|
|
### F-COMP-08: DSGVO / Right to be Forgotten
|
|
- **Endpoint:** DELETE `/api/contacts/{id}?gdpr=true` → harte Löschung
|
|
- **Tabelle:** separate `deletion_log` Tabelle (unveränderlich)
|
|
|
|
### F-CONT-01: Kontaktperson anlegen
|
|
- **Endpoint:** POST `/api/contacts`
|
|
- **Response:** 201 (valide Daten); 422 (ohne last_name)
|
|
- **N:M:** mit company_ids → N:M-Verknüpfungen erstellt
|
|
|
|
### F-CONT-02: Kontaktperson anzeigen
|
|
- **Endpoint:** GET `/api/contacts/{id}`
|
|
- **Response:** 200 mit Kontaktdaten + Firmen-Array; 404
|
|
|
|
### F-CONT-03: Kontaktperson bearbeiten
|
|
- **Endpoint:** PUT/PATCH `/api/contacts/{id}`
|
|
- **Response:** 200; Änderung an company_ids → N:M-Tabelle aktualisiert
|
|
|
|
### F-CONT-04: Kontaktperson löschen (Soft-Delete)
|
|
- **Endpoint:** DELETE `/api/contacts/{id}`
|
|
- **Response:** 200; `deleted_at` gesetzt; N:M-Einträge gelöscht; Firmen unberührt
|
|
|
|
### F-CONT-05: Kontakt-Liste mit Pagination
|
|
- **Endpoint:** GET `/api/contacts?page=1&page_size=25&sort_by=last_name&sort_order=asc`
|
|
- **Response:** 200 mit paginierten Ergebnissen
|
|
- **Performance:** Response-Zeit <500ms bei 200k Datensätzen (mit DB-Index)
|
|
|
|
### F-CONT-06: Kontakt-Suche & Filter
|
|
- **Endpoint:** GET `/api/contacts?search=Müller&company_id=5`
|
|
- **Response:** 200 mit gefilterten Ergebnissen
|
|
|
|
### F-CONT-07: N:M Firmen-Kontakt-Zuordnung
|
|
- **Tabelle:** `company_contacts` (N:M-Verknüpfungstabelle)
|
|
- **Zuordnung:** POST `/api/companies/{id}/contacts/{contact_id}`
|
|
- **Entfernung:** DELETE `/api/companies/{id}/contacts/{contact_id}`
|
|
|
|
### F-DATA-01: CSV-Export
|
|
- **Endpoint:** GET `/api/companies/export?format=csv&filters=...`
|
|
- **Response:** 200 mit `Content-Type: text/csv`, Datei-Download
|
|
|
|
### F-DATA-02: Excel-Export
|
|
- **Endpoint:** GET `/api/companies/export?format=xlsx&filters=...`
|
|
- **Response:** 200 mit `Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
|
|
|
|
### F-DATA-03: Daten-Validierung
|
|
- **Technologie:** Pydantic-Schemas für alle Entities
|
|
- **Response:** ungültige Eingaben → 422 mit detailiertem Fehler-Objekt
|
|
|
|
### F-DATA-06: ARIA-Rollen auf DataTable
|
|
- **ARIA-Rollen:** `role="table"`, `role="row"`, `role="columnheader"`, `role="cell"`
|
|
- **Sortierung:** `aria-sort="ascending|descending|none"`
|
|
- **Label:** beschreibendes `aria-label`
|
|
- **Testing:** NVDA und axe DevTools
|
|
|
|
### F-UI-01: Responsive Design
|
|
- **Breakpoints:** Mobile-First; Touch-Targets min 44px
|
|
- **Testing:** Chrome DevTools (375px, 768px, 1920px)
|
|
|
|
### F-UI-02: Internationalisierung
|
|
- **Library:** react-i18next
|
|
- **Locale-Files:** JSON-Locale-Files für DE/EN
|
|
- **Persistenz:** Sprachwahl persistiert in User-Settings
|
|
|
|
### F-UI-03: Error-Handling & Toast-Notifications
|
|
- **Komponente:** Toast-Komponente (success/error/warning/info)
|
|
- **Routen:** dedizierte 404/500-Routen im Frontend
|
|
|
|
### F-UI-04: Loading-States
|
|
- **Komponenten:** Loading-State-Komponenten; Skeleton-Loader
|
|
- **State:** API-Loading-State im Frontend-Store; Button-Disabled-State während Pending
|
|
|
|
### F-UI-05: Empty-States
|
|
- **Komponente:** Empty-State-Komponenten mit Icon + Text + CTA
|
|
|
|
### F-UI-06: Confirmation-Dialogs
|
|
- **Komponente:** Modal-Dialog-Komponente
|
|
- **Regel:** alle DELETE-Operationen erfordern Bestätigung
|
|
|
|
### F-SEC-01: CSRF-Schutz
|
|
- **Implementierung:** SameSite=Strict Cookies + Origin-Header-Validierung
|
|
- **Kein Double-Submit-Token**
|
|
- **Middleware:** Origin-Header-Validierung-Middleware
|
|
- **Exempt:** nur GET/HEAD/OPTIONS
|
|
|
|
### F-SEC-02: XSS-Schutz & Input-Sanitization
|
|
- **Frontend:** Output-Encoding
|
|
- **Backend:** Pydantic-Validierung strippt gefährliche Eingaben
|
|
- **Header:** CSP-Header gesetzt
|
|
|
|
### F-SEC-03: Session-Timeout
|
|
- **Timeout:** 8h
|
|
- **Interceptor:** 401-Interceptor im Frontend → Redirect zur Login-Seite
|
|
- **Refresh:** Refresh-Token optional für v1
|
|
|
|
### F-INFRA-01: Health-Check Endpoint
|
|
- **Endpoint:** GET `/api/health`
|
|
- **Response:** 200 `{status: "healthy", db: "connected"}` oder 503 `{status: "unhealthy", db: "disconnected"}`
|
|
- **Auth:** kein Auth erforderlich
|
|
- **Coolify:** nutzt Endpoint für Health-Check
|
|
|
|
### F-INFRA-02: Backup & Restore
|
|
- **Backup:** Docker-Volume-Backup konfiguriert
|
|
- **Doku:** Restore-Dokumentation in README
|
|
|
|
### F-INFRA-03: Logging
|
|
- **Format:** Python `logging` mit JSON-Formatter
|
|
- **Konfiguration:** Log-Level konfigurierbar via Env-Var
|
|
- **Inhalt:** Method, Path, Status, Duration; Error-Logs mit Stacktrace
|
|
|
|
### F-INFRA-04: Monitoring & Alerting
|
|
- **Monitoring:** Coolify-Built-in-Monitoring nutzt `/api/health`
|
|
- **Optional:** externes Monitoring-Tool
|
|
|
|
### F-MIG-01: CSV-Import
|
|
- **Endpoint:** POST `/api/import` mit CSV-Datei + Entity-Type
|
|
- **Response:** Import-Job mit success/skipped/failed counts
|
|
|
|
### F-INT-01: E-Mail-Integration (Passwort-Reset)
|
|
- **Config:** SMTP via Env-Vars (SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS)
|
|
- **Templates:** E-Mail-Templates für Reset
|
|
|
|
### F-INT-02: API-Keys / Token-Auth
|
|
- **Auth:** Session-Auth-Middleware auf allen Endpoints außer `/api/health` und `/api/auth/*`
|
|
- **Optional:** API-Key für externe Integrationen (post-MVP)
|
|
|
|
### F-TEST-01: Testing-Strategie
|
|
- **Backend:** pytest + httpx für API-Tests; Coverage >80%
|
|
- **Frontend:** Vitest für Component-Tests
|
|
- **E2E:** Playwright für Critical Paths
|
|
- **CI:** Coverage-Report in CI; bei Fehlschlag wird Build blockiert
|
|
|
|
### F-ENV-01: Environments & Secrets
|
|
- **Env-Vars:** `LEOCRM_SECRET_KEY` (Session-Secret, min 32 Zeichen)
|
|
- **Datei:** `.env.example` dokumentiert alle Vars
|
|
- **Secrets:** keine Secrets im Git-Repo
|
|
- **Coolify:** Env-Vars konfiguriert
|
|
|
|
### F-DOC-01: Dokumentation
|
|
- **README:** `README.md` mit Setup-Anleitung
|
|
- **API-Doku:** `/docs` (Swagger/OpenAPI) auto-gen via FastAPI
|
|
- **Admin-Doku:** `docs/admin-guide.md` für Deploy/Backup/Restore
|
|
|
|
### F-PERF-01: Performance
|
|
- **Response-Zeit:** <500ms für List-Endpunkte bei 200k Datensätzen
|
|
- **DB-Indizes:** last_name, first_name, email, company.name, company.industry, company.country
|
|
- **Pagination:** verhindert Full-Table-Scan
|
|
- **Export:** >50k Datensätze als Background-Job oder Streaming-Response
|
|
|
|
### F-A11Y-01: Accessibility (WCAG 2.1 AA)
|
|
- **CSS-Utility:** `.sr-only` CSS-Klasse (visual-hidden pattern)
|
|
- **Kontrast:** >4.5:1
|
|
- **Testing:** axe DevTools
|
|
|
|
### F-A11Y-02: prefers-reduced-motion
|
|
- **CSS:** `@media (prefers-reduced-motion: reduce)` Block in globalen Styles
|
|
- **Properties:** alle `transition` und `animation` auf `none` oder `0.01ms`
|
|
- **Testing:** Chrome DevTools (Rendering → Emulate CSS prefers-reduced-motion: reduce)
|
|
|
|
### F-A11Y-03: 44px Touch-Targets
|
|
- **CSS-Regel:** `min-height: 44px; min-width: 44px;` für alle interaktiven Elemente
|
|
- **Alternative:** `::after` Pseudo-Element mit 44px Touch-Bereich
|
|
- **Testing:** Chrome DevTools (375px Breite, Touch-Emulation)
|
|
|
|
### F-SCHED-01: Background-Jobs
|
|
- **Queue:** Background-Task-Queue (Celery, ARQ, oder FastAPI BackgroundTasks für v1)
|
|
- **Status:** Job-Status-Endpoint
|
|
- **Export-Limit:** >50k Datensätze als Background-Job, <50k als direkter Download
|
|
|
|
---
|
|
|
|
## 4. DB-Schema-Definitionen (Feld-Tabellen)
|
|
|
|
### F-COMP-01: Company-Felder
|
|
|
|
| Feld | Typ | Pflicht | Bemerkung |
|
|
|------|-----|---------|-----------|
|
|
| name | String(100) | ✅ | Firmenname |
|
|
| account_number | String(40) | ❌ | Interne Referenznummer |
|
|
| industry | Picklist | ❌ | IT, Finance, Manufacturing, etc. |
|
|
| account_type | Picklist | ❌ | Customer, Partner, Prospect, etc. |
|
|
| ownership | Picklist | ❌ | Public, Private, Government, etc. |
|
|
| employees | Integer | ❌ | Anzahl Mitarbeiter |
|
|
| annual_revenue | Decimal | ❌ | Jahresumsatz |
|
|
| phone | String(30) | ❌ | Haupttelefon |
|
|
| fax | String(30) | ❌ | Fax |
|
|
| email | Email | ❌ | Allgemeine E-Mail |
|
|
| website | URL | ❌ | Website |
|
|
| rating | Picklist | ❌ | Hot, Warm, Cold |
|
|
| parent_account_id | FK→Company | ❌ | Muttergesellschaft |
|
|
| billing_street | String(250) | ❌ | Rechnungsadresse Strasse |
|
|
| billing_city | String(100) | ❌ | Rechnungsadresse Stadt |
|
|
| billing_state | String(100) | ❌ | Rechnungsadresse Bundesland |
|
|
| billing_postal_code | String(20) | ❌ | Rechnungsadresse PLZ |
|
|
| billing_country | String(100) | ❌ | Rechnungsadresse Land |
|
|
| shipping_street | String(250) | ❌ | Besuchsadresse Strasse |
|
|
| shipping_city | String(100) | ❌ | Besuchsadresse Stadt |
|
|
| shipping_state | String(100) | ❌ | Besuchsadresse Bundesland |
|
|
| shipping_postal_code | String(20) | ❌ | Besuchsadresse PLZ |
|
|
| shipping_country | String(100) | ❌ | Besuchsadresse Land |
|
|
| description | Text(32000) | ❌ | Notizfeld |
|
|
| sic_code | String(10) | ❌ | Standard Industrial Classification |
|
|
| ticker_symbol | String(30) | ❌ | Börsenkürzel |
|
|
| account_site | String(80) | ❌ | Standort-Name (z.B. Headquarters) |
|
|
|
|
### F-CONT-01: Contact-Felder
|
|
|
|
| Feld | Typ | Pflicht | Bemerkung |
|
|
|------|-----|---------|-----------|
|
|
| first_name | String(50) | ❌ | Vorname |
|
|
| last_name | String(50) | ✅ | Nachname |
|
|
| salutation | Picklist | ❌ | Herr, Frau, Dr., etc. |
|
|
| email | Email | ❌ | Haupt-E-Mail |
|
|
| secondary_email | Email | ❌ | Zweit-E-Mail |
|
|
| phone | String(30) | ❌ | Bürotelefon |
|
|
| mobile | String(30) | ❌ | Mobiltelefon |
|
|
| home_phone | String(30) | ❌ | Privattelefon |
|
|
| fax | String(30) | ❌ | Fax |
|
|
| title | String(100) | ❌ | Jobtitel (CEO, Manager, etc.) |
|
|
| department | String(100) | ❌ | Abteilung |
|
|
| reports_to | FK→Contact | ❌ | Vorgesetzter |
|
|
| date_of_birth | Date | ❌ | Geburtsdatum |
|
|
| assistant | String(50) | ❌ | Assistent-Name |
|
|
| assistant_phone | String(30) | ❌ | Assistent-Telefon |
|
|
| mailing_street | String(250) | ❌ | Postadresse Strasse |
|
|
| mailing_city | String(100) | ❌ | Postadresse Stadt |
|
|
| mailing_state | String(100) | ❌ | Postadresse Bundesland |
|
|
| mailing_postal_code | String(20) | ❌ | Postadresse PLZ |
|
|
| mailing_country | String(100) | ❌ | Postadresse Land |
|
|
| other_street | String(250) | ❌ | Andere Adresse Strasse |
|
|
| other_city | String(100) | ❌ | Andere Adresse Stadt |
|
|
| other_state | String(100) | ❌ | Andere Adresse Bundesland |
|
|
| other_postal_code | String(20) | ❌ | Andere Adresse PLZ |
|
|
| other_country | String(100) | ❌ | Andere Adresse Land |
|
|
| skype_id | String(50) | ❌ | Skype |
|
|
| linkedin | URL | ❌ | LinkedIn-Profil |
|
|
| twitter | String(50) | ❌ | Twitter-Handle |
|
|
| description | Text(32000) | ❌ | Notizfeld |
|
|
| company_ids | [FK→Company] | ❌ | N:M-Zuordnung (0..n Firmen) |
|
|
|
|
### Implizite DB-Tabellen (aus Akzeptanzkriterien extrahiert)
|
|
|
|
- `company_contacts` — N:M-Verknüpfungstabelle (company_id ↔ contact_id)
|
|
- `audit_log` — Tabelle für Audit-Log-Einträge (user, action, entity, entity_id, timestamp)
|
|
- `deletion_log` — Unveränderliche Tabelle für DSGVO-Löschungen
|
|
- `users` — User-Tabelle (email, name, role, password_hash)
|
|
- `companies` — Firmen-Tabelle (siehe Feld-Tabelle oben, + `deleted_at` TIMESTAMP für Soft-Delete)
|
|
- `contacts` — Kontakt-Tabelle (siehe Feld-Tabelle oben, + `deleted_at` TIMESTAMP für Soft-Delete)
|
|
|
|
---
|
|
|
|
## 5. Nicht-funktionale Anforderungen (vollständige Tabelle)
|
|
|
|
| ID | Kategorie | Anforderung | Metrik |
|
|
|----|-----------|-------------|--------|
|
|
| NF-01 | Performance | API-Response <500ms bei 200k Datensätzen | 95th Percentile <500ms |
|
|
| NF-02 | Skalierung | DB-Connection-Pooling (SQLAlchemy Pool size=10) | Keine Connection-Erschöpfung bei 10 concurrent Users |
|
|
| NF-03 | Sicherheit | Alle Passwörter bcrypt-gehashed (cost=12) | Keine Plain-Text-Passwörter in DB |
|
|
| NF-04 | Sicherheit | Session-Secret via Env-Var, min 32 Zeichen | App startet nicht ohne Secret |
|
|
| NF-05 | Verfügbarkeit | Health-Check für Coolify Auto-Restart | Auto-Restart bei 503 |
|
|
| NF-06 | Wartbarkeit | Code-Struktur: `api/`, `models/`, `schemas/`, `services/`, `tests/` | Klare Trennung |
|
|
| NF-07 | Wartbarkeit | Linting: ruff + black, pre-commit-hook | CI blockt bei Lint-Fehlern |
|
|
| NF-08 | Testbarkeit | pytest Coverage >80% Backend | Coverage-Report in CI |
|
|
| NF-09 | i18n | DE + EN, Sprachwahl persistiert | Alle UI-Texte übersetzt |
|
|
| NF-10 | Accessibility | WCAG 2.1 AA | axe-Check: 0 Violations |
|
|
| NF-11 | Logging | Strukturierte JSON-Logs | `docker logs` zeigt JSON-Einträge |
|
|
| NF-12 | Deployment | Docker-Container auf Coolify | `docker-compose.yml` + `Dockerfile` |
|
|
|
|
---
|
|
|
|
## 6. Annahmen (entfernte Details)
|
|
|
|
1. **Multi-Tenant (Multi-Company):** v1 ist Multi-Tenant (Multi-Company) — mehrere Organisationen, mehrere User.
|
|
2. **PostgreSQL statt SQLite:** Bei 200k Kontakten + Multi-User + Concurrent Writes wird PostgreSQL empfohlen. SQLite limitiert auf ~15 concurrent updates/sec vs PostgreSQL ~1500/sec.
|
|
3. **SPA-Frontend:** Client-side rendering mit React SPA (bestätigt durch genehmigten Prototyp leocrm-prototype-x7k2p9, v7).
|
|
4. **Max 10 concurrent Users:** v1 ist für kleine Teams, nicht für Enterprise.
|
|
5. **E-Mail-Versand:** SMTP via externem Provider (Mailtrap für Dev, Production-SMTP für Prod).
|
|
6. **Soft-Delete als Default:** Firmen und Kontakte werden soft-deleted (recoverable), außer DSGVO-Löschung (hard delete).
|
|
7. **Rollensystem v1:** 3 Rollen: admin, editor, viewer.
|
|
8. **Export-Limit:** Export von >50k Datensätzen als Background-Job, <50k als direkter Download.
|
|
9. **i18n-Default:** Deutsch ist Default-Sprache, Englisch ist zweites Locale.
|
|
10. **Python 3.12:** Aktuelle stabile Python-Version.
|
|
|
|
---
|
|
|
|
## 7. Technologie-Wahlen (Alle Module)
|
|
|
|
### Core-Stack
|
|
| Technologie | Verwendung |
|
|
|-------------|-----------|
|
|
| FastAPI | Backend-Framework |
|
|
| SQLAlchemy 2.0 | ORM |
|
|
| PostgreSQL 16 | Datenbank |
|
|
| React 18 SPA | Frontend |
|
|
| Uvicorn (async) | ASGI-Server |
|
|
| ruff + black | Linting |
|
|
| pytest + httpx | Backend-Tests |
|
|
| Vitest | Frontend-Tests |
|
|
| Playwright | E2E-Tests |
|
|
| Pydantic | Input-Validierung |
|
|
| bcrypt (cost=12) | Passwort-Hashing |
|
|
| Celery / ARQ / FastAPI BackgroundTasks | Background-Jobs |
|
|
| react-i18next | i18n-Frontend |
|
|
| Docker Compose | Multi-Container-Deployment |
|
|
| Coolify | Deployment-Plattform |
|
|
|
|
### DMS-Modul
|
|
| Technologie | Verwendung |
|
|
|-------------|-----------|
|
|
| PDF.js | PDF-Preview im Browser |
|
|
| OnlyOffice Document Server | Online-Bearbeitung (separater Container) |
|
|
|
|
### Mail-Modul
|
|
| Technologie | Verwendung |
|
|
|-------------|-----------|
|
|
| IMAP4rev1 (RFC 3501) | Mail-Empfang |
|
|
| IMAP IDLE (RFC 2177) | Real-time Push für neue Mails |
|
|
| SMTP Submission (RFC 6409) | Mail-Versand |
|
|
| DOMPurify | HTML-Sanitization (XSS-Schutz) |
|
|
| python-gnupg | PGP-Verschlüsselung (OpenPGP, RFC 4880) |
|
|
| PostgreSQL Full-Text Search (tsvector) | Volltext-Suche über Mails |
|
|
| AES-256 | Passwort-Verschlüsselung für IMAP/SMTP-Credentials |
|
|
|
|
---
|
|
|
|
## 8. Farbcodes (Hex-Werte)
|
|
|
|
### Kalender-Modul (F-CAL-06)
|
|
|
|
| Eintrags-Typ | Subtyp | Farbe (Hex) |
|
|
|-------------|-------|-------------|
|
|
| appointment | normal | `#3B82F6` (blau) |
|
|
| task | normal | `#F59E0B` (gelb) |
|
|
| * | follow_up | `#F97316` (orange) |
|
|
| * | private | `#9CA3AF` (grau) |
|
|
|
|
### Tag-System (F-TAG-02)
|
|
- Tags können mit Farbe versehen werden, z.B. `#FF0000` für „VIP"
|
|
|
|
---
|
|
|
|
## 9. Protokoll-Details (Mail-Modul)
|
|
|
|
### IMAP
|
|
- **Protokoll:** IMAP4rev1 (RFC 3501)
|
|
- **IDLE:** RFC 2177 für Push, Fallback: Polling alle 5 Min
|
|
- **Port:** 993 (SSL/TLS)
|
|
- **Auth:** PLAIN/LOGIN
|
|
- **MOVE:** RFC 6851 (Fallback: COPY + STORE \\Deleted + EXPUNGE)
|
|
- **Flags:** `\\Seen`, `\\Flagged`, `\\Answered`, `\\Draft`, `$Spam`
|
|
- **Kompatibilität:** Getestet mit Dovecot, Courier IMAP
|
|
|
|
### SMTP
|
|
- **Protokoll:** SMTP Submission (RFC 6409)
|
|
- **Port 587:** mit STARTTLS
|
|
- **Port 465:** mit SSL
|
|
- **Auth:** PLAIN/LOGIN mit Benutzername+Passwort
|
|
|
|
### PGP
|
|
- **Standard:** OpenPGP (RFC 4880)
|
|
- **Library:** python-gnupg
|
|
- **Private Key:** verschlüsselt gespeichert (AES-256 + Passphrase)
|
|
- **Passphrase:** nicht gespeichert, nur im Session-Cache
|
|
- **Public Key:** pro Kontakt speicherbar
|
|
- **S/MIME:** post-MVP
|
|
|
|
### Attachment-Storage
|
|
- Anhänge werden lokal gespeichert (Dateisystem oder S3)
|
|
- Nicht im IMAP-Server belassen (Caching)
|
|
- Mail-Body wird in PostgreSQL gespeichert
|
|
- Anhänge bei Bedarf vom IMAP-Server nachgeladen
|
|
- Max: 25 MB pro Anhang, 50 MB pro Mail
|
|
|
|
### Passwort-Speicherung
|
|
- IMAP/SMTP-Passwörter AES-256 verschlüsselt
|
|
- Key via Env-Var `MAIL_ENCRYPTION_KEY`
|
|
|
|
### Ordner-Mapping
|
|
- INBOX → Posteingang
|
|
- Sent → Postausgang
|
|
- Drafts → Entwürfe
|
|
- Spam/Junk → Spam
|
|
|
|
### Threading
|
|
- Basierend auf `References`- und `In-Reply-To`-Headern (RFC 5322)
|
|
|
|
### HTML-Rendering
|
|
- Sanitization mit DOMPurify (XSS-Schutz)
|
|
- Inline-Styles erlaubt, Scripts/IFrames entfernt
|
|
- Plain-Text-Fallback
|
|
|
|
---
|
|
|
|
## 10. RFC-Referenzen (alle)
|
|
|
|
| RFC | Titel | Verwendung |
|
|
|-----|-------|-----------|
|
|
| RFC 3501 | IMAP4rev1 | Mail-Empfang |
|
|
| RFC 2177 | IMAP IDLE | Real-time Push |
|
|
| RFC 6851 | IMAP MOVE | Mail verschieben |
|
|
| RFC 6409 | SMTP Submission | Mail-Versand |
|
|
| RFC 4880 | OpenPGP | PGP-Verschlüsselung |
|
|
| RFC 5322 | Internet Message Format | Threading (References/In-Reply-To) |
|
|
| RFC 5545 | iCalendar | Recurrence Rules (RRULE) für wiederkehrende Termine |
|
|
| RFC 6047 | iMIP | (Non-Goal: nicht in v2-mail) |
|
|
| RFC 8620 | JMAP | (Non-Goal: nicht unterstützt) |
|
|
|
|
---
|
|
|
|
## 11. DMS-Modul: HTTP-Endpunkte & Architektur-Details
|
|
|
|
### F-DMS-01: Ordner-Struktur
|
|
- **Create:** POST `/api/dms/folders` → 201
|
|
- **Rename/Move:** PATCH `/api/dms/folders/{id}` → 200
|
|
- **Delete:** DELETE `/api/dms/folders/{id}` → 200 (Soft-Delete)
|
|
- **Zirkuläre Verschiebung:** serverseitig verhindert → 422
|
|
|
|
### F-DMS-02: Datei-Upload
|
|
- **Endpoint:** POST `/api/dms/files/upload` (multipart) → 201 mit Datei-Metadaten
|
|
- **Max-Size:** 413 bei Überschreitung (Default 100 MB)
|
|
- **Status:** WebSocket oder Polling
|
|
|
|
### F-DMS-03: Datei-Operationen
|
|
- **Rename:** PATCH `/api/dms/files/{id}` → 200
|
|
- **Move:** PATCH `/api/dms/files/{id}` mit `folder_id` → 200
|
|
- **Delete:** DELETE `/api/dms/files/{id}` → 200 (Soft-Delete)
|
|
- **Restore:** POST `/api/dms/files/{id}/restore` → 200
|
|
|
|
### F-DMS-04: PDF-Preview
|
|
- **Endpoint:** GET `/api/dms/files/{id}/preview` → 200 mit `Content-Type: application/pdf`
|
|
- **Frontend:** PDF.js
|
|
- **Non-PDF:** 415 Unsupported Media Type
|
|
|
|
### F-DMS-05: OnlyOffice Integration
|
|
- **Server:** OnlyOffice Document Server als separater Container (Coolify)
|
|
- **Endpoint:** POST `/api/dms/files/{id}/edit-session` → 200 mit OnlyOffice-URL
|
|
- **Formate:** DOCX, XLSX, PPTX
|
|
|
|
### F-DMS-06: Datei-Metadaten
|
|
- **Endpoint:** GET `/api/dms/files/{id}` → 200 mit `{name, size, mime_type, uploaded_at, modified_at, uploaded_by}`
|
|
- **Größe:** serverseitig in Bytes, clientseitig formatiert
|
|
|
|
### F-DMS-07: Datei-Suche
|
|
- **Endpoint:** GET `/api/dms/search?q=vertrag&folder_id={id}` → 200 mit `{items: [{type, name, path, ...}]}`
|
|
- **Global:** ohne `folder_id` → gesamte DMS
|
|
|
|
### F-LINK-01: Dateien mit Firmen verknüpfen
|
|
- **Endpoint:** POST `/api/dms/files/{id}/link` mit `{entity_type: "company", entity_id: N}` → 201
|
|
- **Delete:** DELETE `/api/dms/files/{id}/link?entity_type=company&entity_id=N` → 204
|
|
- **Unique Constraint:** Duplikate serverseitig verhindert
|
|
|
|
### F-LINK-02: Dateien mit Kontakten verknüpfen
|
|
- **Endpoint:** POST `/api/dms/files/{id}/link` mit `{entity_type: "contact", entity_id: N}` → 201
|
|
|
|
### F-LINK-03: Verknüpfte Dateien in Firmen-Detail
|
|
- **Response:** GET `/api/companies/{id}` enthält `linked_files: [{id, name, size, mime_type, modified_at}]`
|
|
|
|
### F-LINK-04: Verknüpfte Dateien in Kontakt-Detail
|
|
- **Response:** GET `/api/contacts/{id}` enthält `linked_files: [{id, name, size, mime_type, modified_at}]`
|
|
|
|
### F-LINK-05: Reverse-Verknüpfung
|
|
- **Response:** GET `/api/dms/files/{id}` enthält `linked_entities: [{entity_type, entity_id, entity_name}]`
|
|
- **Delete:** DELETE `/api/dms/files/{id}/link?entity_type=company&entity_id=N` → 204
|
|
|
|
### F-LINK-06: Mehrfach-Verknüpfung
|
|
- **Bulk:** POST `/api/dms/files/bulk-link` mit `{file_ids: [...], entity_type: "company", entity_id: N}` → 201
|
|
- **Ordner:** POST `/api/dms/folders/{id}/link`
|
|
|
|
### F-TAG-01: Tags anwenden
|
|
- **Assign:** POST `/api/tags/assign` mit `{entity_type, entity_id, tag_id}` → 201
|
|
- **Remove:** DELETE `/api/tags/assign?entity_type=...&entity_id=...&tag_id=...` → 204
|
|
- **Unique Constraint:** (entity_type, entity_id, tag_id)
|
|
|
|
### F-TAG-02: Tag-Verwaltung
|
|
- **Create:** POST `/api/tags` (Admin) → 201
|
|
- **Update:** PATCH `/api/tags/{id}` → 200
|
|
- **Delete:** DELETE `/api/tags/{id}` → 204 mit Cascade Delete auf `tag_assignments`
|
|
- **Non-Admin:** → 403
|
|
|
|
### F-TAG-03: Tag-Filterung
|
|
- **Endpoint:** GET `/api/companies?tag_ids=1,2&tag_mode=and` → 200
|
|
- **Modi:** `tag_mode=or` → OR-Verknüpfung
|
|
- **Gleiche Parameter** für `/api/contacts`, `/api/dms/files`
|
|
|
|
### F-TAG-04: Tag-Cloud
|
|
- **Endpoint:** GET `/api/tags?with_counts=true` → 200 mit `[{id, name, color, count}]`
|
|
|
|
### F-PERM-01: Persönlicher Root-Ordner
|
|
- **User-Creation:** automatisch `personal_folder_id` gesetzt
|
|
- **Ownership-Check:** GET `/api/dms/folders/{id}` prüft Ownership → 403 bei fremdem Ordner (außer Admin)
|
|
|
|
### F-PERM-02: Gemeinsame Root-Ordner
|
|
- **Create:** POST `/api/dms/folders` mit `shared_with_group_id` → 201
|
|
- **Permissions:** `folder_permissions` Tabelle (folder_id, group_id, permission: read|write)
|
|
|
|
### F-PERM-03: Datei/Ordner mit Usern teilen
|
|
- **Share:** POST `/api/dms/files/{id}/share` mit `{user_id, permission: "read|write"}` → 201
|
|
- **Remove:** DELETE `/api/dms/files/{id}/share?user_id=N` → 204
|
|
- **Shared-with-me:** GET `/api/dms/shared-with-me` → 200
|
|
|
|
### F-PERM-04: Datei/Ordner mit Gruppen teilen
|
|
- **Share:** POST `/api/dms/files/{id}/share` mit `{group_id, permission: "read|write"}` → 201
|
|
- **Resolution:** Individual > Group > Default (Deny vor Allow)
|
|
- **Remove:** DELETE `/api/dms/files/{id}/share?group_id=N` → 204
|
|
|
|
### F-PERM-05: Share-Links
|
|
- **Create:** POST `/api/dms/files/{id}/share-link` mit `{password?, expires_at?, download_only?}` → 201 mit `{url}`
|
|
- **Public:** GET `/api/public/share/{token}` → 200 (oder 401 bei Passwort, 410 bei abgelaufen)
|
|
- **Public-Endpoint:** braucht keine Auth
|
|
|
|
### F-PERM-06: Berechtigungs-Anzeige
|
|
- **Endpoint:** GET `/api/dms/files/{id}/permissions` → 200 mit `{owner: {id, name}, shares: [{user_id?, group_id?, name, permission}], share_links: [{id, url, has_password, expires_at, is_active}]}`
|
|
|
|
### F-FILEUI-01: Datei-Browser
|
|
- **Komponenten:** `FileBrowser` mit `SidebarTree` + `MainView` (Grid/List Toggle)
|
|
- **State-Management:** für aktiven Ordner
|
|
|
|
### F-FILEUI-02: Breadcrumb-Navigation
|
|
- **Komponente:** `Breadcrumb` mit klickbaren Segmenten
|
|
- **Pfad:** aus `folder.path` (Materialized Path oder rekursive Abfrage) generiert
|
|
|
|
### F-FILEUI-03: Kontext-Menü
|
|
- **Komponente:** `ContextMenu` mit dynamischen Items basierend auf `entity_type` (file/folder) und `permission` (read/write)
|
|
- **Mobile:** Long-Press statt Rechtsklick
|
|
|
|
### F-FILEUI-04: Bulk-Aktionen
|
|
- **Move:** POST `/api/dms/files/bulk-move` mit `{file_ids: [...], target_folder_id}` → 200
|
|
- **Delete:** POST `/api/dms/files/bulk-delete` → 200
|
|
- **Tag:** POST `/api/tags/bulk-assign` → 201
|
|
|
|
### F-FILEUI-05: Upload-Progress
|
|
- **Komponente:** `UploadProgress` mit pro-Datei Progress
|
|
- **Upload:** via `XMLHttpRequest` (für Progress-Events) oder WebSocket
|
|
|
|
### F-FILEUI-06: Drag & Drop
|
|
- **Frontend:** HTML5 Drag & Drop API
|
|
- **Drop-Target:** validiert Permission clientseitig (grün = erlaubt, rot = verboten)
|
|
- **Server:** PATCH `/api/dms/files/{id}` mit `folder_id` → Permission-Check → 200 oder 403
|
|
|
|
---
|
|
|
|
## 12. Kalender-Modul: HTTP-Endpunkte & Architektur-Details
|
|
|
|
### Architektur-Hinweis
|
|
|
|
Ein Kalender-Eintrag (`CalendarEntry`) hat einen `entry_type`:
|
|
- **`appointment`** — Termin mit Start/Ende (Datum+Zeit), optional Ganztägig, Ort, Teilnehmer
|
|
- **`task`** — Aufgabe mit Fälligkeitsdatum (due_date), Priorität, Status, Sub-Tasks, Zuständiger — keine feste Uhrzeit
|
|
|
|
Beide Typen teilen sich die gleichen Verknüpfungs-, Erinnerungs-, Wiederholungs- und Anzeige-Mechanismen.
|
|
|
|
### F-CAL-01: Kalender-Ansichten
|
|
- **Frontend:** `CalendarView` Komponente mit `mode: month|week|day`
|
|
- **Endpoint:** GET `/api/calendar/entries?from=2026-06-01&to=2026-06-30` → 200 mit Entry-Array
|
|
|
|
### F-CAL-02: Kanban-Zeitraum-Ansicht
|
|
- **Frontend:** `KanbanCalendar` Komponente mit `period: this_week|next_2_weeks|this_month|custom`
|
|
- **Endpoint:** GET `/api/calendar/kanban?period=this_week` → 200 mit `{columns: [{date, appointments: [...], tasks: [...]}]}`
|
|
|
|
### F-CAL-03: Kalender-Eintrag erstellen
|
|
- **Endpoint:** POST `/api/calendar/entries` → 201
|
|
- **Fields appointment:** `entry_type`, `title`, `description?`, `start_at`, `end_at`, `all_day?`, `location?`, `attendees?`
|
|
- **Fields task:** `entry_type`, `title`, `description?`, `due_date?`, `priority: "high|medium|low"`, `status?: "open"`
|
|
- **Validierung:** end_at > start_at (außer all_day=true); title nicht leer
|
|
|
|
### F-CAL-04: Einträge mit Entitäten verknüpfen
|
|
- **Link:** POST `/api/calendar/entries/{id}/link` mit `{entity_type: "company|contact|deal", entity_id: N}` → 201
|
|
- **Response companies:** GET `/api/companies/{id}` enthält `upcoming_events: [...]` und `open_tasks: [...]`
|
|
|
|
### F-CAL-05: Drag & Drop im Kalender
|
|
- **Frontend:** HTML5 Drag & Drop auf Kalender-Zellen (appointments) und Kanban-Spalten (tasks)
|
|
- **Update:** PATCH `/api/calendar/entries/{id}` mit neuem `start_at`/`end_at` (appointment) oder `status` (task) → 200
|
|
- **Optimistic Update** mit Rollback bei Fehler
|
|
|
|
### F-CAL-06: Farbcodierung
|
|
- **Fields:** `entry_type: appointment|task` und `subtype: normal|follow_up|private`
|
|
- **Color Map:** `{appointment+normal: "#3B82F6", task+normal: "#F59E0B", *+follow_up: "#F97316", *+private: "#9CA3AF"}`
|
|
- **Update:** PATCH `/api/calendar/entries/{id}` mit `subtype` → 200
|
|
|
|
### F-CAL-07: Erinnerungen/Alerts
|
|
- **Field:** `reminder: {value: N, unit: "minutes|hours|days", channel: "in_app|email"}`
|
|
- **Background-Job:** prüft fällige Erinnerungen (Cron, minütlich)
|
|
- **Notification:** POST `/api/notifications` bei Fälligkeit
|
|
- **Überfällig-Check:** täglicher Cron-Job
|
|
|
|
### F-CAL-08: Wiederkehrende Einträge
|
|
- **Field:** `recurrence: {pattern: "daily|weekly|monthly|yearly|custom", custom_rule?, end_date?, exceptions?: [dates]}`
|
|
- **Appointments:** Server generiert Instanzen via RRULE (RFC 5545)
|
|
- **Tasks:** bei Status-Wechsel auf `done` → POST `/api/calendar/entries` mit neuem `due_date`
|
|
- **Status `cancelled`:** keine Generierung
|
|
|
|
### F-CAL-09: Kalender-Feeds (ICS)
|
|
- **Export:** GET `/api/calendar/{calendar_id}/ics-feed?token={user_token}` → 200 mit `Content-Type: text/calendar` (RFC 5545 konform)
|
|
- **Import:** POST `/api/calendar/import` (multipart ICS, optional calendar_id) → 201 mit `{imported: N, skipped: M}`
|
|
- **Auth:** Token-basierte Auth für Feed-URL
|
|
|
|
### F-CAL-10: Ressourcen-Booking
|
|
- **Create Resource:** POST `/api/resources` (Admin) → 201
|
|
- **Book:** POST `/api/calendar/entries/{id}/book-resource` mit `{resource_id}` → 201 oder 409 bei Konflikt
|
|
- **Bookings:** GET `/api/resources/{id}/bookings?from=...&to=...` → 200
|
|
|
|
### F-CAL-11: Mehrere Kalender
|
|
- **Create:** POST `/api/calendars` mit `{name, color, type: "personal|team|project|company"}` → 201
|
|
- **Auto-Create:** bei User-Anlage automatisch POST `/api/calendars` mit `{name: "Mein Kalender", type: "personal"}`
|
|
- **List:** GET `/api/calendars` → 200 mit sichtbaren Kalendern
|
|
- **Delete:** DELETE `/api/calendars/{id}` → 204 (Cascade auf Einträge)
|
|
|
|
### F-CAL-12: Kalender abonnieren
|
|
- **Frontend:** `CalendarSidebar` mit Toggle-Switches pro Kalender
|
|
- **Endpoint:** GET `/api/calendar/entries?calendar_ids=1,3,5` → 200
|
|
- **State:** persistiert pro User (`user_calendar_visibility` Tabelle)
|
|
|
|
### F-CAL-13: Kalender teilen
|
|
- **Share:** POST `/api/calendars/{id}/share` mit `{user_id?, group_id?, permission: "read|write"}` → 201
|
|
- **Remove:** DELETE `/api/calendars/{id}/share?user_id=N` → 204
|
|
- **Permissions:** GET `/api/calendars/{id}/permissions` → 200 mit `{owner, shares}`
|
|
|
|
### F-CAL-14: Default-Kalender
|
|
- **Setting:** `default_calendar_id` (User-Setting)
|
|
- **Update:** PATCH `/api/users/me/settings` mit `{default_calendar_id: N}` → 200
|
|
- **Fallback:** persönlicher Kalender bei keinem Setting
|
|
|
|
### F-CAL-15: Aufgaben-Zuweisung
|
|
- **Field:** `assigned_to: user_id` (nur für entry_type=task)
|
|
- **Update:** PATCH `/api/calendar/entries/{id}` mit `assigned_to` → 200, sendet Notification
|
|
- **Query:** GET `/api/calendar/entries?entry_type=task&assigned_to={user_id}` → 200
|
|
|
|
### F-CAL-16: Sub-Tasks
|
|
- **Create:** POST `/api/calendar/entries/{id}/subtasks` mit `{title}` → 201 (nur für entry_type=task)
|
|
- **Update:** PATCH `/api/calendar/entries/{id}/subtasks/{sub_id}` mit `{completed: true}` → 200
|
|
- **Response:** GET `/api/calendar/entries/{id}` enthält `subtasks: [{id, title, completed}]` und `progress: {completed, total}`
|
|
|
|
### F-CAL-17: Aufgaben-Filter & Liste
|
|
- **Query:** GET `/api/calendar/entries?entry_type=task&assigned_to={id}&priority=high&status=open&due_filter=overdue&page=1&page_size=25&sort_by=due_date&sort_order=asc` → 200 mit `{items, total, page, page_size}`
|
|
- **Export:** GET `/api/calendar/entries/export?format=csv&entry_type=task&status=open` → 200 mit CSV-Download
|
|
- **Frontend:** `TaskKanban` (4 Spalten) und `TaskList` (DataTable) Komponenten
|
|
|
|
### F-CAL-18: Bulk-Aktionen
|
|
- **Endpoint:** POST `/api/calendar/entries/bulk` mit `{entry_ids: [...], action: "status|assign|due_date|delete", value: ...}` → 200 mit `{updated: N}`
|
|
|
|
---
|
|
|
|
## 13. Mail-Modul: HTTP-Endpunkte & Architektur-Details
|
|
|
|
### F-MAIL-01: Standard-Ordner
|
|
- **Endpoint:** GET `/api/mail/folders` → 200 mit `{folders: [{id, name, type: "inbox|sent|drafts|spam|custom", unread_count, total_count}]}`
|
|
- **IMAP IDLE-Listener:** läuft als Background-Task, neue Mails innerhalb von 5 Sekunden
|
|
- **Bidirektionale Flag-Sync:** \\Seen, \\Flagged, \\Answered, \\Draft, $Spam
|
|
|
|
### F-MAIL-02: E-Mail schreiben
|
|
- **Send:** POST `/api/mail/send` mit `{to: [...], cc: [...], bcc: [...], subject, body_html, in_reply_to?, attachments: [...]}` → 201
|
|
- **Draft:** POST `/api/mail/drafts` → speichert Entwurf
|
|
- **Reply:** POST `/api/mail/{id}/reply` → erstellt Reply mit vorausgefüllten Feldern
|
|
- **Forward:** POST `/api/mail/{id}/forward` → erstellt Forward
|
|
- **HTML-Sanitization:** DOMPurify
|
|
|
|
### F-MAIL-03: Volltext-Suche
|
|
- **Endpoint:** GET `/api/mail/search?q=angebot&folder=inbox&date_from=...&date_to=...` → 200 mit `{items: [{id, subject, from, date, snippet, folder}], total}`
|
|
- **Index:** PostgreSQL tsvector-Index über `mail_body_tsv` und `mail_subject_tsv`
|
|
- **Performance:** <500ms bei 10.000 Mails
|
|
|
|
### F-MAIL-04: Anhänge
|
|
- **Send:** POST `/api/mail/send` mit multipart-Form-Data für Anhänge
|
|
- **Download:** GET `/api/mail/{id}/attachments/{att_id}` → 200
|
|
- **Content-Disposition:** `attachment` für Downloads, `inline` für inline-Bilder
|
|
- **Max:** 25 MB pro Anhang, 50 MB pro Mail
|
|
|
|
### F-MAIL-05: Threading
|
|
- **Endpoint:** GET `/api/mail/threads?folder=inbox` → 200 mit `{items: [{thread_id, subject, participants, message_count, last_date, messages: [{id, from, date, snippet}]}]}`
|
|
- **Basis:** `In-Reply-To`/`References`-Header (RFC 5322)
|
|
- **Frontend:** `ThreadView` Komponente
|
|
|
|
### F-MAIL-06: Vorlagen/Templates
|
|
- **Create:** POST `/api/mail/templates` mit `{name, subject, body_html, shared: false}` → 201
|
|
- **List:** GET `/api/mail/templates?scope=me|shared` → 200
|
|
- **Compose:** POST `/api/mail/compose?template_id=N&contact_id=M&company_id=K` → 200 mit aufgelösten Platzhaltern
|
|
- **Syntax:** `{{entity.field}}`
|
|
|
|
### F-MAIL-07: Filter/Regeln
|
|
- **Create:** POST `/api/mail/rules` mit `{name, conditions: [{field, operator, value}], actions: [{type, value}]}` → 201
|
|
- **Evaluation:** Background-Worker nach IMAP-IDLE-Trigger
|
|
- **List:** GET `/api/mail/rules` → 200
|
|
- **Delete:** DELETE `/api/mail/rules/{id}` → 204
|
|
|
|
### F-MAIL-08: Abwesenheitsnotiz
|
|
- **Endpoint:** POST `/api/mail/vacation` mit `{active: true, subject, body, start_date, end_date}` → 201
|
|
- **Background-Worker:** prüft eingehende Mails, `vacation_sent_log` Tabelle (einmal pro Absender)
|
|
- **No-Reply-Erkennung:** Absender enthält „noreply", „no-reply", „donotreply"
|
|
|
|
### F-MAIL-09: Labels/Flags
|
|
- **Flag:** PATCH `/api/mail/{id}/flags` mit `{starred: true}` → 200, setzt IMAP \\Flagged
|
|
- **Label:** POST `/api/mail/{id}/labels` mit `{label_id: N}` → 201
|
|
- **Filter:** GET `/api/mail?label=vertrieb` → 200
|
|
- **Label-Verwaltung:** POST `/api/mail/labels` mit `{name, color}`
|
|
|
|
### F-MAIL-10: Kontakt-Verknüpfung
|
|
- **Contact-Mails:** GET `/api/contacts/{id}/emails` → 200 mit `{items: [{id, subject, from, date, direction: "in|out", thread_id}]}`
|
|
- **Company-Mails:** GET `/api/companies/{id}/emails` → 200
|
|
- **Auto-Verknüpfung:** Background-Job matcht Absender/Empfänger gegen `contacts.email`-Feld
|
|
- **Manual:** POST `/api/mail/{id}/link` mit `{contact_id, company_id}`
|
|
|
|
### F-MAIL-11: Kalender-Integration
|
|
- **Endpoint:** POST `/api/mail/{id}/create-event` mit `{date, time, duration, calendar_id?, title?}` → 201
|
|
- **Defaults:** Titel = Mail-Betreff, Beschreibung = Mail-Body (plain text), Teilnehmer = Mail-Absender
|
|
- **Link:** `calendar_entry.source_mail_id = mail.id`
|
|
|
|
### F-MAIL-12: PGP-Verschlüsselung
|
|
- **Import Private Key:** POST `/api/mail/pgp/keys` mit `{private_key, passphrase}` → 201 (verschlüsselt gespeichert)
|
|
- **Import Public Key:** POST `/api/contacts/{id}/pgp-key` mit `{public_key}` → 201
|
|
- **Encrypt Send:** POST `/api/mail/send` mit `{encrypt: true}` → python-gnupg Verschlüsselung
|
|
- **Decrypt:** GET `/api/mail/{id}` → erkennt PGP-Block, entschlüsselt bei vorhandener Passphrase (Session-Cache)
|
|
|
|
### F-MAIL-13: Signaturen
|
|
- **Create:** POST `/api/mail/signatures` mit `{name, body_html}` → 201
|
|
- **Assign Default:** PATCH `/api/mail/accounts/{id}` mit `{default_signature_id: N}` → 200
|
|
- **Compose:** POST `/api/mail/compose?account_id=N` → Response enthält `signature`-Feld
|
|
|
|
### F-MAIL-14: Mehrere Postfächer
|
|
- **Create:** POST `/api/mail/accounts` mit `{name, imap_host, imap_port, imap_ssl, smtp_host, smtp_port, smtp_ssl, username, password}` → 201 (Passwort verschlüsselt)
|
|
- **List:** GET `/api/mail/accounts` → 200
|
|
- **Default:** PATCH `/api/users/me/settings` mit `{default_mail_account_id: N}`
|
|
|
|
### F-MAIL-15: Geteilte Postfächer
|
|
- **Create:** POST `/api/mail/accounts` mit `{type: "shared", name: "info@firma.de", ...}` → 201
|
|
- **Assign Users:** POST `/api/mail/accounts/{id}/users` mit `{user_ids: [1, 2, 3]}` → 200
|
|
- **List:** GET `/api/mail/accounts/shared` → 200
|
|
- **Seen-By:** `mail_seen_by` Tabelle (mail_id, user_id, seen_at)
|
|
|
|
### F-MAIL-16: Stellvertretung
|
|
- **Delegate:** POST `/api/mail/accounts/{id}/delegates` mit `{delegate_user_id, permission: "read|full"}` → 201
|
|
- **Remove:** DELETE `/api/mail/accounts/{id}/delegates?user_id=N` → 204
|
|
- **List:** GET `/api/mail/accounts?include_delegated=true` → 200 mit `delegated: true` Markierung
|
|
|
|
### F-MAIL-17: Sende-Berechtigungen
|
|
- **Grant:** POST `/api/mail/accounts/{id}/send-permissions` mit `{user_ids: [...]}` → 200
|
|
- **List:** GET `/api/mail/accounts/{id}/send-permissions` → 200
|
|
- **Send-Check:** POST `/api/mail/send` mit `{from_account_id: N}` → prüft, 403 wenn nicht
|
|
|
|
### F-MAIL-18: Postfach-Konfiguration
|
|
- **Create:** POST `/api/mail/accounts` mit `{imap_host, imap_port, imap_ssl: true, smtp_host, smtp_port, smtp_starttls: true, username, password}`
|
|
- **Validierung:** IMAP-Login + SMTP-Login, 422 bei Fehler
|
|
- **Verschlüsselung:** AES-256, Key via Env-Var `MAIL_ENCRYPTION_KEY`
|
|
- **Maskierung:** GET `/api/mail/accounts/{id}` → Passwort-Feld ist `***`
|
|
|
|
### F-MAIL-19: Mail-Ordner verwalten
|
|
- **Create:** POST `/api/mail/folders` mit `{name, parent_id?}` → 201, führt IMAP CREATE aus
|
|
- **Rename:** PATCH `/api/mail/folders/{id}` mit `{name}` → 200, führt IMAP RENAME aus
|
|
- **Delete:** DELETE `/api/mail/folders/{id}` → 204, führt IMAP DELETE aus
|
|
|
|
---
|
|
|
|
## 14. Mail-Modul: Technische Constraints (vollständige Tabelle)
|
|
|
|
| Constraint | Beschreibung |
|
|
|-----------|-------------|
|
|
| IMAP-Protokoll | IMAP4rev1 (RFC 3501) — Server muss IMAP IDLE (RFC 2177) für Push unterstützen |
|
|
| SMTP-Protokoll | SMTP Submission (RFC 6409) — Port 587 mit STARTTLS oder Port 465 mit SSL |
|
|
| Auth | SMTP: PLAIN/LOGIN mit Benutzername+Passwort; IMAP: PLAIN/LOGIN |
|
|
| Attachment-Storage | Anhänge lokal gespeichert (DMS-Integration), nicht im IMAP-Server belassen (Caching) |
|
|
| IMAP IDLE | Real-time Push; Fallback: Polling alle 5 Min |
|
|
| Volltext-Suche | PostgreSQL Full-Text Search (tsvector) über Mail-Body + Anhang-Namen (OCR optional post-MVP) |
|
|
| HTML-Rendering | Sanitization mit DOMPurify (XSS-Schutz); Plain-Text-Fallback |
|
|
| Verschlüsselung | PGP (OpenPGP, RFC 4880) via python-gnupg; S/MIME post-MVP |
|
|
| Mail-Sync | Bidirektionale Sync: IMAP-Flags (Seen/Flagged) synchronisiert |
|
|
| Ordner-Mapping | INBOX→Posteingang, Sent→Postausgang, Drafts→Entwürfe, Spam/Junk→Spam |
|
|
| Passwort-Speicherung | IMAP/SMTP-Passwörter AES-256 verschlüsselt, Key via Env-Var MAIL_ENCRYPTION_KEY |
|
|
| Max Anhang-Größe | 25 MB pro Anhang, 50 MB pro Mail |
|
|
|
|
---
|
|
|
|
## 15. Mail-Modul: Annahmen
|
|
|
|
1. **IMAP-Server-Kompatibilität:** IMAP4rev1 (RFC 3501), getestet mit Dovecot, Courier IMAP.
|
|
2. **SMTP-Auth:** SMTP-Server muss Authentifizierung unterstützen (PLAIN/LOGIN). Kein Open-Relay.
|
|
3. **IMAP IDLE:** Server muss RFC 2177 unterstützen. Fallback: Polling alle 5 Minuten.
|
|
4. **IMAP MOVE:** Server muss RFC 6851 unterstützen. Fallback: COPY + STORE \\Deleted + EXPUNGE.
|
|
5. **Attachment-Storage:** Lokal (Dateisystem oder S3), Caching in PostgreSQL.
|
|
6. **Maximale Anhang-Größe:** 25 MB pro Anhang, 50 MB pro Mail.
|
|
7. **Passwort-Speicherung:** AES-256 verschlüsselt, Key via `MAIL_ENCRYPTION_KEY`.
|
|
8. **HTML-Sanitization:** DOMPurify, Inline-Styles erlaubt, Scripts/IFrames entfernt.
|
|
9. **Multi-Tenant (Multi-Company):** Mail-Modul ist Multi-Tenant (Multi-Company).
|
|
10. **PGP-Key-Verwaltung:** Private Keys verschlüsselt (AES-256 + Passphrase). Passphrase nicht gespeichert, nur Session-Cache.
|
|
|
|
---
|
|
|
|
## 16. Implizite DB-Tabellen (aus allen Akzeptanzkriterien extrahiert)
|
|
|
|
| Tabelle | Modul | Zweck |
|
|
|---------|-------|-------|
|
|
| `users` | Core | User (email, name, role, password_hash, default_calendar_id, default_mail_account_id) |
|
|
| `companies` | Core | Firmen (siehe Feld-Tabelle + deleted_at, created_at) |
|
|
| `contacts` | Core | Kontakte (siehe Feld-Tabelle + deleted_at, created_at) |
|
|
| `company_contacts` | Core | N:M-Verknüpfung (company_id, contact_id) |
|
|
| `audit_log` | Core | Audit (user, action, entity, entity_id, timestamp) |
|
|
| `deletion_log` | Core | Unveränderliche DSGVO-Lösch-Logs |
|
|
| `dms_folders` | DMS | Ordner (id, name, parent_id, owner_id, deleted_at) |
|
|
| `dms_files` | DMS | Dateien (id, name, folder_id, size, mime_type, uploaded_at, modified_at, uploaded_by, deleted_at) |
|
|
| `file_links` | DMS | Verknüpfung (file_id, entity_type, entity_id) — Unique Constraint |
|
|
| `tags` | DMS | Tags (id, name, color) |
|
|
| `tag_assignments` | DMS | Zuordnung (entity_type, entity_id, tag_id) — Unique Constraint |
|
|
| `folder_permissions` | DMS | (folder_id, group_id, permission: read|write) |
|
|
| `file_shares` | DMS | (file_id, user_id?, group_id?, permission) |
|
|
| `share_links` | DMS | (file_id, token, password?, expires_at?, download_only?) |
|
|
| `calendar_entries` | Kalender | (id, entry_type, title, description, start_at?, end_at?, due_date?, all_day?, location?, priority?, status?, assigned_to?, calendar_id, subtype, reminder, recurrence) |
|
|
| `calendars` | Kalender | (id, name, color, type, owner_id) |
|
|
| `calendar_entry_links` | Kalender | (entry_id, entity_type, entity_id) |
|
|
| `calendar_shares` | Kalender | (calendar_id, user_id?, group_id?, permission) |
|
|
| `user_calendar_visibility` | Kalender | (user_id, calendar_id, visible) |
|
|
| `subtasks` | Kalender | (entry_id, title, completed) |
|
|
| `resources` | Kalender | (id, name, type) — später |
|
|
| `resource_bookings` | Kalender | (resource_id, entry_id, from, to) — später |
|
|
| `mail_accounts` | Mail | (id, user_id, name, imap_host, imap_port, imap_ssl, smtp_host, smtp_port, smtp_ssl/starttls, username, password_encrypted, type) |
|
|
| `mails` | Mail | (id, account_id, folder_id, subject, body_html, body_text, from, to, cc, bcc, date, in_reply_to, references, thread_id, seen, flagged, has_attachments) |
|
|
| `mail_attachments` | Mail | (mail_id, filename, mime_type, size, dms_file_id) |
|
|
| `mail_folders` | Mail | (id, account_id, name, type, parent_id, unread_count, total_count) |
|
|
| `mail_labels` | Mail | (id, name, color) |
|
|
| `mail_label_assignments` | Mail | (mail_id, label_id) |
|
|
| `mail_rules` | Mail | (id, account_id, name, conditions, actions, priority) |
|
|
| `mail_templates` | Mail | (id, user_id, name, subject, body_html, shared) |
|
|
| `mail_signatures` | Mail | (id, user_id, name, body_html) |
|
|
| `vacation_sent_log` | Mail | (account_id, sender_address, sent_at) |
|
|
| `mail_seen_by` | Mail | (mail_id, user_id, seen_at) |
|
|
| `mail_account_delegates` | Mail | (account_id, delegate_user_id, permission) |
|
|
| `mail_account_send_permissions` | Mail | (account_id, user_id) |
|
|
| `pgp_keys` | Mail | (user_id, private_key_encrypted, public_key) |
|
|
| `contact_pgp_keys` | Mail | (contact_id, public_key) |
|
|
| `notifications` | Core | (id, user_id, type, title, body, created_at, read_at) |
|
|
|
|
---
|
|
|
|
## 17. Historische v0.1 Endpoints (archiviert)
|
|
|
|
| Endpoint | Historisch | Aktuell |
|
|
|----------|-----------|--------|
|
|
| POST `/login` | F-1 | POST `/api/auth/login` |
|
|
| POST `/logout` | F-2 | POST `/api/auth/logout` |
|
|
| GET `/companies` | F-5 | GET `/api/companies` |
|
|
| POST `/companies` | F-5 | POST `/api/companies` |
|
|
| DELETE `/companies/{id}` | F-8 | DELETE `/api/companies/{id}` |
|
|
| POST `/contacts` | F-10 | POST `/api/contacts` |
|
|
| GET `/api/health` | F-15 | GET `/api/health` |
|
|
|
|
### v0.1 Demo-Seed
|
|
- Beim ersten Start: 1 Admin (`admin`/`admin`), 2 Firmen, 3 Kontakte
|
|
- Frische DB → Seed-Daten vorhanden
|
|
- DB bereits befüllt → Seed überspringt
|
|
|
|
---
|
|
|
|
## 18. Non-Goals (alle Module, vollständig)
|
|
|
|
### v1 Non-Goals
|
|
1. Self-Registration (nur Admin legt User an)
|
|
2. ~~Multi-Tenant (Single-Tenant in v1)~~ — Multi-Tenant (Multi-Company) ist v1-Feature
|
|
3. Sales Pipeline / Deals
|
|
4. Kampagnen-Management
|
|
5. Mobile App (Native) — nur Responsive Web-UI
|
|
6. Offline-Support
|
|
7. AI-Features (Lead-Scoring, Auto-Enrichment)
|
|
8. Real-time Collaboration (Google Docs-style)
|
|
9. Webhooks für externe Systeme
|
|
10. Multi-Currency
|
|
11. Advanced Analytics/Dashboards (BI)
|
|
12. SSO/OAuth (nur E-Mail/Passwort)
|
|
13. Custom Fields (Standard-Felder fest definiert)
|
|
14. Two-Factor Auth (post-MVP)
|
|
15. PWA
|
|
|
|
### v2 DMS Non-Goals
|
|
1. Versionierung (History, Diff)
|
|
2. Volltext-Suche in Dokumenten (kein OCR, kein Volltext-Index)
|
|
3. Eigene Office-Suite (nur OnlyOffice)
|
|
4. E-Mail-Attachment aus DMS
|
|
5. Externes Sync (WebDAV/Nextcloud)
|
|
6. Watermarking
|
|
|
|
### v2 Kalender Non-Goals
|
|
1. Ressourcen-Booking (nur optional markiert)
|
|
2. Time-Tracking
|
|
3. Task-Templates
|
|
4. Automatisierte Task-Erstellung aus Triggern
|
|
5. Gantt-Diagramm
|
|
6. Öffentliche Kalender-Sync (außer ICS)
|
|
|
|
### v2 Mail Non-Goals
|
|
1. Google/Microsoft API (nur direkte IMAP/SMTP)
|
|
2. S/MIME (nur PGP in v2-mail)
|
|
3. Mail-Server-Hosting (nur Client)
|
|
4. Mailinglisten-Management
|
|
5. Newsletter-Tool
|
|
6. Mail-to-Ticket
|
|
7. OCR für Anhänge
|
|
8. Kalender-Einladungen per Mail (iMIP, RFC 6047)
|
|
9. JMAP (RFC 8620)
|
|
10. Push-Benachrichtigungen (Web Push) — nur In-App
|
|
|
|
---
|
|
|
|
**Ende der extrahierten Architektur-Details.**
|