- auth: Supabase-JWT lokal verifizieren (hono/jwt, HS256) statt GoTrue- Roundtrip pro Request; JWT_SECRET in cms-env, Remote-Fallback wenn ungesetzt - dialog: comment_stats-View (group by thread) ersetzt Full-Table-Scan + JS-Aggregation bei jedem Forum-Aufruf - ops: scripts/backup-db.sh (pg_dump, rotiert) + täglicher Cron im Proxmox- Script — Dialog-Daten liegen nur in Postgres, nicht in Git - security: Rate-Limit auf Schreib-Endpunkte (/api non-GET, 60/min je Nutzer) - perf: Cache-Control (1 Woche) auf statische Assets, HTML bleibt frisch Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.3 KiB
OPENBUREAU CMS
Headless CMS vor der Hugo-Engine. Hugo bleibt die Render-Engine; dieser Stack
schreibt Content aus Supabase in content/*.md, baut die Site und serviert sie.
Architektur
Ein docker-compose-Stack / ein LXC (Muster gespiegelt von RAPPORT-SERVER):
docker compose
├── db supabase/postgres ← posts-Tabelle (schema.sql)
├── auth gotrue ← Login
├── rest postgrest ← supabase-js liest/schreibt posts
├── kong :8000 ← API-Gateway (/auth/v1, /rest/v1)
└── cms :8080 Node + Hugo-Binary + Admin-SPA
├─ / live (public/)
├─ /_preview Vorschau (preview/, --buildDrafts)
├─ /admin React-Editor
└─ /api Backend (mountet Repo unter /site)
- cms hält das Hugo-Binary (0.161.1 extended, = lokal), mountet das Repo-Root
unter
/site, serviert die Site selbst (kein separater nginx). - Server-seitig spricht
cmsSupabase intern überhttp://kong:8000(Service-Key); die Admin-SPA nutzt browser-seitigAPI_EXTERNAL_URL+ANON_KEY. - Abweichung von RAPPORT:
realtime+storageweggelassen (nutzt das CMS nicht — Bild-Uploads gehen auf Platte nachstatic/images/). Nachrüstbar durch Kopieren der Service-Blöcke aus RAPPORT-SERVER.
Quelle der Wahrheit: die .md-Dateien
Dateibasiert. Die echten content/**/*.md sind kanonisch — das CMS liest und
schreibt sie direkt (Frontmatter via gray-matter). Damit erscheinen alle
bestehenden Inhalte im Editor: Beiträge (library/<rubrik>/…), Seiten
(manifest.md, colophon.md) und Rubriken (_index.md).
Supabase wird nur noch für den Login (GoTrue) gebraucht — keine Posts in der
DB. Drafts liegen als draft: true in der Datei; der Live-Build lässt sie aus,
der Preview-Build (--buildDrafts) zeigt sie.
Rechte & Kollaboration
- Admin (E-Mails in
ADMIN_EMAILS) sieht und bearbeitet alle Einträge. - Autor:innen sehen nur Einträge, in denen ihre Mail unter
authors:steht. Beim Anlegen wird der Ersteller automatisch eingetragen. - Kollaboration: im Editor weitere E-Mails ins Feld „Autor:innen" → beide haben Zugriff auf denselben Beitrag.
- Bestehende Beiträge/Seiten/Rubriken ohne
authors:sind nur für Admins sichtbar; ein Admin kann Autor:innen zuweisen, um sie freizugeben. - Hinweis:
authors:landet im Frontmatter (öffentliches Repo) — also E-Mails, die du dort einträgst, sind im Repo sichtbar.
Setup
Schnellweg: Proxmox-LXC
proxmox/create-openbureau-lxc.sh auf dem Proxmox-Host ausführen — legt den LXC
an, installiert Docker, zieht das (öffentliche) Repo, generiert alle Secrets und
startet den Stack. Als Einzeiler:
bash <(curl -fsSL https://git.kgva.ch/karim/OPENBUREAU/raw/branch/main/cms/proxmox/create-openbureau-lxc.sh)
Fragt interaktiv nur Storage/Bridge/IP ab (Enter = Default). Kein Token nötig.
GIT_TOKEN nur setzen, wenn das CMS per GIT_PUBLISH nach Gitea zurückschreiben soll.
Manuell (oder im Container)
cp .env.example .envPOSTGRES_PASSWORD+JWT_SECRETsetzen: jeopenssl rand -hex 32- Keys ableiten:
node scripts/generate-keys.mjs→ANON_KEY+SERVICE_ROLE_KEYin.env SITE_URL+API_EXTERNAL_URLauf die LAN-/Domain-Adresse setzenkong.yml: Platzhalter__CORS_ORIGIN__durchSITE_URL(Browser-Origin) ersetzenBIND_ADDRin.env:127.0.0.1hinter Reverse-Proxy (Standard),0.0.0.0für LAN-Direktzugriff- Repo dem Container-User (uid 1000) übereignen:
chown -R 1000:1000 <repo-root> docker compose up -d --build(Erststart: DB bootet + Schema/Migrations)- Login-User anlegen (Self-Signup ist aus):
source .env curl -X POST "$API_EXTERNAL_URL/auth/v1/admin/users" \ -H "apikey: $SERVICE_ROLE_KEY" -H "Authorization: Bearer $SERVICE_ROLE_KEY" \ -H "Content-Type: application/json" \ -d '{"email":"du@example.ch","password":"…","email_confirm":true}'
Dann: Admin …:8080/admin/ · Live …:8080/ · Preview …:8080/_preview/
Lokale Entwicklung am Admin
cd admin && npm install && npm run dev (Vite-Devserver, proxyt /api +
/_preview an den laufenden Container auf :8080).
Demo-Inhalt fürs Forum (optional)
db/seed-demo.sql füllt die Forum-Kategorien mit ein paar Beispiel-Threads und
-Wortmeldungen — bewusst getrennt von der Migration, damit die Produktion
leer startet. Bei Bedarf manuell einspielen (idempotent, mehrfach lauffähig):
docker compose exec -T db psql -U supabase_admin -d postgres < db/seed-demo.sql
Wieder entfernen: DELETE-Block am Ende der Datei (auskommentiert).
Backup der Dialog-Daten
⚠️ Foren, Threads und Wortmeldungen liegen nur in Postgres — anders als
content/*.md (in Git) sind sie sonst nirgends gesichert. Das Proxmox-Script
richtet ein tägliches Backup ein (/etc/cron.d/openbureau-backup, 3:15 Uhr).
Manuell/sonst:
bash scripts/backup-db.sh # → backups/openbureau-<TS>.sql.gz (rotiert, 14 Stk.)
Wiederherstellen:
gunzip -c backups/openbureau-<TS>.sql.gz \
| docker compose exec -T db psql -U supabase_admin -d postgres
Sicherheit / Härtung
Eingebaute Schutzmaßnahmen (Stand: Härtungs-Pass):
- Sicherheits-Header auf allen Antworten (X-Frame-Options, nosniff,
Referrer-Policy, HSTS); Uploads unter
/images/*mit strikter CSP +sandbox→ ein bösartiges SVG kann kein JavaScript im Origin ausführen. - Rate-Limit auf
/api/auth/login(10 Versuche/IP pro 5 Min) gegen Brute-Force. - Body-Limit 256 KB auf JSON-
/api/*, Bild-Upload max. 8 MB mit Format-Verifikation (sharp-Metadaten bzw. SVG/GIF-Signatur). - Comment-Limits (Body ≤ 10 000 Zeichen) gegen DB-Bloat.
- Kein Info-Leak: rohe DB-Fehler werden serverseitig geloggt, nach außen nur generische Meldungen.
- Non-root: der CMS-Container läuft als
node(uid 1000). - Port-Binding über
BIND_ADDR(Standard127.0.0.1), DB nur auf localhost. - CORS am Kong-Gateway auf die eigene
SITE_URL-Origin beschränkt (kein*).
Migration eines bestehenden Containers
Bei git pull auf einer schon laufenden Instanz greifen drei Änderungen, die
sonst einen frischen Deploy voraussetzen — vor dem nächsten
docker compose up -d --build von Hand nachziehen:
- Non-root:
chown -R 1000:1000 <repo-root>— sonst kann Hugopublic/nicht mehr bauen (Permission denied). - CORS:
kong.ymlenthält jetzt__CORS_ORIGIN__; auf einem bereits initialisierten Container ersetzt das Proxmox-Script den Platzhalter nicht. Manuell auf dieSITE_URLsetzen, sonst werden alle Browser-API-Calls (inkl. Login) per CORS geblockt. - BIND_ADDR: Key in
.envergänzen. Default127.0.0.1ist hinter einem TLS-Proxy korrekt; für LAN-Direktzugriff0.0.0.0setzen.
API
Alle /api/* (ausser /health) verlangen Authorization: Bearer <supabase-token>.
| Methode | Pfad | Zweck |
|---|---|---|
| GET | /api/health |
Healthcheck (offen) |
| GET | /api/content |
Alle Einträge listen (Beiträge/Seiten/Rubriken) |
| GET | /api/content/entry?path=… |
Einen Eintrag lesen (Frontmatter + Body) |
| PUT | /api/content/entry |
Eintrag anlegen/speichern ({path, frontmatter, body}) |
| POST | /api/preview |
Preview-Build (--buildDrafts), liefert /_preview/… |
| POST | /api/publish |
Public-Build → live + (opt.) git commit |
| POST | /api/upload |
Bild → static/images/, liefert /images/<name> |
Stand
- api + Hugo-Binary, Ein-Container-Setup
- Publish-Flow (DB → MD →
hugo→ live) - Echte Hugo-Vorschau (
--buildDrafts→/_preview) - React-Admin (
admin/) — Login, Editor, Frontmatter-Formular, iframe-Preview - Supabase-Auth-Middleware auf der API
- Bild-Upload für
cover_image
Noch denkbar
- nginx davor für Caching/TLS (oder über deinen bestehenden Reverse-Proxy)
- Post löschen / Slug-Umbenennung (alte MD-Datei entfernen)
- Mehrbenutzer + Rollen, wenn ein zweiter Autor dazukommt