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

Alle CONFIG-Werte sind auch per **Umgebungsvariable** überschreibbar (für
non-interaktiv/SSH) — `ROOTFS_STORAGE` (z.B. `local-zfs`), `HOSTNAME`, `CTID`,
`IP`, `GATEWAY`, `DISK_GB`, `RAM_MB`, `CORES`, `BRIDGE`. Mit **`SITE_DOMAIN`**
wird der Stack direkt für eine öffentliche HTTPS-Domain hinter einem
Reverse-Proxy konfiguriert (Same-Origin, Pfad-Routing der Supabase-API).
Beispiel + Caddy-Block: siehe [`proxmox/README.md`](../proxmox/README.md).

### 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 `*`).
- **Reverse-Proxy nur `/auth/*`**: bei einem Domain-Deploy gehört nur das Login
  (GoTrue) public — `/rest`, `/storage`, `/realtime` nicht durchreichen (PostgREST
  `/rest/v1/` würde sonst die DB-Schema-Beschreibung preisgeben). Siehe Caddy-Block
  in [`../proxmox/README.md`](../proxmox/README.md).
- **Login-Rate-Limit** an GoTrue (`GOTRUE_RATE_LIMIT_TOKEN_REFRESH`), weil das
  öffentliche Login direkt aufs `/token` geht (nicht übers Node-Limit).
- **Keine Tabellenrechte für `anon`/`authenticated`** (`revoke` in `db/schema.sql`):
  RLS bleibt so auch bei künftigen Policies dicht; nur `service_role` (Node) liest.

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