Frontend+API: Namen ändern (POST /user/rename)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 11:47:44 +02:00
parent 7ffc35808a
commit 844bbb44e7
5 changed files with 98 additions and 0 deletions
+46
View File
@@ -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) { func TestDeleteUser(t *testing.T) {
srv := newTestServer(t) srv := newTestServer(t)
alice := registerAndLogin(t, srv, "alice") alice := registerAndLogin(t, srv, "alice")
+1
View File
@@ -49,6 +49,7 @@ func routes() http.Handler {
r.Post("/entry/create", handleCreateEntry) r.Post("/entry/create", handleCreateEntry)
r.Post("/entry/{pid}/vote", handleVote) r.Post("/entry/{pid}/vote", handleVote)
r.Get("/user/info", handleUserInfo) r.Get("/user/info", handleUserInfo)
r.Post("/user/rename", handleUserRename)
r.Post("/user/delete", handleUserDelete) r.Post("/user/delete", handleUserDelete)
}) })
+32
View File
@@ -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(&current); 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 // handleUserDelete löscht den eingeloggten Account dauerhaft, nach Bestätigung
// durch das Passwort (Formularfeld pass1). Beiträge, Votes und Sessions werden // durch das Passwort (Formularfeld pass1). Beiträge, Votes und Sessions werden
// in einer Transaktion mitgelöscht; zugehörige Mediendateien best effort entfernt. // in einer Transaktion mitgelöscht; zugehörige Mediendateien best effort entfernt.
+11
View File
@@ -3,6 +3,17 @@
// Nutzername aus dem Pfad /u/<name>. // Nutzername aus dem Pfad /u/<name>.
const username = decodeURIComponent(location.pathname.split("/")[2] || ""); 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) => { document.getElementById("delete-form").addEventListener("submit", async (e) => {
e.preventDefault(); e.preventDefault();
if (!confirm("Konto wirklich unwiderruflich löschen?")) return; if (!confirm("Konto wirklich unwiderruflich löschen?")) return;
+8
View File
@@ -19,6 +19,14 @@
<!-- Nur für den Eigentümer sichtbar --> <!-- Nur für den Eigentümer sichtbar -->
<section id="settings" hidden> <section id="settings" hidden>
<h2>Einstellungen</h2> <h2>Einstellungen</h2>
<details>
<summary>Name ändern</summary>
<form id="rename-form">
<input name="user" type="text" placeholder="Neuer Nutzername" autocomplete="username">
<button type="submit">Namen ändern</button>
</form>
<p class="msg" id="rename-msg"></p>
</details>
<details> <details>
<summary>Konto löschen</summary> <summary>Konto löschen</summary>
<form id="delete-form"> <form id="delete-form">