favoritter/internal/handler/admin.go

352 lines
10 KiB
Go
Raw Normal View History

// SPDX-License-Identifier: AGPL-3.0-or-later
package handler
import (
"crypto/rand"
"encoding/hex"
"errors"
"log/slog"
"net/http"
"strconv"
"strings"
"kode.naiv.no/olemd/favoritter/internal/middleware"
"kode.naiv.no/olemd/favoritter/internal/render"
"kode.naiv.no/olemd/favoritter/internal/store"
)
// handleAdminDashboard shows admin overview stats.
func (h *Handler) handleAdminDashboard(w http.ResponseWriter, r *http.Request) {
userCount, _ := h.deps.Users.Count()
faveCount, _ := h.deps.Faves.Count()
pendingCount, _ := h.deps.SignupRequests.PendingCount()
settings, _ := h.deps.Settings.Get()
h.deps.Renderer.Page(w, r, "admin_dashboard", render.PageData{
Title: "Administrasjon",
Data: map[string]any{
"UserCount": userCount,
"FaveCount": faveCount,
"PendingCount": pendingCount,
"Settings": settings,
},
})
}
// handleAdminUsers lists all users.
func (h *Handler) handleAdminUsers(w http.ResponseWriter, r *http.Request) {
users, err := h.deps.Users.ListAll()
if err != nil {
slog.Error("list users error", "error", err)
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
h.deps.Renderer.Page(w, r, "admin_users", render.PageData{
Title: "Brukere",
Data: map[string]any{"Users": users},
})
}
// handleAdminCreateUser creates a new user with a temporary password.
func (h *Handler) handleAdminCreateUser(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
username := strings.TrimSpace(r.FormValue("username"))
role := r.FormValue("role")
if !usernameRegexp.MatchString(username) {
h.adminUsersFlash(w, r, "Ugyldig brukernavn.", "error")
return
}
if role != "user" && role != "admin" {
role = "user"
}
// Generate a temporary password the admin can share with the user.
tempPassword := generateTempPassword()
_, err := h.deps.Users.CreateWithReset(username, tempPassword, role)
if err != nil {
if errors.Is(err, store.ErrUserExists) {
h.adminUsersFlash(w, r, "Brukernavnet er allerede i bruk.", "error")
return
}
slog.Error("admin create user error", "error", err)
h.adminUsersFlash(w, r, "Noe gikk galt.", "error")
return
}
h.adminUsersFlash(w, r,
"Bruker opprettet. Midlertidig passord: "+tempPassword+" — brukeren må endre det ved første innlogging.",
"success")
}
// handleAdminResetPassword resets a user's password to a temporary one.
func (h *Handler) handleAdminResetPassword(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
http.NotFound(w, r)
return
}
tempPassword := generateTempPassword()
if err := h.deps.Users.UpdatePassword(id, tempPassword); err != nil {
slog.Error("admin reset password error", "error", err)
h.adminUsersFlash(w, r, "Noe gikk galt.", "error")
return
}
if err := h.deps.Users.SetMustResetPassword(id, true); err != nil {
slog.Error("set must-reset-password error", "error", err)
}
// Invalidate all sessions for this user.
if delErr := h.deps.Sessions.DeleteAllForUser(id); delErr != nil {
slog.Error("session cleanup error", "error", delErr)
}
h.adminUsersFlash(w, r,
"Passord tilbakestilt. Midlertidig passord: "+tempPassword,
"success")
}
// handleAdminToggleDisabled enables or disables a user.
func (h *Handler) handleAdminToggleDisabled(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
}
// Don't allow disabling yourself.
if id == admin.ID {
h.adminUsersFlash(w, r, "Du kan ikke deaktivere 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
}
if err := h.deps.Users.SetDisabled(id, !user.Disabled); err != nil {
slog.Error("toggle disabled error", "error", err)
h.adminUsersFlash(w, r, "Noe gikk galt.", "error")
return
}
if !user.Disabled {
// Was enabled, now disabled — invalidate sessions.
if delErr := h.deps.Sessions.DeleteAllForUser(id); delErr != nil {
slog.Error("session cleanup error", "error", delErr)
}
}
action := "aktivert"
if !user.Disabled {
action = "deaktivert"
}
h.adminUsersFlash(w, r, "Bruker "+user.Username+" er "+action+".", "success")
}
// handleAdminTags lists all tags.
func (h *Handler) handleAdminTags(w http.ResponseWriter, r *http.Request) {
tags, err := h.deps.Tags.ListAll()
if err != nil {
slog.Error("list tags error", "error", err)
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
h.deps.Renderer.Page(w, r, "admin_tags", render.PageData{
Title: "Merkelapper",
Data: map[string]any{"Tags": tags},
})
}
// handleAdminRenameTag renames a tag.
func (h *Handler) handleAdminRenameTag(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
http.NotFound(w, r)
return
}
newName := strings.TrimSpace(r.FormValue("name"))
if newName == "" {
h.adminTagsFlash(w, r, "Navn kan ikke være tomt.", "error")
return
}
if err := h.deps.Tags.Rename(id, newName); err != nil {
slog.Error("rename tag error", "error", err)
h.adminTagsFlash(w, r, "Noe gikk galt. Kanskje navnet allerede finnes?", "error")
return
}
h.adminTagsFlash(w, r, "Merkelapp omdøpt.", "success")
}
// handleAdminDeleteTag deletes a tag.
func (h *Handler) handleAdminDeleteTag(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
http.NotFound(w, r)
return
}
if err := h.deps.Tags.Delete(id); err != nil {
slog.Error("delete tag error", "error", err)
h.adminTagsFlash(w, r, "Noe gikk galt.", "error")
return
}
h.adminTagsFlash(w, r, "Merkelapp slettet.", "success")
}
// handleAdminSignupRequests lists pending signup requests.
func (h *Handler) handleAdminSignupRequests(w http.ResponseWriter, r *http.Request) {
requests, err := h.deps.SignupRequests.ListPending()
if err != nil {
slog.Error("list signup requests error", "error", err)
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
h.deps.Renderer.Page(w, r, "admin_requests", render.PageData{
Title: "Registreringsforespørsler",
Data: map[string]any{"Requests": requests},
})
}
// handleAdminSignupRequestAction approves or rejects a signup request.
func (h *Handler) handleAdminSignupRequestAction(w http.ResponseWriter, r *http.Request) {
admin := middleware.UserFromContext(r.Context())
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
http.NotFound(w, r)
return
}
action := r.FormValue("action")
switch action {
case "approve":
if err := h.deps.SignupRequests.Approve(id, admin.ID); err != nil {
slog.Error("approve signup request error", "error", err)
h.adminRequestsFlash(w, r, "Noe gikk galt ved godkjenning.", "error")
return
}
h.adminRequestsFlash(w, r, "Forespørsel godkjent. Brukeren må endre passord ved første innlogging.", "success")
case "reject":
if err := h.deps.SignupRequests.Reject(id, admin.ID); err != nil {
slog.Error("reject signup request error", "error", err)
h.adminRequestsFlash(w, r, "Noe gikk galt.", "error")
return
}
h.adminRequestsFlash(w, r, "Forespørsel avvist.", "success")
default:
http.Error(w, "Bad request", http.StatusBadRequest)
}
}
// handleAdminSettingsGet shows the site settings page.
func (h *Handler) handleAdminSettingsGet(w http.ResponseWriter, r *http.Request) {
settings, err := h.deps.Settings.Get()
if err != nil {
slog.Error("get settings error", "error", err)
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
h.deps.Renderer.Page(w, r, "admin_settings", render.PageData{
Title: "Nettstedsinnstillinger",
Data: map[string]any{"Settings": settings},
})
}
// handleAdminSettingsPost updates site settings.
func (h *Handler) handleAdminSettingsPost(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
siteName := strings.TrimSpace(r.FormValue("site_name"))
siteDescription := strings.TrimSpace(r.FormValue("site_description"))
signupMode := r.FormValue("signup_mode")
if siteName == "" {
siteName = "Favoritter"
}
if signupMode != "open" && signupMode != "requests" && signupMode != "closed" {
signupMode = "open"
}
if err := h.deps.Settings.Update(siteName, siteDescription, signupMode); err != nil {
slog.Error("update settings error", "error", err)
h.adminSettingsFlash(w, r, "Noe gikk galt.", "error")
return
}
h.adminSettingsFlash(w, r, "Innstillingene er lagret.", "success")
}
// Flash helpers that redirect back to the relevant admin page.
func (h *Handler) adminUsersFlash(w http.ResponseWriter, r *http.Request, msg, flashType string) {
// Re-fetch and re-render the users page with flash.
users, _ := h.deps.Users.ListAll()
h.deps.Renderer.Page(w, r, "admin_users", render.PageData{
Title: "Brukere", Flash: msg, FlashType: flashType,
Data: map[string]any{"Users": users},
})
}
func (h *Handler) adminTagsFlash(w http.ResponseWriter, r *http.Request, msg, flashType string) {
tags, _ := h.deps.Tags.ListAll()
h.deps.Renderer.Page(w, r, "admin_tags", render.PageData{
Title: "Merkelapper", Flash: msg, FlashType: flashType,
Data: map[string]any{"Tags": tags},
})
}
func (h *Handler) adminRequestsFlash(w http.ResponseWriter, r *http.Request, msg, flashType string) {
requests, _ := h.deps.SignupRequests.ListPending()
h.deps.Renderer.Page(w, r, "admin_requests", render.PageData{
Title: "Registreringsforespørsler", Flash: msg, FlashType: flashType,
Data: map[string]any{"Requests": requests},
})
}
func (h *Handler) adminSettingsFlash(w http.ResponseWriter, r *http.Request, msg, flashType string) {
settings, _ := h.deps.Settings.Get()
h.deps.Renderer.Page(w, r, "admin_settings", render.PageData{
Title: "Nettstedsinnstillinger", Flash: msg, FlashType: flashType,
Data: map[string]any{"Settings": settings},
})
}
func generateTempPassword() string {
b := make([]byte, 12)
rand.Read(b)
return hex.EncodeToString(b)
}