130 lines
4.6 KiB
Go
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)
|
||
|
|
}
|