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) }