From 844bbb44e76fb8ac5ef02064b083163de0c86e1c Mon Sep 17 00:00:00 2001 From: irrlicht Date: Wed, 3 Jun 2026 11:47:44 +0200 Subject: [PATCH] =?UTF-8?q?Frontend+API:=20Namen=20=C3=A4ndern=20(POST=20/?= =?UTF-8?q?user/rename)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- endpoints_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ main.go | 1 + user.go | 32 ++++++++++++++++++++++++++++++++ web/js/user.js | 11 +++++++++++ web/user.html | 8 ++++++++ 5 files changed, 98 insertions(+) diff --git a/endpoints_test.go b/endpoints_test.go index 8f7b87a..439064b 100644 --- a/endpoints_test.go +++ b/endpoints_test.go @@ -221,6 +221,52 @@ func TestVoteRequiresAuthAndValidMode(t *testing.T) { } } +func TestRenameUser(t *testing.T) { + srv := newTestServer(t) + alice := registerAndLogin(t, srv, "alice") + registerAndLogin(t, srv, "bob") + + // Zu kurz -> 400. + resp := postForm(t, alice, srv.URL+"/user/rename", url.Values{"user": {"ab"}}) + resp.Body.Close() + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("zu kurz: erwartet 400, bekam %d", resp.StatusCode) + } + + // Bereits vergeben -> 409. + resp = postForm(t, alice, srv.URL+"/user/rename", url.Values{"user": {"bob"}}) + resp.Body.Close() + if resp.StatusCode != http.StatusConflict { + t.Fatalf("vergeben: erwartet 409, bekam %d", resp.StatusCode) + } + + // Gültig -> 200 + neuer Name. + resp = postForm(t, alice, srv.URL+"/user/rename", url.Values{"user": {"alice2"}}) + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("rename: erwartet 200, bekam %d", resp.StatusCode) + } + var out struct { + Username string `json:"username"` + } + json.NewDecoder(resp.Body).Decode(&out) + if out.Username != "alice2" { + t.Fatalf("erwarteter Name alice2, bekam %q", out.Username) + } + + // Profil unter dem neuen Namen erreichbar, alter Name weg. + r2, _ := http.Get(srv.URL + "/u/alice2/info") + r2.Body.Close() + if r2.StatusCode != http.StatusOK { + t.Fatalf("/u/alice2/info: erwartet 200, bekam %d", r2.StatusCode) + } + r3, _ := http.Get(srv.URL + "/u/alice/info") + r3.Body.Close() + if r3.StatusCode != http.StatusNotFound { + t.Fatalf("/u/alice/info: erwartet 404, bekam %d", r3.StatusCode) + } +} + func TestDeleteUser(t *testing.T) { srv := newTestServer(t) alice := registerAndLogin(t, srv, "alice") diff --git a/main.go b/main.go index bd1999f..c60e92c 100644 --- a/main.go +++ b/main.go @@ -49,6 +49,7 @@ func routes() http.Handler { r.Post("/entry/create", handleCreateEntry) r.Post("/entry/{pid}/vote", handleVote) r.Get("/user/info", handleUserInfo) + r.Post("/user/rename", handleUserRename) r.Post("/user/delete", handleUserDelete) }) diff --git a/user.go b/user.go index 51f8063..568349f 100644 --- a/user.go +++ b/user.go @@ -78,6 +78,38 @@ func handleUserInfo(w http.ResponseWriter, r *http.Request) { }) } +// handleUserRename benennt den eingeloggten Account um. Nichts außer der user-Zeile +// referenziert den username (alles hängt an der uid), daher genügt ein UPDATE. +func handleUserRename(w http.ResponseWriter, r *http.Request) { + uid := uidFromContext(r.Context()) + + name := sanitizeUsername(r.FormValue("user")) + if len(name) < 3 { + writeError(w, http.StatusBadRequest, "Der Nutzername ist zu kurz.") + return + } + + var current string + if err := db.QueryRow(`SELECT username FROM user WHERE uid = ?`, uid).Scan(¤t); err != nil { + writeError(w, http.StatusNotFound, "Nutzer nicht gefunden") + return + } + if name == current { + writeError(w, http.StatusBadRequest, "Das ist bereits dein Name.") + return + } + if idFromUsername(name) != 0 { + writeError(w, http.StatusConflict, "Der Nutzername wird bereits verwendet.") + return + } + + if _, err := db.Exec(`UPDATE user SET username = ? WHERE uid = ?`, name, uid); err != nil { + writeError(w, http.StatusInternalServerError, "Umbenennen fehlgeschlagen") + return + } + writeJSON(w, http.StatusOK, map[string]string{"username": name}) +} + // handleUserDelete löscht den eingeloggten Account dauerhaft, nach Bestätigung // durch das Passwort (Formularfeld pass1). Beiträge, Votes und Sessions werden // in einer Transaktion mitgelöscht; zugehörige Mediendateien best effort entfernt. diff --git a/web/js/user.js b/web/js/user.js index 55cd6d6..7dfd257 100644 --- a/web/js/user.js +++ b/web/js/user.js @@ -3,6 +3,17 @@ // Nutzername aus dem Pfad /u/. const username = decodeURIComponent(location.pathname.split("/")[2] || ""); +document.getElementById("rename-form").addEventListener("submit", async (e) => { + e.preventDefault(); + const res = await api.post("/user/rename", new FormData(e.target)); + if (res.ok) { + location.href = `/u/${encodeURIComponent(res.data.username)}`; + } else { + document.getElementById("rename-msg").textContent = + res.data.error || "Umbenennen fehlgeschlagen."; + } +}); + document.getElementById("delete-form").addEventListener("submit", async (e) => { e.preventDefault(); if (!confirm("Konto wirklich unwiderruflich löschen?")) return; diff --git a/web/user.html b/web/user.html index 7002bee..be89daf 100644 --- a/web/user.html +++ b/web/user.html @@ -19,6 +19,14 @@