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:
co-authored by
Claude Opus 5.5
parent
eccddcebe6
commit
c6ab082f8c
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user