# Payload CMS Multi-Tenant Project ## Projektübersicht Multi-Tenant CMS für 4 Websites unter einer Payload CMS 3.x Instanz: - porwoll.de - complexcaresolutions.de - gunshin.de - zweitmein.ng ## Tech Stack - **CMS:** Payload CMS 3.x - **Framework:** Next.js 15.4.7 - **Sprache:** TypeScript - **Datenbank:** PostgreSQL 17 (separater Server) - **Reverse Proxy:** Caddy 2.10.2 mit Let's Encrypt - **Process Manager:** PM2 - **Package Manager:** pnpm - **Cache:** Redis (optional, mit In-Memory-Fallback) - **Job Queue:** BullMQ (Redis-basiert) ## Architektur ``` Internet → 37.24.237.181 → Caddy (443) → Payload (3000) ↓ PostgreSQL (10.10.181.101:5432) ``` | Server | IP | Funktion | | --------------------- | ------------- | ---------- | | sv-payload (LXC 700) | 10.10.181.100 | App Server | | sv-postgres (LXC 701) | 10.10.181.101 | Datenbank | ## Wichtige Pfade ``` /home/payload/payload-cms/ # Projektroot ├── src/ │ ├── payload.config.ts # Haupt-Konfiguration │ ├── collections/ # Alle Collections │ │ ├── Users.ts │ │ ├── Media.ts │ │ ├── Tenants.ts │ │ ├── Posts.ts │ │ ├── Categories.ts │ │ ├── Portfolios.ts │ │ ├── PortfolioCategories.ts │ │ ├── FAQs.ts │ │ ├── EmailLogs.ts │ │ ├── AuditLogs.ts │ │ └── ... │ ├── app/(payload)/api/ # Custom API Routes │ │ ├── users/login/route.ts # Custom Login mit Audit │ │ ├── send-email/route.ts │ │ ├── email-logs/ │ │ │ ├── export/route.ts │ │ │ └── stats/route.ts │ │ └── test-email/route.ts │ ├── lib/ │ │ ├── email/ # E-Mail-System │ │ │ ├── tenant-email-service.ts │ │ │ └── payload-email-adapter.ts │ │ ├── security/ # Security-Module │ │ │ ├── rate-limiter.ts │ │ │ ├── csrf.ts │ │ │ ├── ip-allowlist.ts │ │ │ └── data-masking.ts │ │ ├── queue/ # BullMQ Job Queue │ │ │ ├── queue-service.ts │ │ │ ├── jobs/email-job.ts │ │ │ ├── jobs/pdf-job.ts │ │ │ ├── workers/email-worker.ts │ │ │ └── workers/pdf-worker.ts │ │ ├── pdf/ # PDF-Generierung │ │ │ └── pdf-service.ts │ │ ├── search.ts # Volltextsuche │ │ └── redis.ts # Redis Cache Client │ └── hooks/ # Collection Hooks │ ├── sendFormNotification.ts │ ├── invalidateEmailCache.ts │ └── auditLog.ts ├── tests/ # Test Suite │ ├── unit/security/ # Security Unit Tests │ └── int/ # Integration Tests ├── scripts/ │ └── run-queue-worker.ts # Queue Worker Starter ├── .env # Umgebungsvariablen ├── ecosystem.config.cjs # PM2 Config └── .next/ # Build Output ``` ## Umgebungsvariablen (.env) ```env DATABASE_URI=postgresql://payload:Finden55@10.10.181.101:5432/payload_db PAYLOAD_SECRET=a53b254070d3fffd2b5cfcc3 PAYLOAD_PUBLIC_SERVER_URL=https://pl.c2sgmbh.de NEXT_PUBLIC_SERVER_URL=https://pl.c2sgmbh.de NODE_ENV=production PORT=3000 # E-Mail (Global Fallback) SMTP_HOST=smtp.example.com SMTP_PORT=587 SMTP_SECURE=false SMTP_USER=user@example.com SMTP_PASS=secret SMTP_FROM_ADDRESS=noreply@c2sgmbh.de SMTP_FROM_NAME=Payload CMS # Redis Cache REDIS_URL=redis://localhost:6379 # Security CSRF_SECRET=your-csrf-secret SEND_EMAIL_ALLOWED_IPS= # Optional: Komma-separierte IPs/CIDRs BLOCKED_IPS= # Optional: Global geblockte IPs ``` ## Multi-Tenant Plugin Verwendet `@payloadcms/plugin-multi-tenant` für Mandantenfähigkeit. **Aktuelle Tenants:** | ID | Name | Slug | |----|------|------| | 1 | porwoll.de | porwoll | | 4 | Complex Care Solutions GmbH | c2s | | 5 | Gunshin | gunshin | **User-Tenant-Zuweisung:** Tabelle `users_tenants` ## Wichtige Befehle ```bash # Entwicklung pnpm dev # Production Build pnpm build # Migrationen pnpm payload migrate:create pnpm payload migrate # ImportMap nach Plugin-Änderungen pnpm payload generate:importmap # PM2 pm2 status pm2 logs payload pm2 logs queue-worker pm2 restart payload pm2 restart queue-worker # Tests pnpm test # Alle Tests pnpm test:security # Security Tests pnpm test:coverage # Mit Coverage-Report # Datenbank prüfen PGPASSWORD=Finden55 psql -h 10.10.181.101 -U payload -d payload_db ``` ## Workflow nach Code-Änderungen 1. Code ändern 2. `pnpm build` 3. `pm2 restart payload` 4. Testen unter https://pl.c2sgmbh.de/admin ## Bekannte Besonderheiten - **ES Modules:** package.json hat `"type": "module"`, daher PM2 Config als `.cjs` - **Plugin ImportMap:** Nach Plugin-Änderungen `pnpm payload generate:importmap` ausführen - **User-Tenant-Zuweisung:** Neue User müssen manuell Tenants zugewiesen bekommen - **Admin Login:** Custom Route mit Audit-Logging, unterstützt JSON und `_payload` FormData ## Build-Konfiguration Der Build ist für speichereffizientes Kompilieren optimiert: - `package.json`: `--max-old-space-size=2048` (2GB Heap-Limit) - `next.config.mjs`: `experimental.cpus: 1`, `workerThreads: false` **WICHTIG:** Der Server hat 8GB RAM ohne Swap. Bei laufendem VS Code Server kann der Build mit reduziertem Memory ausgeführt werden: ```bash pm2 stop payload # Speicher freigeben NODE_OPTIONS="--no-deprecation --max-old-space-size=1024" ./node_modules/.bin/next build pm2 start payload ``` Ohne PM2-Stop und mit VS Code wird der Build vom OOM Killer beendet (Exit code 137). ## Mehrsprachigkeit (i18n) Das System unterstützt Deutsch (default) und Englisch: - **Admin UI:** `@payloadcms/translations` für DE/EN - **Content:** Localization mit Fallback auf Deutsch - **Datenbank:** 36 `_locales` Tabellen für lokalisierte Felder - **API:** `?locale=de` oder `?locale=en` Parameter - **Frontend:** Routing über `/[locale]/...` ```bash # Locales in der Datenbank prüfen PGPASSWORD=Finden55 psql -h 10.10.181.101 -U payload -d payload_db -c "\dt *_locales" ``` ## URLs - **Admin Panel:** https://pl.c2sgmbh.de/admin - **API:** https://pl.c2sgmbh.de/api - **E-Mail API:** https://pl.c2sgmbh.de/api/send-email (POST, Auth erforderlich) - **Test-E-Mail:** https://pl.c2sgmbh.de/api/test-email (POST, Admin erforderlich) - **E-Mail Stats:** https://pl.c2sgmbh.de/api/email-logs/stats (GET, Auth erforderlich) - **PDF-Generierung:** https://pl.c2sgmbh.de/api/generate-pdf (POST/GET, Auth erforderlich) ## Security-Features ### Rate-Limiting Zentraler Rate-Limiter mit vordefinierten Limits: - `publicApi`: 60 Requests/Minute - `auth`: 5 Requests/15 Minuten (Login) - `email`: 10 Requests/Minute - `search`: 30 Requests/Minute - `form`: 5 Requests/10 Minuten ### CSRF-Schutz - Double Submit Cookie Pattern - Origin-Header-Validierung - Token-Endpoint: `GET /api/csrf-token` - Admin-Panel hat eigenen CSRF-Schutz ### IP-Allowlist - Konfigurierbar via `SEND_EMAIL_ALLOWED_IPS` - Unterstützt IPs, CIDRs (`192.168.1.0/24`) und Wildcards (`10.10.*.*`) - Globale Blocklist via `BLOCKED_IPS` ### Data-Masking - Automatische Maskierung sensibler Daten in Logs - Erkennt Passwörter, Tokens, API-Keys, SMTP-Credentials - Safe-Logger-Factory für konsistentes Logging ## E-Mail-System Multi-Tenant E-Mail-System mit tenant-spezifischer SMTP-Konfiguration: **Architektur:** - Globaler SMTP als Fallback (via .env) - Tenant-spezifische SMTP in Tenants Collection - Transporter-Caching mit automatischer Invalidierung - EmailLogs Collection für Audit-Trail **Tenant E-Mail-Konfiguration:** ``` Tenants → email → fromAddress, fromName, replyTo → email → useCustomSmtp (Checkbox) → email → smtp → host, port, secure, user, pass ``` **API-Endpoint `/api/send-email`:** ```bash curl -X POST https://pl.c2sgmbh.de/api/send-email \ -H "Content-Type: application/json" \ -H "Cookie: payload-token=..." \ -d '{ "to": "empfaenger@example.com", "subject": "Betreff", "html": "
Inhalt
", "tenantId": 1 }' ``` **Sicherheit:** - Authentifizierung erforderlich - Tenant-Zugriffskontrolle (User muss Tenant-Mitglied sein) - Rate-Limiting: 10 E-Mails/Minute pro User - SMTP-Passwort nie in API-Responses ## BullMQ Job Queue Das System verwendet BullMQ für asynchrone Job-Verarbeitung mit Redis als Backend. ### PM2 Worker Der Queue-Worker läuft als separater PM2-Prozess: ```bash # Worker-Status prüfen pm2 status pm2 logs queue-worker # Worker neustarten pm2 restart queue-worker ``` ### Email Queue E-Mails können asynchron über die Queue versendet werden: ```typescript import { queueEmail } from '@/lib/queue' await queueEmail({ tenantId: 1, to: 'empfaenger@example.com', subject: 'Betreff', html: 'Inhalt
', source: 'form-submission' }) ``` ### PDF Queue PDF-Generierung erfolgt über Playwright (HTML/URL zu PDF): **API-Endpoint `/api/generate-pdf`:** ```bash # PDF aus HTML generieren curl -X POST https://pl.c2sgmbh.de/api/generate-pdf \ -H "Content-Type: application/json" \ -H "Cookie: payload-token=..." \ -d '{ "source": "html", "html": "Inhalt
", "filename": "test.pdf" }' # Job-Status abfragen curl "https://pl.c2sgmbh.de/api/generate-pdf?jobId=abc123" \ -H "Cookie: payload-token=..." ``` **Programmatisch:** ```typescript import { queuePdfFromHtml, queuePdfFromUrl, getPdfJobStatus } from '@/lib/queue' // HTML zu PDF const job = await queuePdfFromHtml('