# kver API Alle Endpunkte liegen unter `/api` und antworten ausschließlich mit JSON. Außerhalb von `/api` liefert das Backend nur Seiten und Dateien des Frontends. --- ## Konventionen **Erfolg oder Fehler steht im HTTP-Status.** `2xx` heißt Erfolg, alles andere Fehler. Einen `200` mit Fehler im Body gibt es nicht. Fehler haben immer diese Form: ```json { "error": { "code": "entry.not_found", "message": "Beitrag nicht gefunden", "field": "content", "fields": { "user": "…" } } } ``` - `code`: stabil, maschinenlesbar (`.`), für Logik - `message`: deutsche Meldung für Menschen - `field` / `fields`: optional, betroffene Formularfelder (`fields` nur bei der Registrierung, die alle Feldfehler auf einmal meldet) | Status | Bedeutung | |--------|-----------| | `400` | Eingabe ungültig | | `401` | nicht angemeldet / falsche Zugangsdaten | | `403` | angemeldet, aber nicht erlaubt (fremder Beitrag, falsches Passwort, TOR, fremder Origin) | | `404` | Objekt oder Endpunkt existiert nicht (`api.not_found`) | | `405` | Endpunkt existiert, aber nicht mit dieser Methode (`api.method_not_allowed`) | | `409` | Konflikt (Nutzername vergeben) | | `413` | Upload zu groß | | `429` | zu viele Anfragen (`rate.limited`) oder Repost-Cooldown | | `500` | Serverfehler (`internal`, Ursache nur im Log) | **Requests** sind `application/x-www-form-urlencoded` bzw. `multipart/form-data` (Uploads). Zeitstempel sind Unix-Sekunden. **Auth** läuft über das Cookie `session` (HttpOnly, SameSite=Lax). Geschützte Endpunkte antworten ohne gültige Session mit `401 auth.required`. **CSRF:** Nicht-GET-Requests mit fremdem `Origin`-Header werden mit `403 csrf.bad_origin` abgewiesen. **Rate-Limits** (pro IP): Login und Registrierung 20/min. --- ## Auth | Methode | Pfad | Auth | Request | Erfolg | |---------|------|------|---------|--------| | POST | `/api/auth/login` | – | `user`, `pass`, `timeout` (s, Default 1 Tag, max. 1 Jahr) | `200 {"status":"ok"}` + Cookie; bereits angemeldet: `200 {"status":"already_logged_in"}` | | POST | `/api/auth/newuser` | – | `user` (3–32 Zeichen), `pass1` (min. 10), `pass2` | `201 {"username"}` | | POST | `/api/auth/logout` | – | – | `200 {"status":"ok"}`, löscht Cookie | | GET | `/api/auth/sessioninfo` | ✓ | – | `200 {"uid","created_at","expires","description"}`, ohne Session `401` | Login-Fehler: `401 auth.bad_credentials` (gleich für unbekannten Nutzer und falsches Passwort). --- ## Beiträge Ein Beitrag (`Entry`): ```json { "pid": 123456789, "uid": 42, "created_at": 1700000000, "content": "…", "filepath": "static/media/…", "reply_to": 0, "reply_count": 3, "last_activity": 1700000500, "deleted": 0, "bump_count": 1, "last_bump": 1700000400, "username": "max", "avatar": "static/media/…" } ``` | Methode | Pfad | Auth | Request | Erfolg | |---------|------|------|---------|--------| | GET | `/api/entry/feed/{page}` | – | – | `200 [Entry]`: Threads nach letzter Aktivität, 20 pro Seite, Seite 0–100, `[]` am Ende | | GET | `/api/entry/{pid}/thread` | – | – | `200 {"entry", "ancestors": [Entry], "replies": [Entry]}` | | GET | `/api/entry/{pid}/votes` | – | – | `200 {"pid","left","right","selected"}` (`selected`: `left`/`right`/`none`) | | POST | `/api/entry/create` | ✓ | multipart: `content` (max. 1000), `file` (optional), `reply_to` (optional) | `201 {"pid","content","filepath","reply_to"}` | | POST | `/api/entry/{pid}/edit` | ✓ Autor | `content` | `200 {"pid","content"}` | | POST | `/api/entry/{pid}/delete` | ✓ Autor | – | `200 {"status":"deleted"}` (Soft-Delete, Thread bleibt) | | POST | `/api/entry/{pid}/vote` | ✓ | `mode` = `left`/`right` | `200` wie `votes`. Gleiche Stimme erneut zieht sie zurück | | POST | `/api/entry/{pid}/bump` | ✓ | – | `200 {"pid","bump_count","last_bump","last_activity","retry_after"}` | Beim Bump wächst der Cooldown mit jedem Repost um einen Tag. Im Cooldown antwortet der Endpunkt mit `429 entry.bump_cooldown`. Dazu kommen der Header `Retry-After` und im Body `retry_after` und `bump_count` (neben `error`). TOR-Anfragen auf `create` werden mit `403 tor.blocked` abgewiesen. --- ## Nutzer | Methode | Pfad | Auth | Request | Erfolg | |---------|------|------|---------|--------| | GET | `/api/u/{username}/info` | – | – | `200 {"uid","username","created_at","avatar"}` | | GET | `/api/u/{username}/feed/{page}` | – | – | `200 [Entry]`: alle Beiträge inkl. Antworten, chronologisch | | GET | `/api/user/info` | ✓ | – | `200 {"uid","username","created_at","last_login","avatar"}` | | POST | `/api/user/rename` | ✓ | `user` | `200 {"username"}`; vergeben: `409 user.name_taken` | | POST | `/api/user/avatar` | ✓ | multipart: `avatar` | `200 {"avatar"}` | | POST | `/api/user/delete` | ✓ | `pass1` | `200 {"status":"deleted"}`, löscht Konto, Beiträge, Votes, Sessions; falsches Passwort: `403 auth.bad_password` | --- ## Statistik | Methode | Pfad | Erfolg | |---------|------|--------| | GET | `/api/stats` | `200 {"users","entries"}` | | GET | `/api/stats/detail` | `200 {"users","entries","toplevel","replies","votes":{"left","right"},"impressions_total","visitors_total","daily":[…],"top_asns":[…]}` | --- ## Seiten (kein JSON) `/`, `/e/{pid}`, `/u/{username}` liefern die HTML-Seiten des Frontends und zählen anonymisierte Impressions. `/static/*` enthält Uploads und Dokumente, alles andere kommt aus `web/`. --- ## Datenbankschema Postgres; maßgeblich ist die Konstante `schema` in `db.go`.