// Copyright (c) 2026 Petr BalvĂ­n (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) }