internal/httpd/adminusers.go

v1.31.0
gitbay/internal/httpd/adminusers.go history · blame · raw

121 lines · 3368 bytes

  1package httpd
  2
  3import (
  4	"net/http"
  5	"net/url"
  6	"strconv"
  7	"strings"
  8
  9	"gitbay.org/gitbay/internal/store"
 10)
 11
 12// adminUserRow is one account as admin user list returns it. The
 13// command owns the shape; this is the page's view of it.
 14type adminUserRow struct {
 15	Username  string `json:"username"`
 16	State     string `json:"state"`
 17	Admin     bool   `json:"admin"`
 18	CreatedAt string `json:"created_at"`
 19	LastSeen  string `json:"last_seen"`
 20}
 21
 22// adminUsersPerPage is how many accounts a page shows before offering
 23// the next, using the command's own keyset cursor so the filter carries
 24// across pages.
 25const adminUsersPerPage = 50
 26
 27// adminUsers is the account list an instance admin manages (#234). It
 28// dispatches admin user list like any other read, which it can because
 29// no command is held back from the web any more. A non-admin gets the
 30// same 404 a missing page would, so the URL confirms nothing.
 31func (s *Server) adminUsers(w http.ResponseWriter, r *http.Request, viewer store.User) {
 32	if !viewer.IsAdmin {
 33		s.notFound(w, r)
 34		return
 35	}
 36	state := r.URL.Query().Get("state")
 37	switch state {
 38	case "active", "pending", "disabled", "admin":
 39	default:
 40		state = "all"
 41	}
 42	argv := []string{"admin", "user", "list", "--limit", strconv.Itoa(adminUsersPerPage)}
 43	if state != "all" {
 44		argv = append(argv, "--state", state)
 45	}
 46	if cursor := r.URL.Query().Get("cursor"); cursor != "" {
 47		argv = append(argv, "--cursor", cursor)
 48	}
 49	var page struct {
 50		Items []adminUserRow `json:"items"`
 51		Next  string         `json:"next"`
 52	}
 53	if msg, ok := s.runControlInto(viewer, argv, &page); !ok {
 54		http.Error(w, msg, http.StatusInternalServerError)
 55		return
 56	}
 57	next := ""
 58	if page.Next != "" {
 59		q := url.Values{"cursor": {page.Next}}
 60		if state != "all" {
 61			q.Set("state", state)
 62		}
 63		next = "?" + q.Encode()
 64	}
 65	s.render(w, "adminusers.html", struct {
 66		basePage
 67		Tab    string
 68		State  string
 69		Users  []adminUserRow
 70		Next   string
 71		Notice string
 72	}{s.baseFor(viewer), "admin", state, page.Items, next, s.takeFlash(w, r)})
 73}
 74
 75// adminUsersSubmit runs one account action. Each is the command an
 76// admin would run over SSH; demote and disable carry the typed-name
 77// check, because both take someone's access away and a mistyped row is
 78// the way that happens by accident. Deletion is not here: it is
 79// permanent, and it stays a typed command.
 80func (s *Server) adminUsersSubmit(w http.ResponseWriter, r *http.Request, viewer store.User) {
 81	if !viewer.IsAdmin {
 82		s.notFound(w, r)
 83		return
 84	}
 85	name := strings.TrimSpace(r.FormValue("user"))
 86	back := func(msg string) {
 87		s.setFlash(w, msg)
 88		dest := "/admin/users"
 89		if state := r.FormValue("state"); state != "" && state != "all" {
 90			dest += "?state=" + url.QueryEscape(state)
 91		}
 92		http.Redirect(w, r, dest, http.StatusSeeOther)
 93	}
 94	var verb string
 95	switch r.FormValue("field") {
 96	case "promote":
 97		verb = "promote"
 98	case "demote":
 99		verb = "demote"
100	case "disable":
101		verb = "disable"
102	case "enable":
103		verb = "enable"
104	default:
105		back("unknown action")
106		return
107	}
108	if verb == "demote" || verb == "disable" {
109		if ok, msg := confirmed(r, name); !ok {
110			back(msg)
111			return
112		}
113	}
114	_, msg, code := s.runControlCode(viewer, []string{"admin", "user", verb, name})
115	s.done(w, r, code, msg, func(w http.ResponseWriter, r *http.Request, m string) {
116		if m == "" {
117			m = name + " " + verb + "d"
118		}
119		back(m)
120	})
121}