Files
volumen/internal/admin/settings_users.go
T
petrbalvin f8ed33df83
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Initial commit
Assisted-by: GLM 5.3
2026-09-29 10:03:32 +02:00

130 lines
4.6 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
package admin
import (
"net/http"
"strings"
"sourcedock.dev/petrbalvin/volumen/internal/i18n"
)
func (a *Admin) handleSettingsUserCreate(w http.ResponseWriter, r *http.Request) {
if !a.requireCSRF(w, r) {
return
}
username := strings.TrimSpace(r.PostFormValue("username"))
// A password keeps its edge spaces: the reset path stores them the
// same way, and trimming here would create a password only the
// trimmed form of which works.
password := r.PostFormValue("password")
role := r.PostFormValue("role")
if username == "" || strings.TrimSpace(password) == "" {
a.renderSettings(w, r, a.tr(r, "Username and password are required."), "", http.StatusUnprocessableEntity)
return
}
if !usernameRe.MatchString(username) {
a.renderSettings(w, r, a.tr(r, "Username may use letters, numbers, dot, dash, underscore."), "", http.StatusUnprocessableEntity)
return
}
minLen, maxLen := a.passwordPolicy()
if key, n := PasswordError(password, minLen, maxLen); key != "" {
msg := a.tr(r, key)
if n > 0 {
msg = i18n.Admin.N(a.langFor(r), key, n)
}
a.renderSettings(w, r, msg, "", http.StatusUnprocessableEntity)
return
}
if _, err := a.deps.Users.Add(username, password, role); err != nil {
a.renderSettings(w, r, a.trf(r, "That user could not be added: %s", err.Error()), "", http.StatusUnprocessableEntity)
return
}
a.record(r, "user.created", username, nil)
a.renderSettings(w, r, "", a.tr(r, "User added."), http.StatusOK)
}
func (a *Admin) handleSettingsUserRole(w http.ResponseWriter, r *http.Request) {
if !a.requireCSRF(w, r) {
return
}
target := r.PathValue("name")
if target == a.currentUser(r) {
a.renderSettings(w, r, a.tr(r, "You cannot change your own role."), "", http.StatusUnprocessableEntity)
return
}
if _, err := a.deps.Users.SetRole(target, r.PostFormValue("role")); err != nil {
a.renderSettings(w, r, a.trf(r, "The role could not be changed: %s", err.Error()), "", http.StatusUnprocessableEntity)
return
}
a.record(r, "user.role_changed", target, nil)
a.renderSettings(w, r, "", a.tr(r, "Role updated."), http.StatusOK)
}
func (a *Admin) handleSettingsUserDelete(w http.ResponseWriter, r *http.Request) {
if !a.requireCSRF(w, r) {
return
}
target := r.PathValue("name")
if target == a.currentUser(r) {
a.renderSettings(w, r, a.tr(r, "You cannot delete your own account."), "", http.StatusUnprocessableEntity)
return
}
photo := ""
if record := a.deps.Users.Find(target); record != nil {
photo = record.Photo
}
if _, err := a.deps.Users.Delete(target); err != nil {
a.renderSettings(w, r, a.trf(r, "The user could not be removed: %s", err.Error()), "", http.StatusUnprocessableEntity)
return
}
if photo != "" {
a.deleteUnreferencedMedia(photo)
}
a.record(r, "user.deleted", target, nil)
a.renderSettings(w, r, "", a.tr(r, "User removed."), http.StatusOK)
}
// --- post templates ---------------------------------------------------------
// handleSettingsUserPassword resets another account's password. The
// account's sessions die with the change (the session fingerprint
// changes), which is the point: an admin resetting a password is
// remedying an account, and every cookie issued before must stop
// working.
func (a *Admin) handleSettingsUserPassword(w http.ResponseWriter, r *http.Request) {
if !a.requireCSRF(w, r) {
return
}
target := r.PathValue("name")
if target == a.currentUser(r) {
a.renderSettings(w, r, a.tr(r, "You cannot reset your own password here."), "", http.StatusUnprocessableEntity)
return
}
if a.deps.Users.Find(target) == nil {
a.renderSettings(w, r, a.tr(r, "That user was not found."), "", http.StatusUnprocessableEntity)
return
}
newPassword := r.PostFormValue("password")
if strings.TrimSpace(newPassword) == "" {
a.renderSettings(w, r, a.tr(r, "New password cannot be empty."), "", http.StatusUnprocessableEntity)
return
}
minLen, maxLen := a.passwordPolicy()
if key, n := PasswordError(newPassword, minLen, maxLen); key != "" {
msg := a.tr(r, key)
if n > 0 {
msg = i18n.Admin.N(a.langFor(r), key, n)
}
a.renderSettings(w, r, msg, "", http.StatusUnprocessableEntity)
return
}
if _, err := a.deps.Users.UpdatePassword(target, newPassword); err != nil {
a.renderSettings(w, r, a.trf(r, "The new password could not be saved: %s", err.Error()), "", http.StatusInternalServerError)
return
}
a.record(r, "user.password_reset", target, nil)
a.renderSettings(w, r, "", a.tr(r, "Password reset; that user's sessions were signed out."), http.StatusOK)
}