# 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//…`), 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 ` 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). ## 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 ` — 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 `. | 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/` | ## 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