internal/httpd/adminusers.go

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

123 lines · 3516 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	notice := s.takeFlash(w, r)
 66	s.render(w, "adminusers.html", struct {
 67		basePage
 68		Tab    string
 69		State  string
 70		Users  []adminUserRow
 71		Next   string
 72		Notice string
 73		Reauth bool // Notice is the stale-session refusal: link to sign in
 74	}{s.baseFor(viewer), "admin", state, page.Items, next, notice, s.reauthNotice(w, notice, r.URL.Path)})
 75}
 76
 77// adminUsersSubmit runs one account action. Each is the command an
 78// admin would run over SSH; demote and disable carry the typed-name
 79// check, because both take someone's access away and a mistyped row is
 80// the way that happens by accident. Deletion is not here: it is
 81// permanent, and it stays a typed command.
 82func (s *Server) adminUsersSubmit(w http.ResponseWriter, r *http.Request, viewer store.User) {
 83	if !viewer.IsAdmin {
 84		s.notFound(w, r)
 85		return
 86	}
 87	name := strings.TrimSpace(r.FormValue("user"))
 88	back := func(msg string) {
 89		s.setFlash(w, msg)
 90		dest := "/admin/users"
 91		if state := r.FormValue("state"); state != "" && state != "all" {
 92			dest += "?state=" + url.QueryEscape(state)
 93		}
 94		http.Redirect(w, r, dest, http.StatusSeeOther)
 95	}
 96	var verb string
 97	switch r.FormValue("field") {
 98	case "promote":
 99		verb = "promote"
100	case "demote":
101		verb = "demote"
102	case "disable":
103		verb = "disable"
104	case "enable":
105		verb = "enable"
106	default:
107		back("unknown action")
108		return
109	}
110	if verb == "promote" || verb == "demote" || verb == "disable" {
111		if ok, msg := confirmed(r, name); !ok {
112			back(msg)
113			return
114		}
115	}
116	_, msg, code := s.runControlCode(viewer, []string{"admin", "user", verb, name})
117	s.done(w, r, code, msg, func(w http.ResponseWriter, r *http.Request, m string) {
118		if m == "" {
119			m = name + " " + verb + "d"
120		}
121		back(m)
122	})
123}