cms.c2sgmbh/CLAUDE.md
Martin Porwoll 3f61050fb3 feat: add Timeline Collection for complex chronological events
Add dedicated Timeline Collection for managing complex timeline events:

- Collection: Multiple types (history, milestones, releases, career, events, process)
- Events: Flexible date handling (year, month+year, full date, ranges, custom text)
- Categories: milestone, founding, product, team, award, partnership, expansion, technology
- Importance levels: highlight, normal, minor
- Display options: layouts (vertical, alternating, horizontal, compact), sorting, year grouping
- Media: Image and gallery support per event
- Localization: Full support for DE/EN
- SEO: Meta fields for each timeline

API Features:
- Public endpoint at /api/timelines with tenant isolation
- Rate limiting and IP blocking
- Filter by type, slug, category, importance
- Locale parameter support
- Date formatting and sorting
- Optional grouping by year

Database: 8 tables created via migration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-13 10:22:56 +00:00

21 KiB

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)
  • Connection Pool: PgBouncer 1.24.1 (Transaction-Mode)
  • 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)
                                              ↓
                                      PgBouncer (6432)
                                              ↓
                              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
│   │   ├── Team.ts
│   │   ├── ServiceCategories.ts
│   │   ├── Services.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
│   │   │   ├── newsletter-service.ts    # Newsletter Double Opt-In
│   │   │   └── newsletter-templates.ts  # E-Mail-Templates
│   │   ├── 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
│       ├── sendNewsletterConfirmation.ts  # Newsletter Double Opt-In
│       ├── invalidateEmailCache.ts
│       └── auditLog.ts
├── tests/                     # Test Suite
│   ├── unit/security/         # Security Unit Tests
│   └── int/                   # Integration Tests
├── scripts/
│   ├── run-queue-worker.ts    # Queue Worker Starter
│   └── backup/                # Backup-System
│       ├── backup-db.sh       # PostgreSQL Backup-Skript
│       ├── restore-db.sh      # PostgreSQL Restore-Skript
│       ├── setup-backup.sh    # Interaktives Setup
│       └── README.md          # Backup-Dokumentation
├── .env                       # Umgebungsvariablen
├── ecosystem.config.cjs       # PM2 Config
└── .next/                     # Build Output

Umgebungsvariablen (.env)

Hinweis: Sensible Credentials (DB_PASSWORD, SMTP_PASS, etc.) werden aus ~/.pgpass und Umgebungsvariablen gelesen. Niemals Klartext-Passwörter in diese Datei schreiben!

# Datenbank (Passwort in ~/.pgpass oder als Umgebungsvariable)
DATABASE_URI=postgresql://payload:${DB_PASSWORD}@127.0.0.1:6432/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

PgBouncer Connection Pooling

PgBouncer läuft auf dem App-Server und pooled Datenbankverbindungen:

# Konfiguration
/etc/pgbouncer/pgbouncer.ini
/etc/pgbouncer/userlist.txt   # chmod 600

# Service
sudo systemctl status pgbouncer
sudo systemctl restart pgbouncer

# Statistiken abfragen
# Pool-Statistiken (Passwort aus ~/.pgpass)
PGPASSWORD="$DB_PASSWORD" psql -h 127.0.0.1 -p 6432 -U payload -d pgbouncer -c "SHOW POOLS;"
PGPASSWORD="$DB_PASSWORD" psql -h 127.0.0.1 -p 6432 -U payload -d pgbouncer -c "SHOW STATS;"

Konfiguration:

Parameter Wert Beschreibung
pool_mode transaction Verbindung wird nach Transaktion freigegeben
default_pool_size 20 Standard-Pool-Größe pro DB/User
min_pool_size 5 Mindestens gehaltene Verbindungen
reserve_pool_size 5 Reserve für Lastspitzen
max_db_connections 50 Max. Verbindungen zu PostgreSQL
max_client_conn 200 Max. Clients zu PgBouncer

