Soft-Delete: Beiträge als [deleted] erhalten statt löschen
- entry.deleted; entrySelect LEFT JOIN + COALESCE(username)
- POST /entry/{pid}/delete: deleted=1, Inhalt/Bild/Autor geleert, Zeile bleibt
- Konto-Löschung soft-deletet die eigenen Beiträge (statt hartem DELETE),
damit fremde Antworten nicht verwaisen
- Frontend: [deleted]-Platzhalter im Feed, Löschen-Button auf Focus-Seite
- notes/migrations.md: ALTER TABLE deleted
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -28,13 +28,17 @@ type Entry struct {
|
||||
ReplyTo int64 `json:"reply_to"`
|
||||
ReplyCount int64 `json:"reply_count"`
|
||||
LastActivity int64 `json:"last_activity"`
|
||||
Deleted int64 `json:"deleted"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// LEFT JOIN + COALESCE, damit soft-gelöschte Beiträge (uid auf 0 gesetzt) als
|
||||
// [deleted]-Platzhalter erhalten bleiben und Threads nicht verwaisen.
|
||||
const entrySelect = `
|
||||
SELECT e.pid, e.uid, e.created_at, e.content, e.filepath,
|
||||
e.reply_to, e.reply_count, e.last_activity, u.username
|
||||
FROM entry e JOIN user u ON u.uid = e.uid`
|
||||
e.reply_to, e.reply_count, e.last_activity, e.deleted,
|
||||
COALESCE(u.username, '')
|
||||
FROM entry e LEFT JOIN user u ON u.uid = e.uid`
|
||||
|
||||
// scanEntries liest Entry-Zeilen aus einem Query mit entrySelect-Spalten.
|
||||
func scanEntries(rows *sql.Rows) ([]Entry, error) {
|
||||
@@ -43,7 +47,7 @@ func scanEntries(rows *sql.Rows) ([]Entry, error) {
|
||||
for rows.Next() {
|
||||
var e Entry
|
||||
if err := rows.Scan(&e.PID, &e.UID, &e.CreatedAt, &e.Content, &e.Filepath,
|
||||
&e.ReplyTo, &e.ReplyCount, &e.LastActivity, &e.Username); err != nil {
|
||||
&e.ReplyTo, &e.ReplyCount, &e.LastActivity, &e.Deleted, &e.Username); err != nil {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, e)
|
||||
@@ -56,7 +60,7 @@ func entryByPID(pid int64) (Entry, error) {
|
||||
var e Entry
|
||||
err := db.QueryRow(entrySelect+` WHERE e.pid = ?`, pid).Scan(
|
||||
&e.PID, &e.UID, &e.CreatedAt, &e.Content, &e.Filepath,
|
||||
&e.ReplyTo, &e.ReplyCount, &e.LastActivity, &e.Username)
|
||||
&e.ReplyTo, &e.ReplyCount, &e.LastActivity, &e.Deleted, &e.Username)
|
||||
return e, err
|
||||
}
|
||||
|
||||
@@ -260,6 +264,40 @@ func handleCreateEntry(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleDeleteEntry löscht einen eigenen Beitrag "weich": die Zeile bleibt als
|
||||
// [deleted]-Platzhalter erhalten (damit Antworten nicht verwaisen), Inhalt, Bild
|
||||
// und Autor werden entfernt.
|
||||
func handleDeleteEntry(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
|
||||
var filepath string
|
||||
if err := db.QueryRow(`SELECT uid, filepath FROM entry WHERE pid = ?`, pid).Scan(&owner, &filepath); err != nil {
|
||||
writeError(w, http.StatusNotFound, "Beitrag nicht gefunden")
|
||||
return
|
||||
}
|
||||
if owner != uid {
|
||||
writeError(w, http.StatusForbidden, "Das ist nicht dein Beitrag.")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := db.Exec(
|
||||
`UPDATE entry SET deleted = 1, content = '', filepath = '', uid = 0 WHERE pid = ?`, pid,
|
||||
); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Löschen fehlgeschlagen")
|
||||
return
|
||||
}
|
||||
if filepath != "" {
|
||||
os.Remove(filepath)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
// voteTally liefert die Zähler und die eigene Auswahl (selected) für einen Beitrag.
|
||||
// Ist uid == 0 (nicht eingeloggt), ist selected immer "none".
|
||||
func voteTally(pid, uid int64) (left, right int, selected string) {
|
||||
|
||||
Reference in New Issue
Block a user