SSR-Renderer entfernt, Fokus auf SPA + JSON-API

Die Kernlogik bleibt von der Schnittstelle getrennt; der letzte SSR-Stand
liegt im Tag ssr-archiv. Die Moderationsseite bleibt, liefert Fehler aber
als JSON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiXsPUqw7oeomZ8wZrQW5q
This commit is contained in:
irrlicht
2026-09-26 21:28:34 +02:00
co-authored by Claude Opus 5.5
parent eccddcebe6
commit c6ab082f8c
26 changed files with 50 additions and 2603 deletions
+7 -91
View File
@@ -58,52 +58,15 @@ func getSession(r *http.Request) (*Session, bool) {
// absichtlich derselbe Fehler -- sonst wird der Endpunkt zum Nutzernamen-Orakel.
var errBadCredentials = Unauth("auth.bad_credentials", "Nutzername oder Passwort ist falsch.")
// loginForm trägt die Eingaben über einen fehlgeschlagenen Versuch hinweg.
// Bewusst ohne Passwort: das wird nie zurück ins Formular geschrieben.
type loginForm struct {
User string
}
// showLogin ist die serverseitig gerenderte Login-Seite -- der Weg für Clients
// ohne JavaScript (TOR Browser auf "Safest", I2P).
func showLogin(w http.ResponseWriter, r *http.Request) error {
if _, ok := getSession(r); ok {
http.Redirect(w, r, "/", http.StatusSeeOther)
return nil
}
render(w, r, "login", pageData{Title: "Login"})
return nil
}
// handleLogin bedient beide Adapter: das JS-Frontend (Accept: application/json)
// bekommt JSON, ein abgeschicktes Browser-Formular bekommt einen Redirect bzw.
// das neu gerenderte Formular. Die Logik dazwischen existiert nur einmal.
func handleLogin(w http.ResponseWriter, r *http.Request) error {
json := wantsJSON(r)
// fail wählt die zum Client passende Fehlerdarstellung. Der Rückgabewert
// geht an den Wrapper; bei HTML ist die Antwort schon geschrieben, deshalb
// nil.
fail := func(username string, err error) error {
if json {
return err
}
failForm(w, r, "login", pageData{Title: "Login", Form: loginForm{User: username}}, err)
return nil
}
if _, ok := getSession(r); ok {
if !json {
http.Redirect(w, r, "/", http.StatusSeeOther)
return nil
}
writeJSON(w, http.StatusOK, map[string]string{"status": "already_logged_in"})
return nil
}
username := sanitizeUsername(r.FormValue("user"))
if len(username) < 3 {
return fail(username, Invalid("user.name_short", "Der Nutzername ist zu kurz.").At("user"))
return Invalid("user.name_short", "Der Nutzername ist zu kurz.").At("user")
}
// Gleiche Meldung für "Nutzer existiert nicht" und "Passwort falsch": sonst
@@ -112,33 +75,24 @@ func handleLogin(w http.ResponseWriter, r *http.Request) error {
var hash []byte
err := db.QueryRow(`SELECT uid, password FROM user WHERE username = ?`, username).Scan(&uid, &hash)
if err == sql.ErrNoRows {
return fail(username, errBadCredentials)
return errBadCredentials
} else if err != nil {
return Internal(err)
}
if bcrypt.CompareHashAndPassword(hash, []byte(r.FormValue("pass"))) != nil {
return fail(username, errBadCredentials)
return errBadCredentials
}
if err := startSession(w, r, uid); err != nil {
return err
}
// POST -> Redirect (303) statt einer gerenderten Seite: sonst zeigt ein
// Reload den "Formular erneut senden?"-Dialog und legt eine zweite Session
// an (Post/Redirect/Get).
if !json {
http.Redirect(w, r, "/", http.StatusSeeOther)
return nil
}
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
return nil
}
// startSession legt eine Session an und setzt den Cookie. Von Login und
// Registrierung gemeinsam genutzt -- die Registrierung ohne JavaScript muss
// direkt einloggen, weil es sonst zwei Formulare hintereinander bräuchte.
// startSession legt eine Session an und setzt den Cookie.
func startSession(w http.ResponseWriter, r *http.Request, uid int64) error {
now := time.Now().Unix()
// timeout ist clientgesteuert -> serverseitig deckeln, damit niemand eine
@@ -200,7 +154,6 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
// abzubrechen -- der Nutzer soll nicht Feld für Feld durchprobieren müssen.
// Deshalb hier Fields (Feld -> Meldung) statt eines einzelnen Field.
func handleNewUser(w http.ResponseWriter, r *http.Request) error {
json := wantsJSON(r)
fields := map[string]string{}
username := sanitizeUsername(r.FormValue("user"))
@@ -224,62 +177,25 @@ func handleNewUser(w http.ResponseWriter, r *http.Request) error {
fields["pass2"] = "Passwort und Passwortbestätigung sind ungleich."
}
// fail wählt die zum Client passende Fehlerdarstellung -- wie bei
// handleLogin: JSON-Clients bekommen den Fehler über den Wrapper, ein
// Browser-Formular wird mit Meldungen neu gerendert.
fail := func(err error) error {
if json {
return err
}
failForm(w, r, "register", pageData{Title: "Registrieren", Form: registerForm{User: username}}, err)
return nil
}
if len(fields) > 0 {
e := Invalid("user.invalid", "Die Angaben sind unvollständig oder ungültig.")
e.Fields = fields
return fail(e)
return e
}
uid, err := createUser(username, pass1)
_, err := createUser(username, pass1)
if errors.Is(err, errUsernameTaken) {
// Rennen zwischen Vorab-Prüfung und INSERT: der UNIQUE-Constraint hat
// gegriffen -> als normale Validierungsmeldung statt 500 melden.
return fail(errUsernameTaken)
return errUsernameTaken
} else if err != nil {
return Internal(err)
}
// Ohne JavaScript direkt einloggen und weiterleiten: das JS-Frontend macht
// genau das (zweiter Aufruf auf /auth/login), und ein zweites Formular nur
// zum Anmelden wäre eine unnötige Hürde.
if !json {
if err := startSession(w, r, uid); err != nil {
return err
}
http.Redirect(w, r, "/", http.StatusSeeOther)
return nil
}
writeJSON(w, http.StatusCreated, map[string]string{"username": username})
return nil
}
// registerForm trägt die Eingaben über einen fehlgeschlagenen Versuch hinweg --
// wieder ohne die Passwörter.
type registerForm struct {
User string
}
// showRegister ist die serverseitig gerenderte Registrierungsseite.
func showRegister(w http.ResponseWriter, r *http.Request) error {
if _, ok := getSession(r); ok {
http.Redirect(w, r, "/", http.StatusSeeOther)
return nil
}
render(w, r, "register", pageData{Title: "Registrieren"})
return nil
}
func handleSessionInfo(w http.ResponseWriter, r *http.Request) error {
s, ok := getSession(r)
if !ok {