From 2dd8f3c3beeb5698c447f6050c84a79c7095929c Mon Sep 17 00:00:00 2001 From: Steve Cliff Date: Tue, 5 May 2026 10:03:41 +0100 Subject: [PATCH] ui: /settings/account self-service password change Adds GET/POST handlers for /settings/account in the viewer band (any authenticated user), account.html template with current-password field suppressed when must_change_password is set, and audits the change via AppendAudit. --- internal/server/http/server.go | 2 + internal/server/http/ui_account.go | 88 ++++++++++++++++++++++++++++++ web/templates/pages/account.html | 46 ++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 web/templates/pages/account.html diff --git a/internal/server/http/server.go b/internal/server/http/server.go index 23329c8..68e7438 100644 --- a/internal/server/http/server.go +++ b/internal/server/http/server.go @@ -182,6 +182,8 @@ func (s *Server) routes(r chi.Router) { r.Get("/alerts", s.handleUIAlerts) r.Get("/audit", s.handleUIAudit) r.Get("/audit.csv", s.handleUIAuditCSV) + r.Get("/settings/account", s.handleUIAccountGet) + r.Post("/settings/account", s.handleUIAccountPost) } }) diff --git a/internal/server/http/ui_account.go b/internal/server/http/ui_account.go index d23483e..31bd025 100644 --- a/internal/server/http/ui_account.go +++ b/internal/server/http/ui_account.go @@ -64,3 +64,91 @@ func (s *Server) handleAPIAccountPassword(w stdhttp.ResponseWriter, r *stdhttp.R }) w.WriteHeader(stdhttp.StatusOK) } + +type accountPage struct { + Username string + Role string + MustChange bool + Error string + Saved bool +} + +func (s *Server) handleUIAccountGet(w stdhttp.ResponseWriter, r *stdhttp.Request) { + u := s.requireUIUser(w, r) + if u == nil { + return + } + full, err := s.deps.Store.GetUserByID(r.Context(), u.ID) + if err != nil { + stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError) + return + } + view := s.baseView(r, u) + view.Title = "Account · restic-manager" + view.Active = "settings" + view.Page = accountPage{ + Username: full.Username, Role: string(full.Role), + MustChange: full.MustChangePassword, + } + _ = s.deps.UI.Render(w, "account", view) +} + +func (s *Server) handleUIAccountPost(w stdhttp.ResponseWriter, r *stdhttp.Request) { + u := s.requireUIUser(w, r) + if u == nil { + return + } + if err := r.ParseForm(); err != nil { + stdhttp.Error(w, "bad request", stdhttp.StatusBadRequest) + return + } + cur := r.PostForm.Get("current_password") + pw := r.PostForm.Get("new_password") + pw2 := r.PostForm.Get("confirm_password") + + full, err := s.deps.Store.GetUserByID(r.Context(), u.ID) + if err != nil { + stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError) + return + } + + render := func(errMsg string, saved bool) { + view := s.baseView(r, u) + view.Title = "Account · restic-manager" + view.Active = "settings" + view.Page = accountPage{ + Username: full.Username, Role: string(full.Role), + MustChange: full.MustChangePassword, + Error: errMsg, Saved: saved, + } + _ = s.deps.UI.Render(w, "account", view) + } + + if pw == "" || pw != pw2 || len(pw) < 12 { + render("Passwords must match and be at least 12 characters.", false) + return + } + if !full.MustChangePassword { + if err := auth.VerifyPassword(full.PasswordHash, cur); err != nil { + render("Current password is incorrect.", false) + return + } + } + hash, err := auth.HashPassword(pw) + if err != nil { + render("Internal error.", false) + return + } + if err := s.deps.Store.SetPasswordHash(r.Context(), u.ID, hash); err != nil { + render("Internal error.", false) + return + } + _ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{ + ID: ulid.Make().String(), UserID: &u.ID, Actor: "user", + Action: "user.password_changed", + TargetKind: ptr("user"), TargetID: &u.ID, + TS: time.Now().UTC(), + }) + full.MustChangePassword = false + render("", true) +} diff --git a/web/templates/pages/account.html b/web/templates/pages/account.html new file mode 100644 index 0000000..5164fc2 --- /dev/null +++ b/web/templates/pages/account.html @@ -0,0 +1,46 @@ +{{define "title"}}Account · restic-manager{{end}} + +{{define "content"}} +{{$page := .Page}} +
+
+ Dashboard/ + account +
+ +

Account

+
+ Signed in as {{$page.Username}} + ({{$page.Role}}). Change your password below. +
+ + {{if $page.Saved}} +
+
Password updated.
+
+ {{end}} + +
+ {{if not $page.MustChange}} +
+ + +
+ {{end}} +
+ + +
+
+ + +
+ {{if $page.Error}}
{{$page.Error}}
{{end}} + +
+
+{{end}}