diff --git a/endpoints_test.go b/endpoints_test.go index e2c37d8..ef20e08 100644 --- a/endpoints_test.go +++ b/endpoints_test.go @@ -318,6 +318,54 @@ func TestThreading(t *testing.T) { } } +func TestEditEntry(t *testing.T) { + srv := newTestServer(t) + alice := registerAndLogin(t, srv, "alice") + bob := registerAndLogin(t, srv, "bob") + + pid := createEntry(t, alice, srv, "original") + editURL := fmt.Sprintf("%s/entry/%d/edit", srv.URL, pid) + + // Eigentümer bearbeitet -> 200, neuer Inhalt sichtbar. + resp := postForm(t, alice, editURL, url.Values{"content": {"korrigiert"}}) + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("edit: erwartet 200, bekam %d", resp.StatusCode) + } + if tv := getThread(t, srv, pid); tv.Entry.Content != "korrigiert" { + t.Fatalf("inhalt nach edit: %q", tv.Entry.Content) + } + + // Leerer Inhalt -> 400. + resp = postForm(t, alice, editURL, url.Values{"content": {" "}}) + resp.Body.Close() + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("leerer edit: erwartet 400, bekam %d", resp.StatusCode) + } + + // Fremder -> 403. + resp = postForm(t, bob, editURL, url.Values{"content": {"hijack"}}) + resp.Body.Close() + if resp.StatusCode != http.StatusForbidden { + t.Fatalf("fremd-edit: erwartet 403, bekam %d", resp.StatusCode) + } + + // Unbekannte pid -> 404. + resp = postForm(t, alice, srv.URL+"/entry/999999/edit", url.Values{"content": {"x"}}) + resp.Body.Close() + if resp.StatusCode != http.StatusNotFound { + t.Fatalf("edit unbekannt: erwartet 404, bekam %d", resp.StatusCode) + } + + // Nach Soft-Delete ist der Beitrag nicht mehr bearbeitbar (uid=0) -> 403. + postForm(t, alice, fmt.Sprintf("%s/entry/%d/delete", srv.URL, pid), url.Values{}).Body.Close() + resp = postForm(t, alice, editURL, url.Values{"content": {"wieder da"}}) + resp.Body.Close() + if resp.StatusCode != http.StatusForbidden { + t.Fatalf("edit nach delete: erwartet 403, bekam %d", resp.StatusCode) + } +} + func TestSoftDelete(t *testing.T) { srv := newTestServer(t) alice := registerAndLogin(t, srv, "alice") diff --git a/entry.go b/entry.go index 76b3098..816015f 100644 --- a/entry.go +++ b/entry.go @@ -264,6 +264,43 @@ func handleCreateEntry(w http.ResponseWriter, r *http.Request) { }) } +// handleEditEntry ändert den Inhalt eines eigenen Beitrags. Das angehängte Bild +// bleibt unverändert; last_activity wird nicht angefasst (eine Korrektur soll den +// Thread nicht nach oben spülen). +func handleEditEntry(w http.ResponseWriter, r *http.Request) { + pid, err := strconv.ParseInt(chi.URLParam(r, "pid"), 10, 64) + if err != nil { + writeError(w, http.StatusBadRequest, "Ungültige pid") + return + } + uid := uidFromContext(r.Context()) + + var owner int64 + if err := db.QueryRow(`SELECT uid FROM entry WHERE pid = ?`, pid).Scan(&owner); err != nil { + writeError(w, http.StatusNotFound, "Beitrag nicht gefunden") + return + } + if owner != uid { + writeError(w, http.StatusForbidden, "Das ist nicht dein Beitrag.") + return + } + + content := strings.TrimSpace(r.FormValue("content")) + if content == "" { + writeError(w, http.StatusBadRequest, "Inhalt darf nicht leer sein.") + return + } + if len(content) > 1000 { + content = content[:1000] + } + + if _, err := db.Exec(`UPDATE entry SET content = ? WHERE pid = ?`, content, pid); err != nil { + writeError(w, http.StatusInternalServerError, "Bearbeiten fehlgeschlagen") + return + } + writeJSON(w, http.StatusOK, map[string]any{"pid": pid, "content": content}) +} + // handleDeleteEntry löscht einen eigenen Beitrag "weich": die Zeile bleibt als // [deleted]-Platzhalter erhalten (damit Antworten nicht verwaisen), Inhalt, Bild // und Autor werden entfernt. diff --git a/main.go b/main.go index 55142d8..59243c3 100644 --- a/main.go +++ b/main.go @@ -50,6 +50,7 @@ func routes() http.Handler { r.Use(requireAuth) r.Post("/entry/create", handleCreateEntry) r.Post("/entry/{pid}/vote", handleVote) + r.Post("/entry/{pid}/edit", handleEditEntry) r.Post("/entry/{pid}/delete", handleDeleteEntry) r.Get("/user/info", handleUserInfo) r.Post("/user/rename", handleUserRename) diff --git a/web/js/entry.js b/web/js/entry.js index 018c021..072115f 100644 --- a/web/js/entry.js +++ b/web/js/entry.js @@ -35,19 +35,43 @@ async function load() { focus.innerHTML = ""; focus.appendChild(renderEntry(data.entry)); - // Eingeloggt? Reply-Box zeigen; eigener, nicht gelöschter Beitrag -> Löschen-Button. + // Eingeloggt? Reply-Box zeigen; eigener, nicht gelöschter Beitrag -> Bearbeiten/Löschen. const me = await api.get("/user/info"); document.getElementById("reply-box").hidden = !me.uid; if (me.uid === data.entry.uid && !data.entry.deleted) { - const btn = document.createElement("button"); - btn.textContent = "Löschen"; - btn.addEventListener("click", async () => { + const editBtn = document.createElement("button"); + editBtn.textContent = "Bearbeiten"; + editBtn.addEventListener("click", () => { + editBtn.disabled = true; + const form = document.createElement("form"); + form.innerHTML = ` + + + +
`; + form.querySelector("textarea").value = data.entry.content; + form.addEventListener("submit", async (ev) => { + ev.preventDefault(); + const res = await api.post(`/entry/${pid}/edit`, new FormData(form)); + if (res.ok) load(); + else form.querySelector(".msg").textContent = + res.data.error || "Bearbeiten fehlgeschlagen."; + }); + form.querySelector(".cancel").addEventListener("click", load); + focus.appendChild(form); + }); + + const delBtn = document.createElement("button"); + delBtn.textContent = "Löschen"; + delBtn.addEventListener("click", async () => { if (!confirm("Beitrag wirklich löschen?")) return; const res = await api.post(`/entry/${pid}/delete`, new FormData()); if (res.ok) load(); else alert(res.data.error || "Löschen fehlgeschlagen."); }); - focus.appendChild(btn); + + focus.appendChild(editBtn); + focus.appendChild(delBtn); } // Direkte Antworten.