diff --git a/main.go b/main.go index 40f2184..bd10518 100644 --- a/main.go +++ b/main.go @@ -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 diff --git a/notes/api.md b/notes/api.md index 7c37f32..98059a3 100644 --- a/notes/api.md +++ b/notes/api.md @@ -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 diff --git a/user.go b/user.go index 4ffe62b..e768f21 100644 --- a/user.go +++ b/user.go @@ -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"}) }