feat: add admin role management and user deletion

Admins can now change user roles and permanently delete user accounts.

- New SetRole store method with validation (user/admin only)
- New Delete store method — cascades via foreign keys to sessions,
  faves, and fave_tags
- handleAdminSetRole: change role with self-modification prevention
- handleAdminDeleteUser: permanent deletion with image cleanup from
  disk before cascade delete, self-deletion prevention
- admin_users.html: role dropdown with save button per user row,
  delete button with hx-confirm for safety
- Routes: POST /admin/users/{id}/role, POST /admin/users/{id}/delete

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-04-07 10:18:00 +02:00
commit 254573316a
4 changed files with 195 additions and 0 deletions

View file

@ -11,6 +11,7 @@ import (
"strconv"
"strings"
"kode.naiv.no/olemd/favoritter/internal/image"
"kode.naiv.no/olemd/favoritter/internal/middleware"
"kode.naiv.no/olemd/favoritter/internal/render"
"kode.naiv.no/olemd/favoritter/internal/store"
@ -158,6 +159,88 @@ func (h *Handler) handleAdminToggleDisabled(w http.ResponseWriter, r *http.Reque
h.adminUsersFlash(w, r, "Bruker "+user.Username+" er "+action+".", "success")
}
// handleAdminSetRole changes a user's role.
func (h *Handler) handleAdminSetRole(w http.ResponseWriter, r *http.Request) {
admin := middleware.UserFromContext(r.Context())
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
http.NotFound(w, r)
return
}
if id == admin.ID {
h.adminUsersFlash(w, r, "Du kan ikke endre din egen rolle.", "error")
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
role := r.FormValue("role")
if role != "user" && role != "admin" {
h.adminUsersFlash(w, r, "Ugyldig rolle.", "error")
return
}
if err := h.deps.Users.SetRole(id, role); err != nil {
slog.Error("set role error", "error", err)
h.adminUsersFlash(w, r, "Noe gikk galt.", "error")
return
}
user, _ := h.deps.Users.GetByID(id)
h.adminUsersFlash(w, r, "Rollen til "+user.Username+" er endret til "+role+".", "success")
}
// handleAdminDeleteUser permanently deletes a user and all their data.
func (h *Handler) handleAdminDeleteUser(w http.ResponseWriter, r *http.Request) {
admin := middleware.UserFromContext(r.Context())
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
http.NotFound(w, r)
return
}
if id == admin.ID {
h.adminUsersFlash(w, r, "Du kan ikke slette din egen konto.", "error")
return
}
user, err := h.deps.Users.GetByID(id)
if err != nil {
slog.Error("get user error", "error", err)
h.adminUsersFlash(w, r, "Noe gikk galt.", "error")
return
}
// Delete user's images from disk before database deletion.
faves, _, _ := h.deps.Faves.ListByUser(id, 100000, 0)
for _, f := range faves {
if f.ImagePath != "" {
if delErr := image.Delete(h.deps.Config.UploadDir, f.ImagePath); delErr != nil {
slog.Error("image delete error", "fave_id", f.ID, "error", delErr)
}
}
}
if user.AvatarPath != "" {
if delErr := image.Delete(h.deps.Config.UploadDir, user.AvatarPath); delErr != nil {
slog.Error("avatar delete error", "user_id", id, "error", delErr)
}
}
if err := h.deps.Users.Delete(id); err != nil {
slog.Error("delete user error", "error", err)
h.adminUsersFlash(w, r, "Noe gikk galt.", "error")
return
}
h.adminUsersFlash(w, r, "Bruker "+user.Username+" er permanent slettet.", "success")
}
// handleAdminTags lists all tags.
func (h *Handler) handleAdminTags(w http.ResponseWriter, r *http.Request) {
tags, err := h.deps.Tags.ListAll()