Verbindungen:

  • App → PgBouncer: 127.0.0.1:6432 (DATABASE_URI)
  • PgBouncer → PostgreSQL: 10.10.181.101:5432 (TLS 1.3, server_tls_sslmode = require)

Direkte Verbindung (für Migrationen/CLI):

PgBouncer im Transaction-Mode kann Probleme mit lang laufenden Migrationen verursachen. Für solche Fälle das db-direct.sh Skript verwenden:

# Migrationen direkt an PostgreSQL (umgeht PgBouncer)
./scripts/db-direct.sh migrate

# Interaktive psql-Session
./scripts/db-direct.sh psql

# Schema-Änderungen
./scripts/db-direct.sh migrate:create

Das Skript liest Credentials aus ~/.pgpass (chmod 600).

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

# 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:access-control  # Access Control Tests
pnpm test:coverage     # Mit Coverage-Report

# Datenbank prüfen
# Verwende ./scripts/db-direct.sh psql oder:
PGPASSWORD="$DB_PASSWORD" psql -h 10.10.181.101 -U payload -d payload_db

# Backup
/home/payload/backups/postgres/backup-db.sh --verbose  # Manuelles Backup
scripts/backup/setup-backup.sh                          # Backup-System einrichten

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:

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]/...
# Locales in der Datenbank prüfen
# Verwende ./scripts/db-direct.sh psql oder:
PGPASSWORD="$DB_PASSWORD" psql -h 10.10.181.101 -U payload -d payload_db -c "\dt *_locales"

URLs

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:

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": "<p>Inhalt</p>",
    "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

Newsletter Double Opt-In

DSGVO-konformes Newsletter-System mit Double Opt-In:

Flow:

  1. User meldet sich an → Status: pending, Token wird generiert
  2. Double Opt-In E-Mail wird automatisch gesendet
  3. User klickt Bestätigungs-Link → Status: confirmed
  4. Willkommens-E-Mail wird gesendet
  5. Abmeldung jederzeit über Link in E-Mails möglich

API-Endpoints:

# Newsletter-Anmeldung
curl -X POST https://pl.c2sgmbh.de/api/newsletter/subscribe \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "firstName": "Max",
    "tenantId": 1,
    "source": "footer"
  }'

# Bestätigung (via Link aus E-Mail)
GET https://pl.c2sgmbh.de/api/newsletter/confirm?token=<uuid>

# Abmeldung (via Link aus E-Mail)
GET https://pl.c2sgmbh.de/api/newsletter/unsubscribe?token=<uuid>

Features:

  • Automatischer E-Mail-Versand bei Anmeldung
  • Token-Ablauf nach 48 Stunden
  • Willkommens-E-Mail nach Bestätigung
  • Abmelde-Bestätigung per E-Mail
  • Rate-Limiting: 5 Anmeldungen/10 Minuten pro IP
  • Erneute Anmeldung nach Abmeldung möglich

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:

# 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:

import { queueEmail } from '@/lib/queue'

await queueEmail({
  tenantId: 1,
  to: 'empfaenger@example.com',
  subject: 'Betreff',
  html: '<p>Inhalt</p>',
  source: 'form-submission'
})

PDF Queue

PDF-Generierung erfolgt über Playwright (HTML/URL zu PDF):

API-Endpoint /api/generate-pdf:

# 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": "<h1>Test</h1><p>Inhalt</p>",
    "filename": "test.pdf"
  }'

# Job-Status abfragen
curl "https://pl.c2sgmbh.de/api/generate-pdf?jobId=abc123" \
  -H "Cookie: payload-token=..."

Programmatisch:

import { queuePdfFromHtml, queuePdfFromUrl, getPdfJobStatus } from '@/lib/queue'

// HTML zu PDF
const job = await queuePdfFromHtml('<h1>Test</h1>', { filename: 'test.pdf' })

// URL zu PDF
const job2 = await queuePdfFromUrl('https://example.com', { format: 'A4' })

// Status abfragen
const status = await getPdfJobStatus(job.id)

Worker-Konfiguration

