diff --git a/docs/INSTALL.md b/docs/INSTALL.md new file mode 100644 index 0000000..9a7078c --- /dev/null +++ b/docs/INSTALL.md @@ -0,0 +1,492 @@ +# LeoCRM — Vollständige Installationsanleitung + +**Stand:** 2026-08-01 +**Commit:** be20a85 +**Alembic-Head:** 0090 + +Diese Anleitung beschreibt die komplette Installation von LeoCRM von Grund auf. +Keine manuellen Nacharbeiten erforderlich. Alle Schritte sind reproduzierbar. + +--- + +## Voraussetzungen + +- Coolify v4 (oder Docker + Docker Compose) +- PostgreSQL 16 mit pgvector-Extension +- Redis 7 +- Git-Zugang zum Forgejo-Repo +- Domain mit DNS-Eintrag + +--- + +## 1. Repository klonen + +```bash +git clone https://forgejo.media-on.de/Leopoldadmin/leocrm.git +cd leocrm +git checkout main +``` + +--- + +## 2. Docker-Image bauen + +```bash +docker build -t leocrm:latest . +``` + +**Dockerfile-Hinweise:** +- Verwendet `npm ci --legacy-peer-deps` (vite 8 peer dependency conflict) +- Frontend wird in Multi-Stage-Build gebaut +- Runtime-Image enthält: Python 3.12, Node.js, prestart.sh, worker.sh, healthcheck.sh + +--- + +## 3. Datenbankrollen + +LeoCRM verwendet 5 separate Datenbankrollen mit unterschiedlichen Berechtigungen. +Diese Rollen werden **automatisch** durch Migration 0085 erstellt. + +### Rollen-Übersicht + +| Rolle | Superuser | BYPASSRLS | Login | Verwendung | +|-------|----------|-----------|-------|-----------| +| crm_user | Ja | Ja | Ja | PostgreSQL-Container-Admin (POSTGRES_USER) | +| crm_migration | Nein | Ja | Ja | Alembic-Migrationen, Plugin-Migrationen (DDL) | +| crm_auth | Nein | Nein | Ja | Login, Authentifizierung, Password-Reset | +| crm_api | Nein | Nein | Ja | Normale API-Abfragen (SELECT, INSERT, UPDATE, DELETE) | +| crm_worker | Nein | Nein | Ja | ARQ-Worker, Outbox-Processing, Cron-Jobs | + +### Bootstrap-Reihenfolge + +``` +1. PostgreSQL-Container startet + → crm_user wird erstellt (POSTGRES_USER, SUPERUSER) + +2. prestart.sh läuft im API-Container + → alembic upgrade head (als crm_user über MIGRATION_DATABASE_URL) + → Migration 0085 erstellt crm_migration, crm_auth, crm_api, crm_worker + → Migration 0085 vergibt Grants und erstellt RLS-Policies + → prestart.sh setzt Passwörter für alle Rollen + +3. API startet (uvicorn) + → Verwendet DATABASE_URL (crm_api) für normale Abfragen + → Verwendet AUTH_DATABASE_URL (crm_auth) für Login + → Plugin-Migrationen über get_migration_engine() (crm_migration) + +4. Worker startet + → Verwendet WORKER_DATABASE_URL (crm_worker) für Jobs + → Plugin-Migrationen über get_migration_engine() (crm_migration) + → Event-Handler für aktive Plugins registriert +``` + +### WICHTIG: MIGRATION_DATABASE_URL + +Der erste Alembic-Lauf auf einer leeren Datenbank MUSS als `crm_user` ausgeführt werden, + weil `crm_migration` erst durch Migration 0085 erstellt wird. + +``` +MIGRATION_DATABASE_URL=postgresql+asyncpg://crm_user:@db:5432/crm_db +``` + +Nach Migration 0085 kann MIGRATION_DATABASE_URL auf `crm_migration` umgestellt werden, + aber für den Bootstrap-Prozess ist `crm_user` erforderlich. + +--- + +## 4. Environment-Variablen + +### API-Container + +| Variable | Wert | Beschreibung | +|----------|------|-------------| +| DATABASE_URL | postgresql+asyncpg://crm_api:PW@db:5432/crm_db | API-Abfragen (crm_api) | +| AUTH_DATABASE_URL | postgresql+asyncpg://crm_auth:PW@db:5432/crm_db | Login/Auth (crm_auth) | +| WORKER_DATABASE_URL | postgresql+asyncpg://crm_worker:PW@db:5432/crm_db | Worker-Jobs (crm_worker) | +| MIGRATION_DATABASE_URL | postgresql+asyncpg://crm_user:PW@db:5432/crm_db | Migrationen (crm_user für Bootstrap) | +| REDIS_URL | redis://default:PW@redis:6379/0 | Redis-Verbindung | +| SECRET_KEY | | Session-Verschlüsselung | +| ENVIRONMENT | production | Umgebung | +| STORAGE_PATH | /data/storage | Datei-Storage | +| FRONTEND_URL | https://crm.example.com | Frontend-URL | +| CORS_ORIGINS | https://crm.example.com | CORS-Konfiguration | +| SESSION_COOKIE_SECURE | true | HTTPS-Cookies | +| LOG_LEVEL | INFO | Logging-Level | + +### Worker-Container + +| Variable | Wert | Beschreibung | +|----------|------|-------------| +| DATABASE_URL | postgresql+asyncpg://crm_worker:PW@db:5432/crm_db | Worker-DB (crm_worker) | +| WORKER_DATABASE_URL | postgresql+asyncpg://crm_worker:PW@db:5432/crm_db | Worker-DB (crm_worker) | +| MIGRATION_DATABASE_URL | postgresql+asyncpg://crm_user:PW@db:5432/crm_db | Plugin-Migrationen (crm_user) | +| REDIS_URL | redis://default:PW@redis:6379/0 | Redis-Verbindung | +| SECRET_KEY | | Session-Verschlüsselung | +| ENVIRONMENT | production | Umgebung | +| STORAGE_PATH | /data/storage | Datei-Storage | + +### DB-Container + +| Variable | Wert | +|----------|------| +| POSTGRES_USER | crm_user | +| POSTGRES_PASSWORD | | +| POSTGRES_DB | crm_db | + +### WICHTIG: Alle DB-Passwörter sind identisch + +Migration 0085 erstellt die Rollen `crm_api`, `crm_auth`, `crm_worker`, `crm_migration` +mit demselben Passwort das in `MIGRATION_DATABASE_URL` für `crm_user` konfiguriert ist. +`prestart.sh` setzt anschließend die Passwörter für alle Rollen aus der `MIGRATION_DATABASE_URL`. + +Daher müssen alle `DATABASE_URL`, `AUTH_DATABASE_URL`, `WORKER_DATABASE_URL` +dasselbe Passwort verwenden wie `MIGRATION_DATABASE_URL`. + +--- + +## 5. Docker Compose + +### Vollständige docker-compose.yml + +```yaml +version: '3.8' + +services: + db: + image: pgvector/pgvector:pg16 + restart: unless-stopped + environment: + POSTGRES_USER: crm_user + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: crm_db + volumes: + - db-data:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U crm_user -d crm_db'] + interval: 5s + timeout: 10s + retries: 20 + + redis: + image: redis:7-alpine + restart: unless-stopped + command: redis-server --requirepass ${REDIS_PASSWORD} + volumes: + - redis-data:/data + + api: + image: leocrm:latest + restart: unless-stopped + expose: + - '8000' + environment: + DATABASE_URL: postgresql+asyncpg://crm_api:${DB_PASSWORD}@db:5432/crm_db + AUTH_DATABASE_URL: postgresql+asyncpg://crm_auth:${DB_PASSWORD}@db:5432/crm_db + WORKER_DATABASE_URL: postgresql+asyncpg://crm_worker:${DB_PASSWORD}@db:5432/crm_db + MIGRATION_DATABASE_URL: postgresql+asyncpg://crm_user:${DB_PASSWORD}@db:5432/crm_db + REDIS_URL: redis://default:${REDIS_PASSWORD}@redis:6379/0 + SECRET_KEY: ${SECRET_KEY} + ENVIRONMENT: production + STORAGE_PATH: /data/storage + FRONTEND_URL: https://crm.example.com + CORS_ORIGINS: https://crm.example.com + SESSION_COOKIE_SECURE: 'true' + LOG_LEVEL: INFO + volumes: + - api-storage:/data/storage + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:8000/api/v1/health'] + interval: 30s + timeout: 10s + retries: 3 + start_period: 180s + + worker: + image: leocrm:latest + restart: unless-stopped + entrypoint: /app/worker.sh + environment: + DATABASE_URL: postgresql+asyncpg://crm_worker:${DB_PASSWORD}@db:5432/crm_db + WORKER_DATABASE_URL: postgresql+asyncpg://crm_worker:${DB_PASSWORD}@db:5432/crm_db + MIGRATION_DATABASE_URL: postgresql+asyncpg://crm_user:${DB_PASSWORD}@db:5432/crm_db + REDIS_URL: redis://default:${REDIS_PASSWORD}@redis:6379/0 + SECRET_KEY: ${SECRET_KEY} + ENVIRONMENT: production + STORAGE_PATH: /data/storage + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + +volumes: + db-data: + redis-data: + api-storage: +``` + +### .env Datei + +```env +DB_PASSWORD=YourSecurePassword2026 +REDIS_PASSWORD=YourRedisPassword2026 +SECRET_KEY=your-secret-key-with-at-least-32-characters!! +``` + +--- + +## 6. Coolify-Setup + +### 6.1 Neue Anwendung erstellen + +1. In Coolify: **+ New Resource** → **Docker Compose** +2. Name: `leocrm` +3. Compose-Datei einfügen (siehe oben) +4. Domain zuweisen: `crm.example.com` + +### 6.2 Environment-Variablen in Coolify + +Alle Variablen aus der `.env`-Datei in Coolify als Environment-Variablen setzen. + +### 6.3 Deploy + +1. **Deploy** klicken +2. Warten bis API-Container healthy wird (start_period: 180s) +3. Worker-Container wird automatisch healthy + +### 6.4 WICHTIG: DB-Image + +Das DB-Image MUSS `pgvector/pgvector:pg16` sein, nicht `postgres:16-alpine`. +LeoCRM benötigt die `vector`-Extension für die unified_search-Plugin-Migration. + +--- + +## 7. Startup-Ablauf (prestart.sh) + +`prestart.sh` wird beim API-Container-Start ausgeführt: + +``` +1. Warten auf PostgreSQL (pg_isready) +2. alembic upgrade head (als crm_user über MIGRATION_DATABASE_URL) + → Migrationen 0001-0090 werden ausgeführt + → Migration 0085 erstellt DB-Rollen, RLS-Policies, Grants +3. Passwörter für alle Rollen setzen + → Extrahiert Passwort aus MIGRATION_DATABASE_URL + → SET PASSWORD für crm_api, crm_auth, crm_worker, crm_migration +4. Uvicorn starten +``` + +--- + +## 8. Admin-User anlegen + +Nach erfolgreichem Start: + +```bash +docker exec api-container python3 scripts/seed_admin.py +``` + +Erstellt: +- Tenant: "Default Org" (slug: default) +- Admin-Role mit permissions={"*:*": True} +- User: admin@media-on.de / Admin123! +- UserTenant-Link mit Admin-Role + +**Passwort ändern:** `ADMIN_PASSWORD` Environment-Variable setzen vor Ausführung. + +--- + +## 9. Verifikation + +### 9.1 Health-Check + +```bash +curl https://crm.example.com/api/v1/health +# Erwartet: {"status":"healthy",...} +``` + +### 9.2 Login-Test + +```bash +curl -X POST https://crm.example.com/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -H "Origin: https://crm.example.com" \ + -d '{"email":"admin@media-on.de","password":"Admin123!"}' +# Erwartet: 200 OK mit user_id, csrf_token, tenant_id +``` + +### 9.3 Alembic-Head prüfen + +```bash +docker exec api-container python3 -m alembic current +# Erwartet: 0090 (head) +``` + +### 9.4 RLS prüfen + +```sql +-- Als crm_api ohne Tenant-Kontext: 0 rows +SET ROLE crm_api; +SELECT count(*) FROM contacts; -- Erwartet: 0 +RESET ROLE; + +-- Als crm_api mit Tenant-Kontext: Tenant-Daten +SET ROLE crm_api; +SET app.current_tenant_id = ''; +SELECT count(*) FROM contacts; -- Erwartet: > 0 +RESET ROLE; +``` + +### 9.5 DDL durch crm_api blockiert + +```sql +SET ROLE crm_api; +CREATE TABLE test_block (id int); -- Erwartet: permission denied +RESET ROLE; +``` + +--- + +## 10. SMTP-Konfiguration (optional) + +Für Passwort-Reset-Emails: + +| Variable | Wert | +|----------|------| +| SMTP_HOST | mail.example.com | +| SMTP_PORT | 465 | +| SMTP_USER | noreply@example.com | +| SMTP_PASSWORD | | +| SMTP_FROM_EMAIL | noreply@example.com | +| SMTP_USE_TLS | true | + +**Wichtig:** Port 465 verwendet implicit TLS (nicht STARTTLS). + +--- + +## 11. Backup und Restore + +### 11.1 Backup erstellen + +```bash +pg_dump -U crm_user -d crm_db -F c -f crm_backup.dump +``` + +### 11.2 Restore + +```bash +# 1. Leere Datenbank erstellen +createdb -U crm_user crm_restore + +# 2. Restore (ohne ACLs, ohne Owner) +pg_restore -U crm_user -d crm_restore --no-owner --no-acl < crm_backup.dump + +# 3. Grants neu anwenden (pg_restore --no-acl überspringt Grants) +# Führe Migration 0085 Grants aus oder verwende das Grant-Skript + +# 4. Alembic auf neuesten Stand bringen +alembic upgrade head + +# 5. App gegen die wiederhergestellte DB starten und verifizieren +``` + +### 11.3 WICHTIG: Grants nach Restore + +`pg_restore --no-acl` überspringt GRANT-Statements. +Nach einem Restore müssen die Grants aus Migration 0085 neu angewendet werden. +Alternativ: `pg_restore` ohne `--no-acl` verwenden (erfordert korrekte Rollen). + +--- + +## 12. Häufige Probleme + +### Problem: "extension vector is not available" +**Ursache:** DB-Image ist `postgres:16-alpine` statt `pgvector/pgvector:pg16` +**Lösung:** DB-Image in docker-compose.yml ändern + +### Problem: "permission denied for schema public" +**Ursache:** crm_api versucht DDL auszuführen +**Lösung:** Plugin-Migrationen müssen über `get_migration_engine()` laufen (bereits implementiert) + +### Problem: "MIGRATION_DATABASE_URL is not set" +**Ursache:** MIGRATION_DATABASE_URL fehlt in Environment-Variablen +**Lösung:** MIGRATION_DATABASE_URL setzen (auf crm_user für Bootstrap) + +### Problem: Worker crasht beim Start +**Ursache:** PluginModel.is_active existiert nicht (alte Migration) +**Lösung:** Sicherstellen dass alle Migrationen bis 0090 ausgeführt wurden + +### Problem: Login gibt 401 zurück +**Ursache:** crm_auth hat keine SELECT-Rechte auf users/tenants +**Lösung:** Migration 0085 Grants prüfen, ggf. neu anwenden + +### Problem: RLS zeigt alle Daten ohne Tenant-Kontext +**Ursache:** FORCE RLS nicht aktiviert oder Rolle ist SUPERUSER +**Lösung:** `ALTER TABLE ... FORCE ROW LEVEL SECURITY` und Rolle NOSUPERUSER setzen + +--- + +## 13. Architektur-Übersicht + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ API (crm_api) │ │ Auth (crm_auth) │ │ Worker (crm_worker)│ +│ SELECT/INSERT/ │ │ Login/Logout │ │ ARQ-Jobs/Outbox │ +│ UPDATE/DELETE │ │ Tenant-Auflösung │ │ Cron-Jobs │ +│ Audit-Log │ │ Password-Reset │ │ Event-Handler │ +└────────┬─────────┘ └────────┬─────────┘ └────────┬───────────┘ + │ │ │ + │ ┌──────────────┐ │ │ + └───┤ PostgreSQL ├──────┘────────────────────────┘ + │ (RLS aktiv) │ + │ 108 Tabellen│ + │ Fail-closed │ + └──────┬───────┘ + │ + ┌──────┴───────┐ + │ Migration │ + │ (crm_migration)│ + │ BYPASSRLS │ + │ DDL-Operationen│ + └──────────────┘ +``` + +--- + +## 14. Datei-Struktur + +``` +leocrm/ +├── app/ +│ ├── core/ +│ │ ├── db/__init__.py # DB-Engines (api, auth, worker, migration) +│ │ ├── worker.py # ARQ-Worker-Konfiguration +│ │ ├── outbox.py # Transactional Outbox +│ │ ├── auth.py # Authentifizierung +│ │ └── middleware.py # CSRF, CORS, Tenant-Context +│ ├── plugins/ # Built-in Plugins +│ ├── routes/ # API-Routes +│ ├── services/ # Business-Logic +│ └── models/ # SQLAlchemy-Models +├── alembic/versions/ # Migrationen 0001-0090 +├── frontend/ # React 18 + TypeScript + Vite +├── scripts/ +│ ├── seed_admin.py # Admin-User erstellen +│ └── test_migrations.sh # Migrations-Test +├── prestart.sh # Container-Entrypoint (API) +├── worker.sh # Container-Entrypoint (Worker) +├── healthcheck.sh # Health-Check-Script +├── docker-compose.yml # Compose-Referenz +├── .env.docker.example # ENV-Template +├── Dockerfile # Multi-Stage-Build +└── requirements.txt # Python-Abhängigkeiten +``` + +--- + +*Diese Anleitung wird mit jedem Release aktualisiert. Stand: Commit be20a85, Alembic-Head 0090.*