c904e3e2db
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>
140 lines
3.6 KiB
Go
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"})
|
|
}
|