Über ecosystem.config.cjs:

  • QUEUE_EMAIL_CONCURRENCY: Parallele E-Mail-Jobs (default: 3)
  • QUEUE_PDF_CONCURRENCY: Parallele PDF-Jobs (default: 2)
  • QUEUE_DEFAULT_RETRY: Retry-Versuche (default: 3)
  • QUEUE_REDIS_DB: Redis-Datenbank für Queue (default: 1)

Redis Caching

Redis wird für API-Response-Caching und E-Mail-Transporter-Caching verwendet:

import { redis } from '@/lib/redis'

// Cache setzen (TTL in Sekunden)
await redis.set('key', JSON.stringify(data), 'EX', 60)

// Cache lesen
const cached = await redis.get('key')

// Pattern-basierte Invalidierung
await redis.keys('posts:*').then(keys => keys.length && redis.del(...keys))

Backup-System

Automatisches tägliches PostgreSQL-Backup mit lokalem Speicher und S3-Offsite-Backup.

Setup auf neuem Server:

cd /home/payload/payload-cms/scripts/backup
./setup-backup.sh

Konfigurationsdateien (nicht im Repo):

  • ~/.pgpass - PostgreSQL-Credentials (chmod 600)
  • ~/.s3cfg - S3-Credentials (chmod 600)

Backup-Speicherorte:

Ort Pfad Retention
Lokal /home/payload/backups/postgres/ 30 Tage
S3 s3://c2s/backups/postgres/ 30 Tage

Cron-Job: Täglich um 03:00 Uhr

Manuelles Backup:

/home/payload/backups/postgres/backup-db.sh --verbose

Restore aus Backup:

# Lokal
gunzip -c /home/payload/backups/postgres/payload_db_YYYY-MM-DD_HH-MM-SS.sql.gz | \
  psql -h 10.10.181.101 -U payload -d payload_db

# Aus S3
s3cmd get s3://c2s/backups/postgres/payload_db_YYYY-MM-DD_HH-MM-SS.sql.gz
gunzip -c payload_db_*.sql.gz | psql -h 10.10.181.101 -U payload -d payload_db

Dokumentation: scripts/backup/README.md

Datenbank-Direktzugriff

# Verwende ./scripts/db-direct.sh psql oder:
PGPASSWORD="$DB_PASSWORD" psql -h 10.10.181.101 -U payload -d payload_db

# Nützliche Queries
SELECT * FROM tenants;
SELECT * FROM users_tenants;
SELECT * FROM email_logs ORDER BY created_at DESC LIMIT 10;
SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 10;
\dt  -- Alle Tabellen

Collections Übersicht

Collection Slug Beschreibung
Users users Benutzer mit isSuperAdmin Flag
Tenants tenants Mandanten mit E-Mail-Konfiguration
Media media Medien mit 11 responsive Image Sizes
Pages pages Seiten mit Blocks
Posts posts Blog/News/Presse mit Kategorien
Categories categories Kategorien für Posts
Portfolios portfolios Portfolio-Galerien (Fotografie)
PortfolioCategories portfolio-categories Kategorien für Portfolios
Testimonials testimonials Kundenbewertungen
FAQs faqs Häufig gestellte Fragen (FAQ)
Team team Team-Mitglieder und Mitarbeiter
ServiceCategories service-categories Kategorien für Leistungen
Services services Leistungen und Dienstleistungen
NewsletterSubscribers newsletter-subscribers Newsletter mit Double Opt-In
SocialLinks social-links Social Media Links
Forms forms Formular-Builder
FormSubmissions form-submissions Formular-Einsendungen mit Status-Workflow
EmailLogs email-logs E-Mail-Protokollierung
AuditLogs audit-logs Security Audit Trail
CookieConfigurations cookie-configurations Cookie-Banner Konfiguration
CookieInventory cookie-inventory Cookie-Inventar
ConsentLogs consent-logs Consent-Protokollierung
Timelines timelines Chronologische Events (Geschichte, Meilensteine)

Timeline Collection

Dedizierte Collection für komplexe chronologische Darstellungen:

