internal/httpd/adminusers.go
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}