# LeoCRM — Umfassender Fix-Plan > Erstellt: 2026-07-25 > Quellen: Externes Audit (geprüft), eigene Code-Inspektion, Coolify-Deployment-Prüfung --- ## P0 — Sofort blockierend (vor jeder Nutzung) ### P0-1: Authentifizierungs-Bypass entfernen **Problem:** `app/deps.py` akzeptiert `X-Internal-Call: true` mit `X-Tenant-Id` und `X-User-Id` Headern. Keine Signatur, kein Token, keine IP-Beschränkung. `except (ValueError, Exception): pass` verschleiert Fehler. **Datei:** `app/deps.py:37-58` **Maßnahme:** - Header-Authentifizierung komplett entfernen - Für interne Service-Kommunikation: dedizierte Service-Accounts mit kurzlebigen signierten Tokens (JWT mit `aud`, `iss`, `sub`, `tenant_id`, `exp`) - Separate interne API oder mTLS - Keine Übernahme beliebiger `user_id` aus einem Header - Audit-Logging jeder Delegation - `except (ValueError, Exception): pass` ersetzen durch spezifisches Exception-Handling mit Logging **Aufwand:** 2-4 Stunden --- ### P0-2: Destruktive Migrationen ersetzen **Problem:** - `alembic/versions/0021_unified_contacts.py`: `DROP TABLE` ohne Datenübernahme - `alembic/versions/0027_unify_company_to_contact.py`: `company_id` wird gelöscht ohne Datenübernahme; Downgrade ändert pauschal alle `entity_type='contact'` zurück zu `'company'` - `migration_0021.sql` im Projekt-Root: konkurrierender Migrationsweg, manipuliert `alembic_version` direkt **Dateien:** - `alembic/versions/0021_unified_contacts.py` - `alembic/versions/0027_unify_company_to_contact.py` - `migration_0021.sql` (löschen) **Maßnahme:** 1. `migration_0021.sql` löschen 2. Migration 0021 durch echte Transformationsmigration ersetzen: - Alte Tabellen umbenennen (`_old` suffix), nicht löschen - Daten mit `INSERT ... SELECT` übertragen - Anzahl, Checksummen und Plausibilität vor/nach der Migration vergleichen - Alttabellen erst in späterer Migration entfernen 3. Migration 0027 korrigieren: - `company_id` Werte vor Drop in `contact_id` übertragen - Downgrade: nur Datensätze zurückändern, die ursprünglich `'company'` waren (Tracking-Spalte oder separate Tabelle) 4. Automatisierten Upgrade-Test von jeder unterstützten Version auf `head` einführen 5. Migrationen gegen reale anonymisierte DB-Kopien testen **Aufwand:** 4-8 Stunden --- ### P0-3: Plugin-Upload und URL-Installation deaktivieren **Problem:** `app/routes/plugins.py` führt `spec.loader.exec_module(module)` aus **bevor** die Sicherheitsprüfung läuft. Das ist Remote Code Execution. Weitere Probleme: unzureichende ZIP-Traversal-Prüfung, kein Symlink-Check, keine ZIP-Bomb-Prävention, SSRF bei URL-Installation, Plugin wird in laufenden Container kopiert. **Datei:** `app/routes/plugins.py:347-354` (`_extract_plugin_from_zip`) **Maßnahme:** 1. **Sofort:** Upload- und URL-Installationsendpunkte (`/upload`, `/install-url`) deaktivieren oder entfernen 2. **Langfristig — Vertrauensmodell:** - Nur signierte Plugin-Artefakte aus einer Allowlist - Plugin-Code wird vor der Ausführung auf Signatur geprüft 3. **Langfristig — Isolationsmodell:** - Plugin-Ausführung in separaten Containern mit minimalen Rechten - Versionierte Plugin-API 4. ZIP-Traversal-Prüfung korrigieren: `os.path.abspath` gegen Base-Dir prüfen nach Extraction 5. Symlink-Check hinzufügen 6. Entpackungsgrößen-Limit (Anzahl Dateien + Gesamtgröße) 7. URL-Download: Redirects verbieten, interne IP-Ranges blockieren, Streaming statt RAM **Aufwand:** Sofort-Deaktivierung 30 Min; Langfristig 2-3 Tage --- ### P0-4: Mandantentrennung (RLS) reparieren **Problem:** - `alembic/versions/0015_rls_policies.py`: Kein `FORCE ROW LEVEL SECURITY`, kein `WITH CHECK` - Tabellen-Owner umgeht RLS - Plugin-Tabellen nicht in RLS-Liste - `TenantMixin` Docstring behauptet ORM-Autofilterung, die nicht existiert - `app/core/tenant.py` hat nur manuelle `apply_tenant_filter()` Funktion - `contactpersons` hat `tenant_id` aber FK auf `contacts.id` ohne Tenant-Bedingung → Cross-Tenant-FK möglich **Dateien:** - `alembic/versions/0015_rls_policies.py` - `app/core/db/__init__.py` (TenantMixin Docstring) - `app/core/tenant.py` - Neue Migration für FORCE + WITH CHECK **Maßnahme:** 1. Neue Migration: `ALTER TABLE ... FORCE ROW LEVEL SECURITY` für alle Tenant-Tabellen 2. Policies mit `USING` und `WITH CHECK` neu erstellen 3. Separater DB-Migrationsowner; Runtime-User ohne Owner- oder Bypass-RLS-Rechte 4. RLS für alle mandantenbezogenen Tabellen, einschließlich Plugin-Tabellen 5. CI-Test: Cross-Tenant-Lese- und Schreibversuche 6. Composite-Integrität: eindeutiges `(tenant_id, id)` und FK auf `(tenant_id, contact_id)` 7. `TenantMixin` Docstring korrigieren: Autofilterung existiert nicht 8. Zentralen Query-/Repository-Mechanismus einführen statt freiwilliger Tenant-Filter 9. Später neu erstellte Tabellen automatisch erfassen (Event-Listener oder CI-Check) **Aufwand:** 1-2 Tage --- ### P0-5: Plugin-System Doppelregistrierung beheben **Problem:** - `app/main.py` `create_app()` registriert alle Plugin-Routen unabhängig vom Aktivierungsstatus - `lifespan()` registriert dieselben Routen nochmal → Doppelregistrierung - `lifespan()` auto-installiert und auto-aktiviert alle Builtins bei jedem Start - Deaktivierte Plugins werden reaktiviert - `registry._plugins` wird direkt zugegriffen (private Feld) - Migrationsfehler werden nur geloggt, Aktivierung wird trotzdem versucht - 204 direkte Cross-Imports zwischen Built-in-Plugins **Datei:** `app/main.py:317-330` und `app/main.py:112-165` **Maßnahme:** 1. Routen **einmalig** beim Prozessstart registrieren — entweder in `create_app()` ODER in `lifespan()`, nicht beides 2. Aktivierungsstatus vor dem Router-Aufbau laden und respektieren 3. Keine dynamische Änderung von FastAPI-Routen während des Betriebs 4. Aktivierung/Deaktivierung erfordert kontrollierten Neustart 5. Fehlgeschlagene Migration blockiert den Start (nicht nur loggen) 6. Core-Module und optionale Module klar trennen 7. Kein Zugriff auf `registry._plugins` — öffentliche API verwenden 8. Plugin-Abhängigkeiten über deklarierte Contracts prüfen 9. **Langfristig:** Cross-Imports reduzieren — öffentliche Schnittstellen statt direkter Modell-Imports **Aufwand:** 1 Tag für Doppelregistrierung; Cross-Import-Reduktion 1-2 Wochen --- ### P0-6: Persistent Volume für Coolify-Deployment **Problem:** Der laufende Container hat **keine Volume-Mounts** (`[]`). `/data/storage` ist nicht persistent. Alle hochgeladenen Dateien (DMS, Attachments, Bilder) gehen bei jedem Redeployment verloren. Plugin-Dateien in `app/plugins/builtins/` überleben keinen Neustart. **Gefunden in:** Coolify-Container-Inspect (live) **Maßnahme:** 1. In Coolify persistentes Volume für `/data/storage` konfigurieren 2. Alternativ: S3-kompatiblen Object Storage verwenden (`.env.example` hat bereits `STORAGE_BACKEND=s3` Support) 3. Plugin-Dateien nicht in Container-Filesystem kopieren — separate Plugin-Registry mit DB-basierter Konfiguration **Aufwand:** 1-2 Stunden (Volume in Coolify konfigurieren) --- ### P0-7: App von öffentlicher Domain nehmen **Problem:** Die App läuft unter `https://crm.media-on.de` und ist öffentlich erreichbar — mit allen P0-Schwachstellen (Auth-Bypass, Plugin-RCE, XSS, etc.). **Gefunden in:** Coolify-Deployment-Prüfung **Maßnahme:** 1. **Sofort:** App von öffentlicher Domain nehmen oder IP-Whitelist/Basic Auth vorschalten 2. Mindestens P0-1 (Auth-Bypass) und P0-3 (Plugin-Upload) beheben bevor wieder öffentlich 3. Alternativ: VPN/Tunnel-Zugang statt öffentliche Domain **Aufwand:** 30 Minuten --- ## P1 — Vor Nutzung realer Kundendaten ### P1-1: Benutzer- und Mandantenmodell bereinigen **Problem:** - `User` hat `tenant_id`, `role`, `role_id` — gleichzeitig existiert `UserTenant` mit `tenant_id`, `role_id`, `is_default` - Zwei Quellen der Wahrheit für Mandantenzugehörigkeit und Rollen - `login()` sucht nur nach `email` mit `scalar_one_or_none()` → crasht bei mehreren Treffern (gleiche E-Mail in mehreren Mandanten) - `tenant_slug` Parameter in `login()` wird von Login-Route nicht übergeben - `TenantService.list_tenant_users()` sucht über `User.tenant_id` und ignoriert N:M-Mitgliedschaften **Dateien:** - `app/models/user.py` - `app/services/auth_service.py:30-80` - `app/routes/auth.py` **Maßnahme:** 1. `users.email` global eindeutig machen (nicht `(tenant_id, email)`) 2. `User.tenant_id` und `User.role`/`User.role_id` entfernen 3. `tenant_memberships` als einzige Quelle: `tenant_id`, `user_id`, `role_id`, `status`, `is_default` 4. `login()` mit `tenant_slug` verknüpfen oder Default-Tenant verwenden 5. `TenantService.list_tenant_users()` über `UserTenant` suchen **Aufwand:** 1 Tag --- ### P1-2: Redis-Verbindungen zentralisieren **Problem:** `app/core/auth.py:49-51` erstellt pro Aufruf einen neuen Redis-Client. Kein Pool, kein Close. Dasselbe bei `enqueue_job()` für ARQ-Pools. Folgen: Connection-Lecks, Socket-Erschöpfung, instabiles Verhalten unter Last. **Datei:** `app/core/auth.py:49-51`, `app/core/worker.py` (enqueue_job) **Maßnahme:** 1. Redis-Client einmal im Application-Lifespan initialisieren 2. Bei Shutdown schließen 3. Über Dependency Injection verteilen 4. ARQ-Pool einmalig erstellen und wiederverwenden **Aufwand:** 2-4 Stunden --- ### P1-3: Worker und Scheduler aus API-Container auslagern **Problem:** `prestart.sh` startet ARQ-Worker im Hintergrund und Uvicorn als PID 1. Worker-Tod wird nicht erkannt. Worker und API konkurrieren um Ressourcen. Keine separate Skalierung. Cron-Jobs können bei mehreren Replikas mehrfach ausgeführt werden. **Datei:** `prestart.sh` **Maßnahme:** 1. Worker in separaten Container auslagern 2. Scheduler in separaten Container mit verteilter Lock-/Leader-Election 3. Idempotente Jobs 4. Heartbeat mit Zeitstempel 5. Dead-Letter-/Failed-Job-Strategie 6. Retry-Policy pro Jobtyp 7. Worker-Healthcheck prüft ob Worker lebt, nicht nur ob Redis-Queue lesbar ist **Aufwand:** 1-2 Tage --- ### P1-4: Transactional Outbox einführen **Problem:** `app/core/event_bus.py` ist rein speicherbasiert. Events verschwinden bei Prozessabsturz, Neustart, mehreren Replikas, Handler-Fehlern. `asyncio.gather(..., return_exceptions=True)` sammelt Fehler ohne Behandlung. **Datei:** `app/core/event_bus.py` **Maßnahme:** 1. Transactional Outbox in PostgreSQL 2. Worker verarbeitet Outbox-Einträge 3. Inbox/Idempotency-Key auf Konsumentenseite 4. Retry und Dead Letter 5. Events versionieren 6. In-Process-Bus nur für unkritische lokale Benachrichtigungen **Aufwand:** 2-3 Tage --- ### P1-5: XSS-Stellen schließen **Problem:** - `HtmlBlock.tsx`: Regex-Sanitizer + `dangerouslySetInnerHTML` — HTML lässt sich nicht sicher mit Regex sanitizen - `SignatureManager.tsx:201`: `dangerouslySetInnerHTML={{ __html: sig.body_html }}` **ohne jegliche Sanitization** - `ActionCardBlock.tsx:21-28`: `window.open(action.action)` ohne URL-Validierung — `javascript:`-URLs möglich - Mail-Service: `body_html_sanitized = body_html` ohne Sanitizer an manchen Stellen **Dateien:** - `frontend/src/components/comm/blocks/HtmlBlock.tsx` - `frontend/src/components/mail/SignatureManager.tsx` - `frontend/src/components/comm/blocks/ActionCardBlock.tsx` - Mail-Service (body_html_sanitized) **Maßnahme:** 1. Serverseitig konsequent `nh3` verwenden 2. Frontend zusätzlich `DOMPurify` als zweite Barriere 3. Keine selbst gebauten Regex-Sanitizer 4. Nur `https:` und kontrollierte interne Pfade erlauben 5. Strikte Content Security Policy ohne `unsafe-inline` 6. Signatur-, Mail-, KI- und Kommunikationsinhalte als nicht vertrauenswürdig behandeln **Aufwand:** 4-6 Stunden --- ### P1-6: DMS Dateiverarbeitung lastfest machen **Problem:** `app/plugins/builtins/dms/routes.py` liest die komplette Datei in RAM (`content = await file.read()`). Max 100 MB. Bei 10 parallelen Uploads mehrere GB RAM. Kein Virenscan, kein Content-Hash, keine Dublettenerkennung, keine Tenant-Quotas, kein Versionierungsmodell, kein Garbage Collector für physische Dateien nach Soft Delete. `storage_path` wird an Frontend ausgegeben. Benutzerdateiname direkt in Content-Disposition. **Datei:** `app/plugins/builtins/dms/routes.py:421-436` **Maßnahme:** 1. Chunked Streaming direkt in Object Storage 2. Maximale Größe auf Proxy- und Anwendungsebene 3. SHA-256 Content-Hash 4. Malware-Scan 5. Quotas pro Tenant 6. Versionierte Metadaten 7. Garbage Collector für physische Dateien nach Soft Delete 8. `storage_path` nicht an Frontend ausgeben 9. Benutzerdateiname sanitizen vor Content-Disposition 10. Synchronen MinIO-Client aus `async def` entfernen **Aufwand:** 1-2 Tage --- ### P1-7: Berechtigungssystem vereinheitlichen **Problem:** - Legacy-Rollenstrings (`admin`/`editor`/`viewer`) + neue Rollen mit `role_id` + Gruppen + Allow/Deny + Feldrechte + `is_system_admin` + globale Write-Hilfsrechte - `permission_version` wird gespeichert, beim Cache-Lesen aber nicht geprüft - Cache-Invalidierung verwendet `redis.keys()` — blockiert Redis bei großen Datenmengen - Feldrechte mehrerer Gruppen werden per `dict.update()` überschrieben (last-write-wins) - `viewer` erhält `user_preferences:write` - `require_write()` erlaubt `*:write` oder `*:create` (zu breit) - `db.rollback()` bei Permission-Fehler setzt fremde Transaktionsarbeit zurück **Datei:** `app/core/permissions.py`, `app/deps.py` **Maßnahme:** 1. Nur noch Capability-basierte Berechtigungen (`contacts.read`, `contacts.create`, etc.) 2. Keine generische `require_write`-Freigabe 3. Alte Rollenlogik entfernen 4. Feldrechte deterministisch nach "strengstes Recht gewinnt" zusammenführen 5. `permission_version` beim Cache-Lesen prüfen 6. `redis.keys()` ersetzen durch `redis.scan()` oder gezielte Cache-Key-Invalidierung 7. `db.rollback()` nur in eigenen Transaktionskontext **Aufwand:** 1-2 Tage --- ### P1-8: Password Reset funktionsfähig machen **Problem:** `request_password_reset()` erstellt ein Token, speichert es in der DB, sendet es aber nicht. Nicht einmal geloggt. Die Variable `raw_token` wird nach Erstellung ignoriert. Die Route sagt "a reset link has been sent" — das ist fachlich falsch. Nach Passwortwechsel werden bestehende Sessions nicht widerrufen. **Datei:** `app/services/auth_service.py:159-200` **Maßnahme:** 1. Reset-Mail über echte Queue verschicken (ARQ-Worker) 2. Token nur einmal verwendbar 3. Alle Sessions des Benutzers nach Passwortänderung widerrufen 4. Sicherheitsereignis protokollieren 5. Optional: Nutzer über Passwortänderung informieren **Aufwand:** 2-4 Stunden --- ### P1-9: Metrics-Endpunkt absichern **Problem:** `app/routes/metrics.py` sagt "admin-only" im Docstring, verwendet aber nur `get_current_user` statt `require_admin`. Jeder angemeldete Benutzer kann Prometheus-Metriken abrufen. **Datei:** `app/routes/metrics.py` **Maßnahme:** 1. `require_admin` oder `require_permission("system:metrics")` verwenden 2. Alternativ: internes Netzwerk, Reverse-Proxy-Allowlist, dedizierten Monitoring-Token oder mTLS **Aufwand:** 30 Minuten --- ### P1-10: Coolify-Dokumentation korrigieren **Problem:** - `COOLIFY_SETUP.md` Abschnitt 6 dokumentiert `/health` als Healthcheck-Pfad — die App hat nur `/api/v1/health`. `/health` liefert nur die SPA `index.html` (Catch-All). - `COOLIFY_SETUP.md` listet `JWT_ALGORITHM` und `JWT_EXPIRY_HOURS` — werden von der App nicht verwendet. - `CORS_ORIGINS` in Coolify ohne `:443` — `COOLIFY_SETUP.md` sagt explizit Port ist mandatory. **Dateien:** `COOLIFY_SETUP.md`, `docs/deployment-guide.md` **Maßnahme:** 1. Healthcheck-Pfad in Doku auf `/api/v1/health` korrigieren 2. JWT-Variablen aus Doku entfernen oder App auf JWT umstellen 3. `CORS_ORIGINS` in Coolify auf `https://crm.media-on.de:443` setzen 4. `docker-compose.yml` Healthcheck auf `/api/v1/health` korrigieren 5. `docker-compose.yml` Redis-Service hinzufügen 6. `docker-compose.yml` `REDIS_URL` setzen 7. `docker-compose.yml` persistentes Volume für `/data/storage` 8. `docker-compose.yml` `SESSION_COOKIE_SECURE=true` für Production 9. `docker-compose.yml` `STORAGE_PATH=/data/storage` setzen 10. `config.py` Default `storage_path` von `/tmp` auf `/data/storage` ändern 11. `config.py` Default `session_cookie_secure` auf `True` ändern (Production-Default) 12. `config.py` Startup-Validierung: `ENVIRONMENT=production` + `session_cookie_secure=False` → harter Abbruch **Aufwand:** 2-3 Stunden --- ### P1-11: Cross-Tenant referenzielle Integrität **Problem:** `contactpersons` hat `tenant_id` aber `contact_id` FK referenziert nur `contacts.id` ohne Tenant-Bedingung. Die DB verhindert nicht, dass ein Contactperson-Datensatz aus Mandant A auf einen Kontakt aus Mandant B zeigt. **Datei:** `alembic/versions/0021_unified_contacts.py` (contactpersons Tabelle) **Maßnahme:** 1. Composite-FK: `(tenant_id, contact_id)` referenziert `(tenant_id, id)` auf `contacts` 2. Eindeutiges `(tenant_id, id)` auf `contacts` 3. Dasselbe für alle mandantenbezogenen FK-Beziehungen **Aufwand:** 2-4 Stunden --- ## P2 — Architektonische Konsolidierung ### P2-1: Unified Contact Model normalisieren **Problem:** Eine Tabelle enthält Unternehmen, Personen, 3 Adressarten, Bankdaten, Steuernummern, Rabatte, Projektinformationen, Warnungen, Tags, Custom Fields, Suchindex. Dubletten zu vorhandenen Modellen für Adressen, Bankkonten, Tags, Custom Fields. **Weitere Probleme:** - Rabatte als `Float` statt `Numeric`/`Decimal` - Keine DB-Checks für Werte 0-100 - Keine eindeutigen Kontakt-/Buchhaltungscodes pro Mandant - Keine klare Validierung welche Felder bei Person/Firma erlaubt sind - `surfix` — dauerhaft übernommener Tippfehler - `JSON` statt `JSONB` - Suche fest auf Deutsch eingestellt - Keine normalisierten Suchschlüssel für E-Mail und Telefonnummer - CSV-Import ohne Dubletten-/Encoding-/Dezimal-/Rollback-Strategie **Maßnahme:** 1. Adressen in separate Tabelle auslagern (bereits vorhanden — nutzen) 2. Bankdaten in separate Tabelle (bereits vorhanden — nutzen) 3. Tags als Relation (bereits vorhanden — nutzen) 4. Custom Fields als Relation (bereits vorhanden — nutzen) 5. Rabatte: `Numeric(5,2)` statt `Float` 6. DB-Check: `discount_* BETWEEN 0 AND 100` 7. Eindeutige `(tenant_id, code)` und `(tenant_id, accounting_code)` 8. `surfix` → `suffix` (Migration mit Rename) 9. `JSON` → `JSONB` 10. Suchkonfiguration pro Mandant konfigurierbar 11. Normalisierte Suchschlüssel (lowercase, trimmed) für E-Mail und Telefon 12. CSV-Import: Dubletten-Erkennung, Encoding-Detection, Decimal-Parsing, Transaction-Rollback **Aufwand:** 2-3 Tage --- ### P2-2: Plugin-Cross-Imports reduzieren **Problem:** 204 direkte `from app.plugins.builtins` Imports zwischen Plugins. Automatisierung importiert Modelle/Services von Kommunikation, Mail, Kalender. Verteilter Monolith ohne Modulgrenzen. **Maßnahme:** 1. Öffentliche Schnittstellen (Contracts) für jedes Modul definieren 2. Direkte Imports fremder Plugin-Modelle verbieten 3. Kommunikation nur über Events oder öffentliche Service-API 4. CI-Check: keine direkten Cross-Plugin-Imports **Aufwand:** 1-2 Wochen --- ### P2-3: Commands und Statusmaschinen **Problem:** Geschäftsoperationen als `Route → Service → mehrere flush/commit` statt als zentrale Commands. Statusstrings frei beschreibbar statt Statusmaschinen. **Maßnahme:** 1. `Route → Command → Authorization → Domain Operation → Transaction → Audit → Outbox Events → Commit` 2. Explizite Statusmaschinen für Angebote, Aufträge, Rechnungen 3. Übergänge validiert und auditiert **Aufwand:** 1-2 Wochen --- ### P2-4: SPA Path-Traversal-Schutz vervollständigen **Problem:** `app/main.py` SPA-Catch-All blockiert `..` nur in bestimmten Positionen. `..` in anderen Positionen wird nicht erfasst. **Datei:** `app/main.py` (spa_spa Funktion) **Maßnahme:** 1. `os.path.abspath` gegen `frontend_dist` prüfen nach Join 2. Kein `..` in irgendeiner Position erlauben **Aufwand:** 30 Minuten --- ## Zusammenfassung | Priorität | Anzahl | Geschätzter Aufwand | |---|---|---| | P0 (sofort) | 7 | ~5-7 Tage | | P1 (vor Kundendaten) | 11 | ~7-10 Tage | | P2 (architektonisch) | 4 | ~2-4 Wochen | | **Total** | **22** | **~4-6 Wochen** | ## Reihenfolge ### Woche 1: P0 absichern 1. P0-7: App von öffentlicher Domain nehmen (30 Min) 2. P0-1: Auth-Bypass entfernen (2-4h) 3. P0-3: Plugin-Upload deaktivieren (30 Min Sofort, langfristig später) 4. P0-6: Persistent Volume in Coolify (1-2h) 5. P0-2: Migrationen ersetzen (4-8h) 6. P0-4: RLS reparieren (1-2 Tage) 7. P0-5: Plugin-Doppelregistrierung beheben (1 Tag) ### Woche 2-3: P1 Fundament 8. P1-9: Metrics absichern (30 Min) 9. P1-8: Password Reset (2-4h) 10. P1-10: Coolify-Doku & Config korrigieren (2-3h) 11. P1-2: Redis zentralisieren (2-4h) 12. P1-5: XSS schließen (4-6h) 13. P1-11: Cross-Tenant FK (2-4h) 14. P1-1: User/Tenant-Modell (1 Tag) 15. P1-7: Permission-System (1-2 Tage) 16. P1-6: DMS lastfest (1-2 Tage) 17. P1-3: Worker auslagern (1-2 Tage) 18. P1-4: Transactional Outbox (2-3 Tage) ### Woche 4-6: P2 Architektur 19. P2-4: SPA Path-Traversal (30 Min) 20. P2-1: Contact Model normalisieren (2-3 Tage) 21. P2-2: Cross-Imports reduzieren (1-2 Wochen) 22. P2-3: Commands & Statusmaschinen (1-2 Wochen) --- ## Validierung nach jedem Fix - [ ] Python-Syntax-Check: `python -m py_compile app/**/*.py` - [ ] pytest: `pytest tests/ -x` - [ ] Frontend-Typecheck: `cd frontend && npx tsc --noEmit` - [ ] Frontend-Build: `cd frontend && npx vite build` - [ ] Manueller Smoke-Test: Login, Kontakt erstellen, DMS-Upload - [ ] Cross-Tenant-Test: Datensatz aus Mandant A kann nicht aus Mandant B gelesen werden - [ ] Deployment: Coolify Deploy + Healthcheck prüfen