Raw
  1package web
  2
  3import (
  4	"errors"
  5	"net/http"
  6	"strings"
  7
  8	"tug/internal/transport/web/text"
  9)
 10
 11type adminGroupRowView struct {
 12	ID          Ident
 13	Name        Ident
 14	Description string
 15	Members     int
 16}
 17
 18type adminGroupsData struct {
 19	Viewer Ident
 20	Rows   []adminGroupRowView
 21	Pager  adminPager
 22}
 23
 24type adminMemberView struct {
 25	ID          Ident
 26	Username    Ident
 27	DisplayName string
 28	Pending     bool
 29}
 30
 31type adminGroupData struct {
 32	Viewer       Ident
 33	ID           Ident
 34	Name         Ident
 35	Description  string
 36	GIDNumber    int
 37	Primary      bool
 38	Referenced   bool
 39	Members      []adminMemberView
 40	Pager        adminPager
 41	Disabled     []adminMemberView
 42	DisabledMore int
 43	Find         string
 44	Searched     bool
 45	Candidates   []adminMemberView
 46	Edit         bool
 47	Message      text.Key
 48}
 49
 50type adminNewGroupData struct {
 51	Viewer      Ident
 52	Name        string
 53	Description string
 54	Message     text.Key
 55}
 56
 57func (a *admin) groups(w http.ResponseWriter, r *http.Request) {
 58	p, ok := a.kit.principal(w, r)
 59	if !ok {
 60		return
 61	}
 62	page := pageNumber(r)
 63	list, err := a.b.ListGroups(r.Context(), page)
 64	if err != nil {
 65		a.failed(w, r, "list groups", err)
 66		return
 67	}
 68	rows := list.Groups[:min(len(list.Groups), AdminGroupsPerPage)]
 69	d := adminGroupsData{Viewer: p.Username, Pager: newPager("/groups", "", page, AdminGroupsPerPage, len(rows), list.Total)}
 70	for _, g := range rows {
 71		d.Rows = append(d.Rows, adminGroupRowView{ID: Ident(g.ID), Name: Ident(g.Name), Description: g.Description, Members: g.Members})
 72	}
 73	a.render(w, r, http.StatusOK, "admin-groups", d)
 74}
 75
 76func (a *admin) newGroup(w http.ResponseWriter, r *http.Request) {
 77	if p, ok := a.kit.principal(w, r); ok {
 78		a.render(w, r, http.StatusOK, "admin-group-new", adminNewGroupData{Viewer: p.Username})
 79	}
 80}
 81
 82func (a *admin) createGroup(w http.ResponseWriter, r *http.Request) {
 83	p, ok := a.kit.principal(w, r)
 84	if !ok {
 85		return
 86	}
 87	f := AdminNewGroup{Name: r.PostFormValue("name"), Description: r.PostFormValue("description")}
 88	id, err := a.b.CreateGroup(r.Context(), p.UserID, f)
 89	if key, ok := adminProblemKey(err); ok {
 90		a.render(w, r, http.StatusOK, "admin-group-new", adminNewGroupData{Viewer: p.Username, Name: echoed(f.Name, echoGroupNameBytes), Description: echoed(f.Description, echoDescriptionBytes), Message: key})
 91		return
 92	}
 93	if err == nil && !reAdminID.MatchString(id) {
 94		err = errors.New("web: the backend returned a malformed group id")
 95	}
 96	if err != nil {
 97		a.failed(w, r, "create group", err)
 98		return
 99	}
100	a.done(w, r, text.FlashAdminGroupCreated, Ident(f.Name), "/groups/"+id)
101}
102
103func (a *admin) loadGroup(w http.ResponseWriter, r *http.Request, page int) (Principal, adminGroupData, bool) {
104	p, ok := a.kit.principal(w, r)
105	if !ok {
106		return p, adminGroupData{}, false
107	}
108	id, ok := a.id(w, r)
109	if !ok {
110		return p, adminGroupData{}, false
111	}
112	g, err := a.b.Group(r.Context(), id, page)
113	if err != nil {
114		a.failed(w, r, "group", err)
115		return p, adminGroupData{}, false
116	}
117	shown := g.Members[:min(len(g.Members), AdminMembersPerPage)]
118	d := adminGroupData{
119		Viewer:      p.Username,
120		ID:          Ident(id),
121		Name:        Ident(g.Name),
122		Description: g.Description,
123		GIDNumber:   g.GIDNumber,
124		Primary:     g.Primary,
125		Referenced:  g.Referenced,
126		Pager:       newPager("/groups/"+id, "", page, AdminMembersPerPage, len(shown), g.Total),
127	}
128	for _, m := range shown {
129		d.Members = append(d.Members, adminMemberView{ID: Ident(m.ID), Username: Ident(m.Username), DisplayName: m.DisplayName})
130	}
131	off := g.Disabled[:min(len(g.Disabled), AdminDetailBadges)]
132	for _, m := range off {
133		d.Disabled = append(d.Disabled, adminMemberView{ID: Ident(m.ID), Username: Ident(m.Username), DisplayName: m.DisplayName, Pending: m.Pending})
134	}
135	d.DisabledMore = max(g.DisabledTotal, len(g.Disabled)) - len(off)
136	return p, d, true
137}
138
139func findQuery(r *http.Request) string {
140	q := strings.TrimSpace(r.URL.Query().Get("find"))
141	if len(q) > adminFindMax {
142		q = strings.ToValidUTF8(q[:adminFindMax], "")
143	}
144	return q
145}
146
147func (a *admin) search(w http.ResponseWriter, r *http.Request, d *adminGroupData) bool {
148	d.Find = findQuery(r)
149	if d.Find == "" || d.Primary {
150		return true
151	}
152	found, err := a.b.GroupCandidates(r.Context(), string(d.ID), d.Find)
153	if err != nil {
154		a.failed(w, r, "group candidates", err)
155		return false
156	}
157	d.Searched = true
158	for _, m := range found[:min(len(found), AdminCandidatesShown)] {
159		d.Candidates = append(d.Candidates, adminMemberView{ID: Ident(m.ID), Username: Ident(m.Username), DisplayName: m.DisplayName})
160	}
161	return true
162}
163
164func (a *admin) group(w http.ResponseWriter, r *http.Request) {
165	if _, d, ok := a.loadGroup(w, r, pageNumber(r)); ok {
166		a.render(w, r, http.StatusOK, "admin-group", d)
167	}
168}
169
170func (a *admin) description(w http.ResponseWriter, r *http.Request) {
171	if _, d, ok := a.loadGroup(w, r, 1); ok {
172		a.render(w, r, http.StatusOK, "admin-group-description", d)
173	}
174}
175
176func (a *admin) saveDescription(w http.ResponseWriter, r *http.Request) {
177	p, d, ok := a.loadGroup(w, r, 1)
178	if !ok {
179		return
180	}
181	description := r.PostFormValue("description")
182	err := a.b.DescribeGroup(r.Context(), p.UserID, string(d.ID), description)
183	if key, ok := adminProblemKey(err); ok {
184		d.Description, d.Message = echoed(description, echoDescriptionBytes), key
185		a.render(w, r, http.StatusOK, "admin-group-description", d)
186		return
187	}
188	if err != nil {
189		a.failed(w, r, "save description", err)
190		return
191	}
192	a.done(w, r, text.FlashAdminDescriptionSaved, d.Name, "/groups/"+string(d.ID))
193}
194
195func (a *admin) members(w http.ResponseWriter, r *http.Request) {
196	if _, d, ok := a.loadGroup(w, r, pageNumber(r)); ok {
197		d.Pager.Action += "/members"
198		d.Edit = true
199		if !a.search(w, r, &d) {
200			return
201		}
202		a.render(w, r, http.StatusOK, "admin-group-members", d)
203	}
204}
205
206func (a *admin) changeMember(w http.ResponseWriter, r *http.Request) {
207	p, d, ok := a.loadGroup(w, r, 1)
208	if !ok {
209		return
210	}
211	gid := string(d.ID)
212	var err error
213	username := r.PostFormValue("username")
214	flash := text.FlashAdminMemberAdded
215	switch r.PostFormValue("op") {
216	case "add":
217		err = a.b.AddMember(r.Context(), p.UserID, gid, username)
218	case "remove":
219		uid := r.PostFormValue("user_id")
220		if !reAdminID.MatchString(uid) {
221			a.badOp(w, r)
222			return
223		}
224		username, flash = "", text.FlashAdminMemberRemoved
225		err = a.b.RemoveMembership(r.Context(), p.UserID, uid, gid)
226	default:
227		a.badOp(w, r)
228		return
229	}
230	if key, ok := adminProblemKey(err); ok {
231		d.Find, d.Message = echoed(username, echoUsernameBytes), key
232		d.Pager.Action += "/members"
233		d.Edit = true
234		a.render(w, r, http.StatusOK, "admin-group-members", d)
235		return
236	}
237	if err != nil {
238		a.failed(w, r, "change member", err)
239		return
240	}
241	a.done(w, r, flash, Ident(username), "/groups/"+gid+"/members")
242}
243
244func (a *admin) groupDelete() adminConfirm {
245	return adminConfirm{
246		a: a, title: text.AdminGroupDeleteTitle, body: text.AdminGroupDeleteBody, button: text.AdminGroupDelete, flash: text.FlashAdminGroupDeleted,
247		load: func(r *http.Request, id string) (Ident, error) {
248			g, err := a.b.Group(r.Context(), id, 1)
249			return Ident(g.Name), err
250		},
251		act:    func(r *http.Request, actor, id string) error { return a.b.DeleteGroup(r.Context(), actor, id) },
252		action: func(id string) string { return "/groups/" + id + "/delete" },
253		cancel: func(id string) string { return "/groups/" + id },
254		after:  func(string) string { return "/groups" },
255	}
256}