Implement user account deletion flow

POST /user/delete (auth required) verifies the password via pass1 and
transactionally removes the user's sessions, votes, entries and the user
row, then clears the session cookie. Associated media files are removed
best effort after commit. This was only a stub in the Flask original.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 10:19:29 +02:00
parent cb237c248e
commit c904e3e2db
3 changed files with 79 additions and 5 deletions
+1 -1
View File
@@ -32,13 +32,13 @@ func main() {
// --- User (öffentlich) ---
r.Get("/u/{username}", handleUserPage)
r.Get("/user/delete", handleUserDelete)
// --- Geschützt (Session erforderlich) ---
r.Group(func(r chi.Router) {
r.Use(requireAuth)
r.Post("/entry/create", handleCreateEntry)
r.Get("/user/info", handleUserInfo)
r.Post("/user/delete", handleUserDelete)
})
// Hochgeladene Medien + alte statische Assets
+14 -3
View File
@@ -174,9 +174,21 @@ Eigene Account-Informationen.
---
### `GET /user/delete`
### `POST /user/delete`
Account löschen. (*Noch nicht implementiert.*)
Eigenen Account dauerhaft löschen. Beiträge, Votes und Sessions werden
mitgelöscht, zugehörige Mediendateien best effort entfernt.
**Auth:** erforderlich
**Request (form-encoded):**
| Feld | Typ | Beschreibung |
|------|-----|--------------|
| `pass1` | string | Passwort zur Bestätigung |
**Response:**
- `200` `{"status": "deleted"}` + löscht den `session`-Cookie
- `403` bei falschem Passwort
---
@@ -223,7 +235,6 @@ CREATE TABLE vote (
## Offene Punkte
- `/entry/:pid/interactions/:mode` ist auskommentiert — Voting-Logik muss neu designed werden
- `/user/delete` ist noch ein Stub
- Kein Rate-Limiting außer dem zufälligen `sleep` beim Login
- `session`-Token ist nur `randbelow(999999999) + timestamp` — sollte auf `crypto/rand` umgestellt werden
- Fehler-Responses sind aktuell Plaintext/HTML — in der Go-API einheitlich JSON
+64 -1
View File
@@ -4,6 +4,7 @@ import (
"database/sql"
"math/rand/v2"
"net/http"
"os"
"time"
"github.com/go-chi/chi/v5"
@@ -71,6 +72,68 @@ func handleUserInfo(w http.ResponseWriter, r *http.Request) {
})
}
// 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.
func handleUserDelete(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusNotImplemented, "Account-Löschung ist noch nicht implementiert")
uid := uidFromContext(r.Context())
var hash []byte
if err := db.QueryRow(`SELECT password FROM user WHERE uid = ?`, uid).Scan(&hash); err != nil {
writeError(w, http.StatusNotFound, "Nutzer nicht gefunden")
return
}
if bcrypt.CompareHashAndPassword(hash, []byte(r.FormValue("pass1"))) != nil {
writeError(w, http.StatusForbidden, "Passwort ist falsch.")
return
}
// Mediendateien der Beiträge einsammeln, bevor die Zeilen verschwinden.
var media []string
if rows, err := db.Query(`SELECT filepath FROM entry WHERE uid = ?`, uid); err == nil {
for rows.Next() {
var fp string
if rows.Scan(&fp) == nil && fp != "" && fp != "none" {
media = append(media, fp)
}
}
rows.Close()
}
tx, err := db.Begin()
if err != nil {
writeError(w, http.StatusInternalServerError, "Löschen fehlgeschlagen")
return
}
for _, q := range []string{
`DELETE FROM session WHERE uid = ?`,
`DELETE FROM vote WHERE uid = ?`,
`DELETE FROM entry WHERE uid = ?`,
`DELETE FROM user WHERE uid = ?`,
} {
if _, err := tx.Exec(q, uid); err != nil {
tx.Rollback()
writeError(w, http.StatusInternalServerError, "Löschen fehlgeschlagen")
return
}
}
if err := tx.Commit(); err != nil {
writeError(w, http.StatusInternalServerError, "Löschen fehlgeschlagen")
return
}
// Erst nach erfolgreichem Commit die Dateien entfernen.
for _, fp := range media {
os.Remove(fp)
}
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: "",
Path: "/",
Expires: time.Unix(0, 0),
MaxAge: -1,
})
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
}