Files
MeshTender/internal/web/admin.go
T
2026-06-19 10:18:33 -04:00

139 lines
4.0 KiB
Go

package web
import (
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/jleight/meshtender/internal/store"
)
// requireCap is middleware that allows the request only if the current user
// satisfies pick; otherwise it 404s (without revealing the admin area exists).
func (s *Server) requireCap(pick func(*store.User) bool) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u, err := s.store.GetUserByID(r.Context(), s.auth.CurrentUserID(r.Context()))
if err != nil || !pick(u) {
http.NotFound(w, r)
return
}
next.ServeHTTP(w, r)
})
}
}
func capAny(u *store.User) bool { return u.CapManageUsers || u.CapManageCatalog }
func capCatalog(u *store.User) bool { return u.CapManageCatalog }
func capUsers(u *store.User) bool { return u.CapManageUsers }
// catalogGroup buckets the catalog by category for the admin catalog page.
type catalogGroup struct {
Name string
Commands []*store.Command
}
func groupCatalog(catalog []*store.Command) []catalogGroup {
var groups []catalogGroup
idx := map[string]int{}
for _, c := range catalog {
gi, ok := idx[c.Category]
if !ok {
gi = len(groups)
idx[c.Category] = gi
groups = append(groups, catalogGroup{Name: c.Category})
}
groups[gi].Commands = append(groups[gi].Commands, c)
}
return groups
}
func (s *Server) pageAdmin(w http.ResponseWriter, r *http.Request) {
u, _ := s.store.GetUserByID(r.Context(), s.auth.CurrentUserID(r.Context()))
s.render(w, r, "admin.html", map[string]any{
"CanCatalog": u.CapManageCatalog,
"CanUsers": u.CapManageUsers,
})
}
func (s *Server) pageCatalog(w http.ResponseWriter, r *http.Request) {
catalog, err := s.store.ListCommands(r.Context())
if err != nil {
http.Error(w, "could not load catalog", http.StatusInternalServerError)
return
}
s.render(w, r, "admin_catalog.html", map[string]any{
"Groups": groupCatalog(catalog),
"Saved": r.URL.Query().Get("saved") != "",
})
}
func (s *Server) handleUpdateCommand(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
http.NotFound(w, r)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "bad form", http.StatusBadRequest)
return
}
on := func(name string) bool { return r.FormValue(name) != "" }
if err := s.store.UpdateCommandFlags(r.Context(), id,
on("risky"), on("share"), on("org_member"), on("org_admin")); err != nil {
http.Error(w, "could not save", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/admin/catalog?saved=1", http.StatusSeeOther)
}
func (s *Server) pageUsers(w http.ResponseWriter, r *http.Request) {
users, err := s.store.ListUsers(r.Context())
if err != nil {
http.Error(w, "could not load users", http.StatusInternalServerError)
return
}
s.render(w, r, "admin_users.html", map[string]any{
"Users": users,
"Self": s.auth.CurrentUserID(r.Context()),
"Error": r.URL.Query().Get("error"),
})
}
func (s *Server) handleSetUserCaps(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
http.NotFound(w, r)
return
}
manageUsers := r.FormValue("manage_users") != ""
manageCatalog := r.FormValue("manage_catalog") != ""
// Guard against removing the last user-manager.
if !manageUsers {
target, err := s.store.GetUserByID(r.Context(), id)
if err != nil {
http.NotFound(w, r)
return
}
if target.CapManageUsers {
n, err := s.store.CountManageUsers(r.Context())
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
if n <= 1 {
http.Redirect(w, r, "/admin/users?error="+
"Can%27t+remove+the+last+user+manager.", http.StatusSeeOther)
return
}
}
}
if err := s.store.SetCapabilities(r.Context(), id, manageUsers, manageCatalog); err != nil {
http.Error(w, "could not save", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
}