Bearbeiten: POST /entry/{pid}/edit (Inhalt ändern)
- handleEditEntry: Eigentümerprüfung, nicht-leer, content-only Update - Bild und last_activity bleiben unverändert; gelöschte Posts nicht editierbar - Frontend: Bearbeiten-Button + Inline-Formular auf der Focus-Seite Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
+29
-5
@@ -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 = `
|
||||
<textarea name="content" rows="4" maxlength="1000"></textarea>
|
||||
<button type="submit">Speichern</button>
|
||||
<button type="button" class="cancel">Abbrechen</button>
|
||||
<p class="msg"></p>`;
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user