Split voting into clean read/write endpoints
Replaces the GET-mutates-state interactions route with a proper split:
- GET /entry/{pid}/votes reads the tally (public, read-only)
- POST /entry/{pid}/vote casts/toggles the vote (auth, mode in body)
Shared voteTally/writeTally helpers back both handlers. Invalid mode now
returns 400. Frontend updated to read via GET and vote via POST.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -106,58 +106,23 @@ func handleCreateEntry(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleInteractions liest den Abstimmungsstand eines Beitrags und schaltet bei
|
||||
// mode=left/right die Stimme des angemeldeten Nutzers um:
|
||||
// - keine bisherige Stimme -> neue Stimme
|
||||
// - gleiche Stimme erneut -> Stimme zurückziehen (Toggle)
|
||||
// - andere Stimme -> auf den neuen Modus wechseln
|
||||
// mode=none liest nur (z. B. beim Laden des Feeds).
|
||||
func handleInteractions(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
|
||||
}
|
||||
mode := chi.URLParam(r, "mode")
|
||||
|
||||
var uid int64
|
||||
if s, ok := getSession(r); ok {
|
||||
uid = s.UID
|
||||
}
|
||||
|
||||
if mode == "left" || mode == "right" {
|
||||
if uid == 0 {
|
||||
writeError(w, http.StatusUnauthorized, "Melde dich an, um abzustimmen!")
|
||||
return
|
||||
}
|
||||
|
||||
var prev string
|
||||
err := db.QueryRow(`SELECT mode FROM vote WHERE uid = ? AND pid = ?`, uid, pid).Scan(&prev)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
db.Exec(`INSERT INTO vote (uid, pid, mode) VALUES (?, ?, ?)`, uid, pid, mode)
|
||||
case err == nil && prev == mode:
|
||||
db.Exec(`DELETE FROM vote WHERE uid = ? AND pid = ?`, uid, pid)
|
||||
case err == nil:
|
||||
db.Exec(`UPDATE vote SET mode = ? WHERE uid = ? AND pid = ?`, mode, uid, pid)
|
||||
default:
|
||||
writeError(w, http.StatusInternalServerError, "Datenbankfehler")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
selected := "none"
|
||||
// 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) {
|
||||
selected = "none"
|
||||
if uid != 0 {
|
||||
var cur string
|
||||
if err := db.QueryRow(`SELECT mode FROM vote WHERE uid = ? AND pid = ?`, uid, pid).Scan(&cur); err == nil {
|
||||
selected = cur
|
||||
}
|
||||
}
|
||||
|
||||
var left, right int
|
||||
db.QueryRow(`SELECT COUNT(*) FROM vote WHERE pid = ? AND mode = 'left'`, pid).Scan(&left)
|
||||
db.QueryRow(`SELECT COUNT(*) FROM vote WHERE pid = ? AND mode = 'right'`, pid).Scan(&right)
|
||||
return
|
||||
}
|
||||
|
||||
func writeTally(w http.ResponseWriter, pid, uid int64) {
|
||||
left, right, selected := voteTally(pid, uid)
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"pid": pid,
|
||||
"left": left,
|
||||
@@ -166,6 +131,63 @@ func handleInteractions(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleVotes liest den Abstimmungsstand eines Beitrags (read-only, öffentlich).
|
||||
func handleVotes(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
|
||||
}
|
||||
|
||||
var uid int64
|
||||
if s, ok := getSession(r); ok {
|
||||
uid = s.UID
|
||||
}
|
||||
writeTally(w, pid, uid)
|
||||
}
|
||||
|
||||
// handleVote gibt die Stimme des angemeldeten Nutzers ab oder schaltet sie um:
|
||||
// - keine bisherige Stimme -> neue Stimme
|
||||
// - gleiche Stimme erneut -> Stimme zurückziehen (Toggle)
|
||||
// - andere Stimme -> auf den neuen Modus wechseln
|
||||
func handleVote(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())
|
||||
|
||||
mode := r.FormValue("mode")
|
||||
if mode != "left" && mode != "right" {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiger Modus (left oder right)")
|
||||
return
|
||||
}
|
||||
|
||||
var prev string
|
||||
readErr := db.QueryRow(`SELECT mode FROM vote WHERE uid = ? AND pid = ?`, uid, pid).Scan(&prev)
|
||||
if readErr != nil && readErr != sql.ErrNoRows {
|
||||
writeError(w, http.StatusInternalServerError, "Datenbankfehler")
|
||||
return
|
||||
}
|
||||
|
||||
var execErr error
|
||||
switch {
|
||||
case readErr == sql.ErrNoRows:
|
||||
_, execErr = db.Exec(`INSERT INTO vote (uid, pid, mode) VALUES (?, ?, ?)`, uid, pid, mode)
|
||||
case prev == mode:
|
||||
_, execErr = db.Exec(`DELETE FROM vote WHERE uid = ? AND pid = ?`, uid, pid)
|
||||
default:
|
||||
_, execErr = db.Exec(`UPDATE vote SET mode = ? WHERE uid = ? AND pid = ?`, mode, uid, pid)
|
||||
}
|
||||
if execErr != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Datenbankfehler")
|
||||
return
|
||||
}
|
||||
|
||||
writeTally(w, pid, uid)
|
||||
}
|
||||
|
||||
// storeImage skaliert ein hochgeladenes Bild auf max. 1024px und speichert es unter static/media.
|
||||
// Hinweis: animierte GIFs werden derzeit auf das erste Frame reduziert.
|
||||
func storeImage(fh *multipart.FileHeader) (string, error) {
|
||||
|
||||
Reference in New Issue
Block a user