Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Add one Admin page with status, accounts, and the audit log

43e681d8a5d94567aa9530d32b2c89223c1a17f9 · Keenan Nemetz · 5d ago · parent af018ce8

Add one Admin page with status, accounts, and the audit log

Instance administration was split between the Status and Accounts pages,
and the audit log had no browser view at all. The new /admin page gathers
the status tabs, the account roster, and a read-only audit viewer into one
place with a tab for each, and /status, /runners, and /users redirect to
the tab they named so old links keep working.

The audit reader walks the file backwards a chunk at a time, returns the
newest records first, and shows one extra record to know whether an older
page exists. It skips a line it cannot parse rather than failing the page
and refuses a symlink or a non-regular file, as startup already does.

Refs #198 (01M6MK0NNZ2XR1D5TQKNWYT4HN).

server/web/admin_users.go +15−15

@@ -20,12 +20,6 @@ type adminUserRow struct {
20 20 Sessions int
21 21 }
22 22
23 type adminUsersView struct {
24 Page
25 Users []adminUserRow
26 Error string
27 }
28
29 23 type adminUserView struct {
30 24 Page
31 25 Account *identity.Account
@@ -45,7 +39,10 @@ type adminInviteView struct {
45 39 BrowserLogin bool
46 40 }
47 41
48 func (w *Web) adminUsersView(r *http.Request, msg string) adminUsersView {
42 // adminUserRows is the roster table on the Accounts tab. The session count is a
43 // read of each account's sessions, so it follows the same rule the account page
44 // does: a failure shows zero rather than failing the roster.
45 func (w *Web) adminUserRows() []adminUserRow {
49 46 accounts := w.Ident.All()
50 47 rows := make([]adminUserRow, 0, len(accounts))
51 48 for _, acct := range accounts {
@@ -57,7 +54,7 @@ func (w *Web) adminUsersView(r *http.Request, msg string) adminUsersView {
57 54 }
58 55 rows = append(rows, row)
59 56 }
60 return adminUsersView{Page: w.page(r, "Accounts"), Users: rows, Error: msg}
57 return rows
61 58 }
62 59
63 60 func (w *Web) adminUserView(r *http.Request, acct *identity.Account, msg string) adminUserView {
@@ -105,11 +102,14 @@ func (w *Web) adminUserPost(rw http.ResponseWriter, r *http.Request) (*identity.
105 102 return acct, pr, true
106 103 }
107 104
105 // AdminUsersPage keeps the old roster URL working. The roster is the Accounts tab
106 // on the Admin page now; the gate still runs here so an anonymous visitor is sent
107 // to login and a non-admin still gets the same 404 a missing page gets.
108 108 func (w *Web) AdminUsersPage(rw http.ResponseWriter, r *http.Request) {
109 109 if _, ok := w.requireInstanceAdmin(rw, r); !ok {
110 110 return
111 111 }
112 w.render(rw, "admin_users", w.adminUsersView(r, ""), 0)
112 http.Redirect(rw, r, "/admin?tab=accounts", http.StatusMovedPermanently)
113 113 }
114 114
115 115 func (w *Web) CreateAdminUser(rw http.ResponseWriter, r *http.Request) {
@@ -119,12 +119,12 @@ func (w *Web) CreateAdminUser(rw http.ResponseWriter, r *http.Request) {
119 119 name := strings.TrimSpace(r.PostFormValue("name"))
120 120 role, err := access.ParseRole(r.PostFormValue("role"))
121 121 if err != nil {
122 w.render(rw, "admin_users", w.adminUsersView(r, err.Error()), http.StatusBadRequest)
122 w.renderAdminTab(rw, r, "accounts", err.Error(), http.StatusBadRequest)
123 123 return
124 124 }
125 125 acct, inv, err := w.Ident.CreateWithInviteRole(name, role, w.accountName(r), 0)
126 126 if err != nil {
127 w.render(rw, "admin_users", w.adminUsersView(r, err.Error()), http.StatusBadRequest)
127 w.renderAdminTab(rw, r, "accounts", err.Error(), http.StatusBadRequest)
128 128 return
129 129 }
130 130 w.noStore(rw)
@@ -143,17 +143,17 @@ func (w *Web) CreateAdminBot(rw http.ResponseWriter, r *http.Request) {
143 143 name := strings.TrimSpace(r.PostFormValue("name"))
144 144 role, err := access.ParseRole(r.PostFormValue("role"))
145 145 if err != nil {
146 w.render(rw, "admin_users", w.adminUsersView(r, err.Error()), http.StatusBadRequest)
146 w.renderAdminTab(rw, r, "accounts", err.Error(), http.StatusBadRequest)
147 147 return
148 148 }
149 149 key := strings.TrimSpace(r.PostFormValue("key"))
150 150 if key == "" {
151 w.render(rw, "admin_users", w.adminUsersView(r, "Paste a public key for the bot. A bot signs in with a key, not a browser."), http.StatusBadRequest)
151 w.renderAdminTab(rw, r, "accounts", "Paste a public key for the bot. A bot signs in with a key, not a browser.", http.StatusBadRequest)
152 152 return
153 153 }
154 154 acct, err := w.Ident.CreateBot(name, role, key, pr.account.Name)
155 155 if err != nil {
156 w.render(rw, "admin_users", w.adminUsersView(r, err.Error()), http.StatusBadRequest)
156 w.renderAdminTab(rw, r, "accounts", err.Error(), http.StatusBadRequest)
157 157 return
158 158 }
159 159 http.Redirect(rw, r, "/users/"+acct.Name, http.StatusSeeOther)
@@ -201,7 +201,7 @@ func (w *Web) RetireAdminUser(rw http.ResponseWriter, r *http.Request) {
201 201 w.render(rw, "admin_user", w.adminUserView(r, acct, err.Error()), http.StatusBadRequest)
202 202 return
203 203 }
204 http.Redirect(rw, r, "/users", http.StatusSeeOther)
204 http.Redirect(rw, r, "/admin?tab=accounts", http.StatusSeeOther)
205 205 }
206 206
207 207 func (w *Web) InviteAdminUser(rw http.ResponseWriter, r *http.Request) {