Files
Kontrollverlust/user.go
T
irrlicht c904e3e2db 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>
2026-06-01 10:19:29 +02:00

140 lines
3.6 KiB
Go

package main
import (
"database/sql"
"math/rand/v2"
"net/http"
"os"
"time"
"github.com/go-chi/chi/v5"
"golang.org/x/crypto/bcrypt"
)
func createUser(username, password string) error {
uid := int64(rand.IntN(99999999))
now := time.Now().Unix()
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
_, err = db.Exec(
`INSERT INTO user (uid, created_at, username, password) VALUES (?, ?, ?, ?)`,
uid, now, username, hash,
)
return err
}
// idFromUsername liefert die interne id oder 0, wenn der Nutzer nicht existiert.
func idFromUsername(username string) int64 {
var id int64
if err := db.QueryRow(`SELECT id FROM user WHERE username = ?`, username).Scan(&id); err != nil {
return 0
}
return id
}
func handleUserPage(w http.ResponseWriter, r *http.Request) {
username := chi.URLParam(r, "username")
var uid, createdAt int64
err := db.QueryRow(`SELECT uid, created_at FROM user WHERE username = ?`, username).Scan(&uid, &createdAt)
if err != nil {
writeError(w, http.StatusNotFound, "Nutzer nicht gefunden")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"uid": uid,
"username": username,
"created_at": createdAt,
})
}
func handleUserInfo(w http.ResponseWriter, r *http.Request) {
uid := uidFromContext(r.Context())
var createdAt, lastLogin sql.NullInt64
var username string
err := db.QueryRow(
`SELECT created_at, last_login, username FROM user WHERE uid = ?`, uid,
).Scan(&createdAt, &lastLogin, &username)
if err != nil {
writeError(w, http.StatusNotFound, "Nutzer nicht gefunden")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"uid": uid,
"username": username,
"created_at": createdAt.Int64,
"last_login": lastLogin.Int64,
})
}
// 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) {
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"})
}