# NewCMS – REST-API Schnittstellenbeschreibung

Diese Datei beschreibt, wie man mit einem **API-Token** (Personal Access Token) direkt
mit dem NewCMS-Backend kommuniziert – ohne Browser, z. B. aus Skripten, cURL, Postman
oder eigenen Programmen.

> Diese Datei dokumentiert nur die Schnittstelle.

---

## 1. Grundlagen

### Basis-URL

Alle Endpunkte hängen unter dem Pfad-Präfix `/api` an der Site-Domain:

```
https://<deine-site-domain>/api/<route>
```

Beispiel: `https://meinecms-site.de/api/menu`

- **Multi-Site:** Die Site wird über den `Host`-Header (Domain) erkannt. Dieselbe
  Code-Basis bedient mehrere Sites; ein Token gehört immer zu **einer** Site und
  funktioniert nur unter deren Domain.
- **Lokale Dev-Umgebung:** `http://localhost:8080/api/<route>`

### Content-Type

- **Request-Body:** JSON (`Content-Type: application/json`) für alle schreibenden
  Endpunkte – ausser Datei-Uploads (siehe [Abschnitt 7](#7-datei-uploads)).
- **Response:** Immer `application/json; charset=utf-8`, ausser bei Datei-Auslieferung
  (`/media/...`) und `GET /auth/verify` (HTML-Redirect).

### Einheitliches Response-Format

Jede JSON-Antwort hat eine von zwei Formen:

**Erfolg:**
```json
{ "status": "ok", "data": <payload> }
```

**Fehler:**
```json
{ "status": "error", "error": "Lesbare Fehlermeldung" }
```

Der HTTP-Statuscode trägt die eigentliche Semantik (200/201 ok, 4xx/5xx Fehler).
`data` kann Objekt, Array oder `null` sein.

### CORS

Das Backend sendet `Access-Control-Allow-Origin: *` und erlaubt die Methoden
`GET, POST, PUT, DELETE, OPTIONS` sowie die Header `Content-Type, Authorization`.
Direkter Zugriff aus Skripten/anderen Origins ist also möglich.

---

## 2. Authentifizierung mit API-Token

### Token-Format

Ein API-Token (Personal Access Token) hat das Format:

```
apt_<32 Zeichen Base62>
```

Beispiel: `apt_4f9KQ2bX7nZpR1aL8mT0cV3wY6sD5hJ2`

Der Token wird beim Erstellen **genau einmal** im Klartext zurückgegeben. Danach
ist er serverseitig nur noch als SHA-256-Hash gespeichert und kann nicht mehr
ausgelesen werden. Geht er verloren, muss ein neuer erstellt werden.

### Token mitsenden

Bei **jedem** Request als Bearer-Token im `Authorization`-Header:

```
Authorization: Bearer apt_4f9KQ2bX7nZpR1aL8mT0cV3wY6sD5hJ2
```

cURL-Beispiel:

```bash
curl -H "Authorization: Bearer apt_4f9KQ..." \
     https://meinecms-site.de/api/auth/me
```

### Scopes (Berechtigungen)

Jeder Token hat einen Scope:

| Scope  | Bedeutung |
|--------|-----------|
| `read` | Nur lesende HTTP-Methoden (`GET`, `HEAD`, `OPTIONS`). Schreibende Requests (POST/PUT/DELETE) werden mit **403** abgewiesen, bevor der Controller läuft. |
| `full` | Volle Rechte im Rahmen der **Rolle des Token-Besitzers**. |

> Wichtig: Der Scope **erweitert keine Rechte**. Ein `full`-Token erbt die Rolle
> des Users, dem er gehört. Ein Editor-Token kann also keine Admin-Endpunkte
> aufrufen, auch nicht mit Scope `full`. Siehe [Rollen](#rollen).

### Gültigkeit & Lebensdauer

- Ablauf zwischen **1 und 365 Tagen** (Default 90), beim Erstellen festgelegt.
- Nach Ablauf (`expires_at`) oder Widerruf (`revoked_at`) wird der Token mit
  **401** abgelehnt.
- API-Tokens **rotieren nicht** (anders als Browser-Session-Tokens). Es wird kein
  `X-Auth-Token`-Refresh-Header gesendet.
- `last_used_at` wird gedrosselt aktualisiert (max. alle 5 Minuten).

### Rollen

Rolle des Users bestimmt, welche Endpunkte erreichbar sind:

| Rolle          | Rechte |
|----------------|--------|
| `admin`        | Alles (inkl. User-Verwaltung, fremde Tokens, Site-weite Aktionen). |
| `editor`       | Seiten/Inhalte/Module bearbeiten, eigene API-Tokens verwalten. |
| `viewer`       | Lesen (auch nicht-öffentliche Inhalte der Site). Kann **keine** API-Tokens erstellen. |
| `unauthorized` | Frisch registriert, noch nicht freigeschaltet – im Wesentlichen kein Zugriff. |

API-Tokens können nur von **Editoren und Admins** erstellt/verwaltet werden
(`[auth, editor+]`).

---

## 3. API-Tokens verwalten

Tokens werden normalerweise über die Weboberfläche (Profil → API-Tokens) erstellt.
Es geht aber auch per API – dafür braucht der erste Zugang allerdings ein
bestehendes Bearer-Token (entweder ein Browser-Session-Token aus `POST /auth/login`
oder ein vorhandenes API-Token mit Scope `full`).

### Token erstellen

```
POST /api/auth/api-tokens          [auth, editor+]
```

Request-Body:
```json
{
  "name": "Mein Import-Skript",
  "scope": "full",
  "expires_days": 90
}
```

| Feld           | Typ    | Pflicht | Beschreibung |
|----------------|--------|---------|--------------|
| `name`         | string | ja      | Bezeichnung (max. 100 Zeichen), nur zur Wiedererkennung. |
| `scope`        | string | nein    | `read` oder `full` (Default `full`). |
| `expires_days` | int    | nein    | 1–365 (Default 90). |

Response (Klartext-Token **nur hier einmalig**):
```json
{
  "status": "ok",
  "data": {
    "id": 12,
    "name": "Mein Import-Skript",
    "scope": "full",
    "prefix": "apt_4f9KQ2bX",
    "token": "apt_4f9KQ2bX7nZpR1aL8mT0cV3wY6sD5hJ2",
    "expires_at": "2026-09-04 12:00:00"
  }
}
```

### Eigene Tokens auflisten

```
GET /api/auth/api-tokens           [auth, editor+]
```
Response: `data.tokens[]` mit `id, name, prefix, scope, created_at, last_used_at,
expires_at` (kein Klartext).

### Fremde Tokens der Site auflisten (Admin)

```
GET /api/auth/api-tokens/others    [admin]
```
Liefert aktive Tokens **anderer** User der Site inkl. `username`.

### Token widerrufen

```
DELETE /api/auth/api-tokens/{id}   [auth, editor+]
```
- Normale User: nur eigene Tokens.
- Admins: alle Tokens innerhalb der eigenen Site.

Idempotent. Response: `{ "status": "ok", "data": { "revoked": true|false } }`.

---

## 4. Auth- & Profil-Endpunkte

| Methode | Route | Auth | Beschreibung |
|---------|-------|------|--------------|
| POST | `/auth/login` | – | Login mit `username`/`password`, liefert 24h-Session-Token. |
| POST | `/auth/logout` | auth | Löscht Auth-Cookie (Token bleibt stateless gültig). |
| POST | `/auth/register` | – | Selbstregistrierung (falls aktiviert). |
| GET  | `/auth/verify?token=XXX` | – | E-Mail-Bestätigung (HTML-Redirect, kein JSON). |
| POST | `/auth/forgot` | – | Passwort-Reset anfordern. |
| POST | `/auth/reset` | – | Passwort mit Reset-Token setzen. |
| GET  | `/auth/me` | auth | Eigene Userdaten (`id, username, email, role, …`). |
| PUT  | `/auth/profile` | auth | E-Mail/Passwort/Newsletter ändern. |

`POST /auth/login` Body:
```json
{ "username": "karl", "password": "geheim" }
```
Response `data`: `{ "token": "<hmac-token>", "username": "karl", "role": "editor" }`.

> Dieses `token` ist ein **kurzlebiges Session-Token** (24h, HMAC-signiert), kein
> API-Token. Für dauerhafte Skript-Zugriffe stattdessen ein `apt_`-Token erstellen.

---

## 5. Inhalts-Endpunkte (Seiten)

Zentrales Objekt ist die **Seite** (`seiten`). Felder im `data`-Objekt von
`GET /pages/{slug}`:

| Feld | Typ | Beschreibung |
|------|-----|--------------|
| `id` | int | Seiten-ID. |
| `slug` | string | URL-Slug (eindeutig pro Site). |
| `titel` | string | Titel. |
| `inhalt` | string | HTML-Inhalt (serverseitig aufgelöste Template-Tags, siehe unten). |
| `teaser` | string | Kurzbeschreibung. |
| `pid` | int | Parent-Seiten-ID (0 = Root-Ebene). |
| `folge` | int | Sortier-Reihenfolge unter dem Parent. |
| `public` | `y`/`n` | Öffentlich sichtbar (Gäste). |
| `privat` | 0/1 | Privat (nur Owner/Admin). Exklusiv zu `public`. |
| `privat_user_id` | int\|null | Owner bei privaten Seiten. |
| `modul` | string\|null | Aktiviertes Modul (`chat`, `kalender`, `whiteboard`, `charsheet`, `news`). |
| `template` | string\|null | Template-Name. |
| `plugins` | string[] | Aktive Plugins (z. B. `showSubSites`, `prevnext`). |
| `slider_images` | array | Zugewiesene Slider-Bilder. |
| `breadcrumb` | array | `[{slug, titel}, …]` von Root bis Parent. |

**Roh-Inhalt:** `GET /pages/{slug}?raw=1` liefert `inhalt` ungeparst (Template-Tags
wie `[seite:ID]`, `[bild:ID]`, `[klapp:Titel]…[/klapp]` bleiben erhalten). Nützlich
zum programmatischen Bearbeiten – beim Speichern den Roh-Text zurückschreiben.

### Endpunkte

| Methode | Route | Auth | Beschreibung |
|---------|-------|------|--------------|
| GET | `/pages` | – | Liste aller öffentlichen Seiten (flach). |
| GET | `/pages/{slug}` | – | Einzelne Seite nach Slug. |
| POST | `/pages` | auth | Seite erstellen. |
| PUT | `/pages/{id}` | auth | Seite ändern. |
| DELETE | `/pages/{id}` | auth | Seite (+ Unterseiten) löschen. |
| PUT | `/pages/{id}/reorder` | auth | Reihenfolge ändern (`{"direction":"up"\|"down"}`). |
| PUT | `/pages/{id}/move` | auth | Verschieben (`{"target_pid": <id>}`). |
| PUT | `/pages/{id}/toggle-public` | auth | Öffentlich-Flag umschalten. |
| GET | `/pages/{id}/versions` | auth | Versionshistorie (max. 50). |
| GET | `/pages/{id}/versions/{versionId}` | auth | Einzelne Version + aktuelle Fassung. |
| POST | `/pages/{id}/versions/{versionId}/restore` | auth | Version wiederherstellen. |
| GET | `/pages/{id}/export-tree` | auth | Seitenbaum exportieren. |
| POST | `/pages/{id}/import-tree` | auth | Seitenbaum importieren. |
| GET | `/structure?variant=public\|full` | – | Vorberechneter Seitenbaum (JSON-Cache). |
| GET | `/menu` | – | Navigations-Menü. |
| GET | `/site` | – | Site-Metadaten (Name, Theme, …). |
| GET | `/sitemap` | – | Sitemap. |
| GET | `/search?q={term}` | – | Volltextsuche. |

### Seite erstellen – Beispiel

```
POST /api/pages
Authorization: Bearer apt_...
Content-Type: application/json
```
```json
{
  "slug": "neue-seite",
  "titel": "Neue Seite",
  "inhalt": "<p>Hallo Welt</p>",
  "teaser": "Kurztext",
  "pid": 5,
  "public": "n",
  "plugins": ["prevnext"]
}
```

| Feld | Pflicht | Hinweis |
|------|---------|---------|
| `slug` | ja | Eindeutig pro Site, sonst 400. |
| `titel` | ja | |
| `inhalt` | nein | HTML/Template-Tags, Default leer. |
| `teaser` | nein | |
| `pid` | nein | Parent (Default 0 = Root). |
| `folge` | nein | Default = ans Ende. |
| `public` | nein | `y`/`n`, Default `n`. |
| `privat` | nein | 1 erzwingt `public=n`, Owner = Token-User. |
| `modul` | nein | Modul aktivieren. |
| `template` | nein | |
| `plugins` | nein | String-Array. |
| `javascript` | nein | **Nur Admins**, wird sonst ignoriert. |

Response: `201` mit dem kompletten neuen Seiten-Datensatz.

Beim Update (`PUT /pages/{id}`) werden nur die übergebenen Felder geändert
(Partial Update). Inhalts-/Titel-/Teaser-Änderungen erzeugen automatisch eine
Version in `seiten_versionen`. Das `suche`-Feld wird serverseitig generiert und
ignoriert eingehende Werte.

---

## 6. Modul-Endpunkte

Module hängen an einer Seite, die das jeweilige Modul aktiviert hat (`modul`-Spalte).
Ist das Modul nicht aktiviert, antwortet der Endpunkt mit **403**. `{id}` ist immer
die **Seiten-ID**.

### Chat

| Methode | Route | Auth |
|---------|-------|------|
| GET | `/pages/{id}/chat` | auth |
| POST | `/pages/{id}/chat` | auth |
| GET | `/pages/{id}/chat/recipients` | auth |

### Kalender

| Methode | Route | Auth |
|---------|-------|------|
| GET | `/pages/{id}/calendar` | – |
| GET | `/pages/{id}/calendar/{date}` | – |
| POST | `/pages/{id}/calendar` | auth |
| DELETE | `/pages/{id}/calendar/{date}` | auth |
| GET | `/pages/{id}/calendar/config` | auth |
| PUT | `/pages/{id}/calendar/config` | admin |
| GET | `/pages/{id}/calendar/{date}/state` | auth |
| POST | `/pages/{id}/calendar/{date}/confirm` | auth |

`{date}` im Format `YYYY-MM-DD`.

### Whiteboard

| Methode | Route | Auth |
|---------|-------|------|
| GET | `/pages/{id}/whiteboard` | – |
| GET | `/pages/{id}/whiteboard/poll` | – |
| POST | `/pages/{id}/whiteboard/config` | auth |
| POST | `/pages/{id}/whiteboard/strokes` | auth |
| DELETE | `/pages/{id}/whiteboard/strokes` | auth |
| POST | `/pages/{id}/whiteboard/icons` | auth |
| DELETE | `/pages/{id}/whiteboard/icons/{iconId}` | auth |
| POST | `/pages/{id}/whiteboard/pings` | auth |

### Charsheet (Charakterbögen)

| Methode | Route | Auth |
|---------|-------|------|
| GET | `/pages/{id}/charsheet` | – |
| GET | `/pages/{id}/charsheet/{uuid}` | – |
| POST | `/pages/{id}/charsheet` | auth |
| POST | `/pages/{id}/charsheet/{uuid}` | auth |
| DELETE | `/pages/{id}/charsheet/{uuid}` | auth |
| GET | `/pages/{id}/charsheet/{uuid}/history` | – |
| GET | `/pages/{id}/charsheet/{uuid}/version/{versionId}` | – |

### News

| Methode | Route | Auth |
|---------|-------|------|
| GET | `/pages/{id}/news` | – |

### Audio (Sound-Library)

| Methode | Route | Auth |
|---------|-------|------|
| GET | `/pages/{id}/audio` | – |
| POST | `/pages/{id}/audio` | auth (Upload) |
| POST | `/pages/{id}/audio/attach` | auth |
| DELETE | `/pages/{id}/audio/{dzId}` | auth |
| GET | `/audio/library` | auth |
| DELETE | `/audio/{id}` | auth |

### Würfel

| Methode | Route | Auth |
|---------|-------|------|
| POST | `/dice-roll` | auth |
| GET | `/dice-rolls` | – |

---

## 7. Datei-Uploads

Upload-Endpunkte erwarten **`multipart/form-data`** (nicht JSON):

| Methode | Route | Auth | Beschreibung |
|---------|-------|------|--------------|
| GET | `/pages/{id}/media` | auth | Medien einer Seite. |
| POST | `/pages/{id}/images` | auth | Bild hochladen (→ WebP). |
| POST | `/pages/{id}/files` | auth | Datei hochladen. |
| PUT | `/media/{id}` | auth | Medien-Metadaten ändern. |
| DELETE | `/media/{id}` | auth | Zuweisung/Medium löschen. |
| DELETE | `/files/{id}` | auth | Datei löschen. |
| GET | `/media-overview` | auth | Medien-Übersicht der Site. |
| GET | `/media/{id}` | – | Medium ausliefern (Access-Check). |
| GET | `/media/{id}/download` | – | Download. |

cURL-Bild-Upload:
```bash
curl -H "Authorization: Bearer apt_..." \
     -F "file=@bild.jpg" \
     https://meinecms-site.de/api/pages/42/images
```

> **PUT/DELETE per POST-Override:** Manche Clients können kein PUT/DELETE mit
> Multipart senden. Dann `POST` mit Formfeld `_method=PUT` (bzw. `DELETE`)
> verwenden – der Front-Controller wertet `_method` aus. Der Scope-Check
> (`read` vs. `full`) berücksichtigt diesen Override.

Server-Limits (Produktion all-inkl): `upload_max_filesize`/`post_max_size` = 512M,
`max_execution_time` = 60s.

---

## 8. Admin-Endpunkte

Nur mit Token eines **Admin**-Users (Scope `full`):

| Methode | Route | Beschreibung |
|---------|-------|--------------|
| GET | `/users` | User der Site auflisten. |
| POST | `/users` | User anlegen. |
| PUT | `/users/{id}` | User ändern. |
| DELETE | `/users/{id}` | User löschen. |
| POST | `/reindex-search` | Suchindex neu aufbauen. |
| GET | `/auth/api-tokens/others` | Fremde Tokens der Site. |

---

## 9. HTTP-Statuscodes

| Code | Bedeutung |
|------|-----------|
| 200 | OK. |
| 201 | Erstellt (z. B. neue Seite). |
| 204 | No Content (CORS-Preflight `OPTIONS`). |
| 400 | Ungültige Eingabe (fehlende Felder, ungültiges JSON, Validierung). |
| 401 | Nicht/ungültig authentifiziert (Token fehlt, abgelaufen, widerrufen). |
| 403 | Authentifiziert, aber keine Berechtigung (falsche Rolle, `read`-Scope schreibt, Modul nicht aktiv). |
| 404 | Nicht gefunden (Route, Seite, Version) – auch bei fehlender Sichtbarkeit. |
| 409 | Konflikt (Slug/Username/E-Mail bereits vergeben). |
| 503 | CMS nicht installiert. |

Fehlerdetails stehen im `error`-Feld der JSON-Antwort.

---

## 10. Schnelltest

```bash
TOKEN="apt_dein_token_hier"
BASE="https://meinecms-site.de/api"

# Wer bin ich?
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/auth/me"

# Eigene Tokens auflisten
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/auth/api-tokens"

# Seite lesen (roh, zum Bearbeiten)
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/pages/home?raw=1"

# Seite aktualisieren
curl -s -X PUT \
     -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"titel":"Neuer Titel"}' \
     "$BASE/pages/42"
```
