f97999c3c0
- coalesce.js: generisches Serialisieren+Koaleszieren je Key; buildSite() in hugo.js nutzt es → Publish/Preview/Profil starten nie überlappende Hugo- Prozesse, schnelle Folge-Aufrufe lösen nur einen Trailing-Build aus - dialog-store: syncLibrary() gedrosselt (60s-TTL) statt bei jedem Forum-Read Filesystem-Walk + Upsert; Publish forciert Sync (force:true) - test/: node:test-Suite (19 Tests) für safeRel/normAuthors/urlFor/hasAccess, roleOf + lokale JWT-Verifikation, Rate-Limiter, Coalescing; npm test Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
188 lines
8.5 KiB
Markdown
188 lines
8.5 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.
|
|
|
|
### 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
|