Files
OPENBUREAU/cms/README.md
T
karim 272d30357f ops: update.sh — Update im LXC in einem Befehl
Kapselt git pull + Deploy-Config + Neustart, damit die Migrationsschritte
nicht mehr per Hand nötig sind:
- kong.yml vor dem Pull auf die Vorlage zurücksetzen (kein Konflikt), danach
  CORS-Origin aus SITE_URL rendern
- chown -R 1000:1000 (non-root-Container darf schreiben)
- git safe.directory für root auf dem uid-1000-Repo
- docker compose up -d --build + kong reload + Healthcheck

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 23:47:35 +02:00

201 lines
8.9 KiB
Markdown

# 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 `cms` Supabase intern über `http://kong:8000` (Service-Key);
die Admin-SPA nutzt browser-seitig `API_EXTERNAL_URL` + `ANON_KEY`.
- **Abweichung von RAPPORT:** `realtime` + `storage` weggelassen (nutzt das CMS
nicht — Bild-Uploads gehen auf Platte nach `static/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
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.
### Updaten (bestehender LXC)
Nicht `git pull` von Hand — das vergisst CORS-Origin (kong.yml), Dateirechte
(non-root) und den Neustart. Stattdessen im Container:
```bash
bash /opt/openbureau/cms/update.sh
```
Das macht: `git pull` → CORS-Origin aus `SITE_URL` in `kong.yml` rendern →
`chown -R 1000:1000``docker compose up -d --build` + kong neu laden →
Healthcheck. (Beim allerersten Mal das Skript per einmaligem `git pull` holen.)
### Manuell (oder im Container)
1. `cp .env.example .env`
2. `POSTGRES_PASSWORD` + `JWT_SECRET` setzen: je `openssl rand -hex 32`
3. Keys ableiten: `node scripts/generate-keys.mjs``ANON_KEY` + `SERVICE_ROLE_KEY` in `.env`
4. `SITE_URL` + `API_EXTERNAL_URL` auf die LAN-/Domain-Adresse setzen
5. `kong.yml`: Platzhalter `__CORS_ORIGIN__` durch `SITE_URL` (Browser-Origin) ersetzen
6. `BIND_ADDR` in `.env`: `127.0.0.1` hinter Reverse-Proxy (Standard), `0.0.0.0` für LAN-Direktzugriff
7. Repo dem Container-User (uid 1000) übereignen: `chown -R 1000:1000 <repo-root>`
8. `docker compose up -d --build` (Erststart: DB bootet + Schema/Migrations)
9. 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).
Tests der API (ohne DB/Container, reine Logik): `cd api && npm test`
(`node --test` — Pfad-Sicherheit, Rollen/Auth, Rate-Limit, Build-Coalescing).
### 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):
```bash
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
bash scripts/backup-db.sh # → backups/openbureau-<TS>.sql.gz (rotiert, 14 Stk.)
```
Wiederherstellen:
```bash
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` (Standard `127.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:
1. **Non-root:** `chown -R 1000:1000 <repo-root>` — sonst kann Hugo `public/`
nicht mehr bauen (Permission denied).
2. **CORS:** `kong.yml` enthält jetzt `__CORS_ORIGIN__`; auf einem bereits
initialisierten Container ersetzt das Proxmox-Script den Platzhalter nicht.
Manuell auf die `SITE_URL` setzen, sonst werden alle Browser-API-Calls
(inkl. Login) per CORS geblockt.
3. **BIND_ADDR:** Key in `.env` ergänzen. Default `127.0.0.1` ist hinter einem
TLS-Proxy korrekt; für LAN-Direktzugriff `0.0.0.0` setzen.
## 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
- [x] api + Hugo-Binary, Ein-Container-Setup
- [x] Publish-Flow (DB → MD → `hugo` → live)
- [x] Echte Hugo-Vorschau (`--buildDrafts` → `/_preview`)
- [x] React-Admin (`admin/`) — Login, Editor, Frontmatter-Formular, iframe-Preview
- [x] Supabase-Auth-Middleware auf der API
- [x] 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