Files
Kontrollverlust/notes/api.md
T
irrlichtandClaude Opus 5.5 81a74375b7 Passwort-Reset per Einmal-Link (kver reset-link)
Ohne E-Mail-System erzeugt der Admin per CLI einen Link (/reset#<token>),
gültig 72 h und einmal. Gespeichert wird nur der SHA-256 des Tokens; es
steht im Fragment und geht nur im POST-Body an die API. Der Reset meldet
alle alten Sessions ab und loggt direkt neu ein. Passwortregeln jetzt
gemeinsam in checkNewPassword.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:56:05 +02:00

6.7 KiB
Raw Blame History

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:

{ "error": { "code": "entry.not_found", "message": "Beitrag nicht gefunden",
             "field": "content", "fields": { "user": "…" } } }
  • code: stabil, maschinenlesbar (<bereich>.<sache>), 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, Registrierung und Passwort-Reset 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
POST /api/auth/reset/check – token 200 {"username"}; ungültig/abgelaufen/benutzt: 404 reset.invalid
POST /api/auth/reset – token, pass1 (min. 10), pass2 200 {"username"} + Cookie (eingeloggt); löscht alle alten Sessions, Link verbraucht

Login-Fehler: 401 auth.bad_credentials (gleich für unbekannten Nutzer und falsches Passwort).

Reset-Links erzeugt nur der Admin per CLI (kver reset-link <nutzername>, siehe deploy.md). Sie zeigen auf /reset#<token>, gelten 72 h und einmal; ein neuer Link macht ältere desselben Nutzers ungültig. Das Token steht im Fragment und geht nur im POST-Body an die API, damit es nicht in Logs landet.


Beiträge

Ein Beitrag (Entry):

{ "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/…" }

In Hauptfeed und replies des Threads trägt jeder Beitrag mit Antworten zusätzlich latest: den jüngsten nicht gelöschten Beitrag aus seinem ganzen Teilbaum (nach created_at, höchstens 50 Ebenen tief). skipped zählt die Beiträge auf dem Pfad dazwischen, gelöschte eingeschlossen (0 = direkte Antwort). Ohne passenden Nachfahren fehlt das Feld.

"latest": { "entry": Entry, "skipped": 2 }
Methode Pfad Auth Request Erfolg
GET /api/entry/feed/{page} – – 200 [Entry] mit latest: Threads nach letzter Aktivität, 20 pro Seite, Seite 0–100, [] am Ende
GET /api/entry/{pid}/thread – – 200 {"entry", "ancestors": [Entry], "replies": [Entry]}; ancestors Root zuerst, replies (max. 100) nach letzter Aktivität, mit latest
GET /api/entry/{pid}/votes – – 200 {"pid","yes","no","selected"} (selected: yes/no/none)
POST /api/entry/{pid}/report – reason (optional, max. 500) 200 {"status":"reported"}. Ohne Session anonym (uid 0); Angemeldete legen keine zweite offene Meldung an; gelöschte Beiträge 404; 10/min pro IP
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 = yes/no 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":{"yes","no"},"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.