1package web
2
3import (
4 "context"
5 "errors"
6 "net/http"
7
8 "tug/internal/transport/web/text"
9)
10
11type adminUserRowView struct {
12 ID Ident
13 Username Ident
14 DisplayName string
15 Disabled bool
16 Pending bool
17 Groups []Ident
18 More int
19}
20
21type adminUsersData struct {
22 Viewer Ident
23 Rows []adminUserRowView
24 Pager adminPager
25}
26
27type adminGroupView struct {
28 ID Ident
29 Name Ident
30 Primary bool
31}
32
33type adminUserData struct {
34 Viewer Ident
35 ID Ident
36 Username Ident
37 DisplayName string
38 UIDNumber int
39 Disabled bool
40 Pending bool
41 Admin bool
42 LastSignIn string
43 Groups []adminGroupView
44 More int
45 Google bool
46 GoogleSince string
47 Sessions int
48 RefreshTokens int
49 Activity []adminAuditView
50 SSHKeys sshKeyList
51 Message text.Key
52}
53
54type adminNewUserData struct {
55 Viewer Ident
56 Username string
57 DisplayName string
58 Message text.Key
59}
60
61type adminMembershipsData struct {
62 Viewer Ident
63 ID Ident
64 Username Ident
65 Groups []adminGroupView
66 Choices []Ident
67 Pager adminPager
68 GroupInput string
69 Message text.Key
70}
71
72func groupViews(refs []AdminGroupRef) []adminGroupView {
73 out := make([]adminGroupView, 0, len(refs))
74 for _, g := range refs {
75 out = append(out, adminGroupView{ID: Ident(g.ID), Name: Ident(g.Name), Primary: g.Primary})
76 }
77 return out
78}
79
80func userRowView(u AdminUserRow) adminUserRowView {
81 shown := u.Groups[:min(len(u.Groups), AdminListBadges)]
82 v := adminUserRowView{ID: Ident(u.ID), Username: Ident(u.Username), DisplayName: u.DisplayName, Disabled: u.Disabled, Pending: u.Pending, More: max(u.GroupCount, len(u.Groups)) - len(shown)}
83 for _, g := range shown {
84 v.Groups = append(v.Groups, Ident(g))
85 }
86 return v
87}
88
89func groupChoices(candidates []string) []Ident {
90 if len(candidates) > adminPickerOptions {
91 return nil
92 }
93 out := make([]Ident, 0, len(candidates))
94 for _, g := range candidates {
95 out = append(out, Ident(g))
96 }
97 return out
98}
99
100func userView(viewer Ident, id string, u AdminUser) adminUserData {
101 shown := u.Groups[:min(len(u.Groups), AdminDetailBadges)]
102 d := adminUserData{
103 Viewer: viewer,
104 ID: Ident(id),
105 Username: Ident(u.Username),
106 DisplayName: u.DisplayName,
107 UIDNumber: u.UIDNumber,
108 Disabled: u.Disabled,
109 Pending: u.Pending,
110 Admin: u.Admin,
111 Groups: groupViews(shown),
112 More: max(u.GroupCount, len(u.Groups)) - len(shown),
113 Sessions: u.Sessions,
114 RefreshTokens: u.RefreshTokens,
115 Activity: auditViews(u.Activity),
116 }
117 add := "/users/" + id + SSHKeyNewPath
118 if u.Disabled || u.Pending {
119 add = ""
120 }
121 d.SSHKeys = newSSHKeyList(u.SSHKeys, "/users/"+id+"/ssh-keys/", add)
122 if u.LastLoginAt != nil {
123 d.LastSignIn = stamp(*u.LastLoginAt)
124 }
125 if u.Google != nil {
126 d.Google = true
127 d.GoogleSince = stamp(u.Google.LinkedAt)
128 }
129 return d
130}
131
132func (a *admin) users(w http.ResponseWriter, r *http.Request) {
133 p, ok := a.kit.principal(w, r)
134 if !ok {
135 return
136 }
137 q, page := searchQuery(r), pageNumber(r)
138 list, err := a.b.ListUsers(r.Context(), q, page)
139 if err != nil {
140 a.failed(w, r, "list users", err)
141 return
142 }
143 rows := list.Users[:min(len(list.Users), AdminUsersPerPage)]
144 d := adminUsersData{Viewer: p.Username, Pager: newPager("/users", q, page, AdminUsersPerPage, len(rows), list.Total)}
145 for _, u := range rows {
146 d.Rows = append(d.Rows, userRowView(u))
147 }
148 a.render(w, r, http.StatusOK, "admin-users", d)
149}
150
151func (a *admin) user(w http.ResponseWriter, r *http.Request) {
152 p, ok := a.kit.principal(w, r)
153 if !ok {
154 return
155 }
156 id, ok := a.id(w, r)
157 if !ok {
158 return
159 }
160 u, err := a.b.User(r.Context(), id)
161 if err != nil {
162 a.failed(w, r, "user", err)
163 return
164 }
165 a.render(w, r, http.StatusOK, "admin-user", userView(p.Username, id, u))
166}
167
168func (a *admin) newUser(w http.ResponseWriter, r *http.Request) {
169 if p, ok := a.kit.principal(w, r); ok {
170 a.render(w, r, http.StatusOK, "admin-user-new", adminNewUserData{Viewer: p.Username})
171 }
172}
173
174func (a *admin) createUser(w http.ResponseWriter, r *http.Request) {
175 p, ok := a.kit.principal(w, r)
176 if !ok {
177 return
178 }
179 f := AdminNewUser{Username: r.PostFormValue("username"), DisplayName: r.PostFormValue("display_name")}
180 id, err := a.b.CreateUser(r.Context(), p.UserID, f)
181 if key, ok := adminProblemKey(err); ok {
182 a.render(w, r, http.StatusOK, "admin-user-new", adminNewUserData{Viewer: p.Username, Username: echoed(f.Username, echoUsernameBytes), DisplayName: echoed(f.DisplayName, echoDisplayNameBytes), Message: key})
183 return
184 }
185 if err == nil && !reAdminID.MatchString(id) {
186 err = errors.New("web: the backend returned a malformed user id")
187 }
188 if err != nil {
189 a.failed(w, r, "create user", err)
190 return
191 }
192 a.done(w, r, text.FlashAdminUserCreated, Ident(f.Username), "/users/"+id+"/reset-link")
193}
194
195func (a *admin) loadUser(w http.ResponseWriter, r *http.Request) (Principal, string, AdminUser, bool) {
196 p, ok := a.kit.principal(w, r)
197 if !ok {
198 return p, "", AdminUser{}, false
199 }
200 id, ok := a.id(w, r)
201 if !ok {
202 return p, id, AdminUser{}, false
203 }
204 u, err := a.b.User(r.Context(), id)
205 if err != nil {
206 a.failed(w, r, "user", err)
207 return p, id, u, false
208 }
209 return p, id, u, true
210}
211
212func (a *admin) profile(w http.ResponseWriter, r *http.Request) {
213 if p, id, u, ok := a.loadUser(w, r); ok {
214 a.render(w, r, http.StatusOK, "admin-user-profile", userView(p.Username, id, u))
215 }
216}
217
218func (a *admin) saveProfile(w http.ResponseWriter, r *http.Request) {
219 p, id, u, ok := a.loadUser(w, r)
220 if !ok {
221 return
222 }
223 f := AdminProfile{DisplayName: r.PostFormValue("display_name")}
224 err := a.b.UpdateProfile(r.Context(), p.UserID, id, f)
225 if key, ok := adminProblemKey(err); ok {
226 d := userView(p.Username, id, u)
227 d.DisplayName, d.Message = echoed(f.DisplayName, echoDisplayNameBytes), key
228 a.render(w, r, http.StatusOK, "admin-user-profile", d)
229 return
230 }
231 if err != nil {
232 a.failed(w, r, "save profile", err)
233 return
234 }
235 a.done(w, r, text.FlashAdminProfileSaved, Ident(u.Username), "/users/"+id)
236}
237
238func (a *admin) renderMemberships(w http.ResponseWriter, r *http.Request, p Principal, id string, page int, input string, message text.Key) {
239 m, err := a.b.Memberships(r.Context(), id, page)
240 if err != nil {
241 a.failed(w, r, "memberships", err)
242 return
243 }
244 shown := m.Groups[:min(len(m.Groups), AdminMembersPerPage)]
245 a.render(w, r, http.StatusOK, "admin-user-groups", adminMembershipsData{
246 Viewer: p.Username,
247 ID: Ident(id),
248 Username: Ident(m.Username),
249 Groups: groupViews(shown),
250 Pager: newPager("/users/"+id+"/groups", "", page, AdminMembersPerPage, len(shown), m.Total),
251 Choices: groupChoices(m.Candidates),
252 GroupInput: input,
253 Message: message,
254 })
255}
256
257func (a *admin) memberships(w http.ResponseWriter, r *http.Request) {
258 p, ok := a.kit.principal(w, r)
259 if !ok {
260 return
261 }
262 if id, ok := a.id(w, r); ok {
263 a.renderMemberships(w, r, p, id, pageNumber(r), "", "")
264 }
265}
266
267func (a *admin) changeMembership(w http.ResponseWriter, r *http.Request) {
268 p, ok := a.kit.principal(w, r)
269 if !ok {
270 return
271 }
272 id, ok := a.id(w, r)
273 if !ok {
274 return
275 }
276 var err error
277 group := r.PostFormValue("group")
278 flash := text.FlashAdminJoined
279 switch r.PostFormValue("op") {
280 case "add":
281 err = a.b.AddMembership(r.Context(), p.UserID, id, group)
282 case "remove":
283 gid := r.PostFormValue("group_id")
284 if !reAdminID.MatchString(gid) {
285 a.badOp(w, r)
286 return
287 }
288 group, flash = "", text.FlashAdminLeft
289 err = a.b.RemoveMembership(r.Context(), p.UserID, id, gid)
290 default:
291 a.badOp(w, r)
292 return
293 }
294 if key, ok := adminProblemKey(err); ok {
295 a.renderMemberships(w, r, p, id, 1, echoed(group, echoGroupNameBytes), key)
296 return
297 }
298 if err != nil {
299 a.failed(w, r, "change membership", err)
300 return
301 }
302 a.done(w, r, flash, Ident(group), "/users/"+id+"/groups")
303}
304
305func (a *admin) userConfirm(path string, title, body, button, flash text.Key, safe bool, act func(ctx context.Context, actor, id string) error, after func(id string) string) adminAction {
306 return adminAction{path: path, adminConfirm: adminConfirm{
307 a: a, title: title, body: body, button: button, flash: flash, safe: safe,
308 load: func(r *http.Request, id string) (Ident, error) {
309 u, err := a.b.User(r.Context(), id)
310 return Ident(u.Username), err
311 },
312 act: func(r *http.Request, actor, id string) error { return act(r.Context(), actor, id) },
313 action: func(id string) string { return "/users/" + id + "/" + path },
314 cancel: func(id string) string { return "/users/" + id },
315 after: after,
316 }}
317}
318
319func (a *admin) userActions() []adminAction {
320 detail := func(id string) string { return "/users/" + id }
321 return []adminAction{
322 a.userConfirm("disable", text.AdminDisableTitle, text.AdminDisableBody, text.AdminUserDisable, text.FlashAdminUserDisabled, false, a.b.DisableUser, detail),
323 a.userConfirm("enable", text.AdminEnableTitle, text.AdminEnableBody, text.AdminUserEnable, text.FlashAdminUserEnabled, true, a.b.EnableUser, detail),
324 a.userConfirm("sessions/revoke-all", text.AdminRevokeAllTitle, text.AdminRevokeAllBody, text.AdminUserRevokeAll, text.FlashAdminSignedOut, false, a.b.RevokeAllSessions, detail),
325 a.userConfirm("google/unlink", text.AdminGoogleUnlinkTitle, text.AdminGoogleUnlinkBody, text.AdminUserGoogleUnlink, text.FlashAdminGoogleUnlinked, false, a.b.UnlinkGoogle, detail).withSelf(text.AdminGoogleUnlinkSelfElsewhere),
326 a.userConfirm("delete", text.AdminDeleteUserTitle, text.AdminDeleteUserBody, text.AdminUserDelete, text.FlashAdminUserDeleted, false, a.b.DeleteUser, func(string) string { return "/users" }),
327 a.userConfirm("reject", text.AdminRejectTitle, text.AdminRejectBody, text.AdminRejectButton, text.FlashAdminUserRejected, false, a.b.RejectAccount, func(string) string { return "/users" }),
328 }
329}
330
331func (act adminAction) withSelf(key text.Key) adminAction {
332 act.self = key
333 return act
334}