Raw
  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}