Timeline-Typen:

  • history - Unternehmensgeschichte
  • milestones - Projektmeilensteine
  • releases - Produkt-Releases
  • career - Karriere/Lebenslauf
  • events - Ereignisse
  • process - Prozess/Ablauf

Event-Features:

  • Flexible Datumsformate (Jahr, Monat+Jahr, vollständig, Zeitraum, Freitext)
  • Kategorien (Meilenstein, Gründung, Produkt, Team, Auszeichnung, etc.)
  • Wichtigkeitsstufen (Highlight, Normal, Minor)
  • Bilder und Galerien
  • Links und Metadaten
  • Rich-Text-Beschreibungen

Display-Optionen:

  • Layouts: vertikal, alternierend, horizontal, kompakt
  • Sortierung: aufsteigend/absteigend
  • Gruppierung nach Jahr
  • Verschiedene Marker-Stile

API-Endpoint:

# Liste aller Timelines eines Tenants
curl "https://pl.c2sgmbh.de/api/timelines?tenant=1"

# Nach Typ filtern
curl "https://pl.c2sgmbh.de/api/timelines?tenant=1&type=history"

# Einzelne Timeline
curl "https://pl.c2sgmbh.de/api/timelines?tenant=1&slug=company-history"

# Mit Sprache
curl "https://pl.c2sgmbh.de/api/timelines?tenant=1&locale=en"

FormSubmissions CRM-Workflow

Die FormSubmissions Collection wurde zu einem leichtgewichtigen CRM erweitert:

Status-Workflow:

  • 🆕 Neu → 👀 Gelesen → 🔄 In Bearbeitung → Warten → Erledigt → 🗑️ Archiviert

Features:

  • Priorität (Hoch/Normal/Niedrig)
  • Zuständigkeits-Zuweisung an User
  • Interne Notizen mit Auto-Autor und Zeitstempel
  • Antwort-Tracking (Methode, Zeitstempel, Zusammenfassung)
  • Tags zur Kategorisierung
  • Auto-Markierung als gelesen beim ersten Öffnen

Dateien:

  • src/collections/FormSubmissionsOverrides.ts - Feld-Definitionen
  • src/hooks/formSubmissionHooks.ts - Automatisierungen

Globals

Global Slug Beschreibung
SiteSettings site-settings Allgemeine Website-Einstellungen
Navigation navigation Navigationsmenü
SEOSettings seo-settings SEO-Einstellungen
PrivacyPolicySettings privacy-policy-settings Datenschutz-Einstellungen

Test Suite

# Alle Tests ausführen
pnpm test

# Security Tests
pnpm test:security

# Coverage Report
pnpm test:coverage

Test Coverage Thresholds:

  • Lines: 35%
  • Functions: 50%
  • Branches: 65%

CI/CD Pipeline

GitHub Actions Workflows in .github/workflows/:

ci.yml (Main CI Pipeline)

Läuft bei Push/PR auf main und develop:

Job Beschreibung
lint ESLint + Prettier Check
typecheck TypeScript Compiler (tsc --noEmit)
test Unit & Integration Tests (Vitest)
build Next.js Production Build
e2e Playwright E2E Tests

Lokale Ausführung:

pnpm lint              # ESLint
pnpm typecheck         # TypeScript Check
pnpm format:check      # Prettier Check
pnpm format            # Prettier Auto-Fix
pnpm test              # Alle Tests
pnpm build             # Production Build

security.yml (Security Scanning)

  • Gitleaks: Secret Scanning
  • pnpm audit: Dependency Vulnerabilities
  • CodeQL: Static Analysis (SAST)
  • Security Tests: Unit & Integration Tests für Security-Module

Dokumentation

  • CLAUDE.md - Diese Datei (Projekt-Übersicht)
  • docs/INFRASTRUCTURE.md - Server-Architektur & Deployment
  • docs/anleitungen/TODO.md - Task-Liste & Roadmap
  • docs/anleitungen/SECURITY.md - Sicherheitsrichtlinien
  • scripts/backup/README.md - Backup-System Dokumentation

Letzte Aktualisierung: 12.12.2025