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}