package web import ( "errors" "net/http" "strings" "tug/internal/transport/web/text" ) type adminGroupRowView struct { ID Ident Name Ident Description string Members int } type adminGroupsData struct { Viewer Ident Rows []adminGroupRowView Pager adminPager } type adminMemberView struct { ID Ident Username Ident DisplayName string Pending bool } type adminGroupData struct { Viewer Ident ID Ident Name Ident Description string GIDNumber int Primary bool Referenced bool Members []adminMemberView Pager adminPager Disabled []adminMemberView DisabledMore int Find string Searched bool Candidates []adminMemberView Edit bool Message text.Key } type adminNewGroupData struct { Viewer Ident Name string Description string Message text.Key } func (a *admin) groups(w http.ResponseWriter, r *http.Request) { p, ok := a.kit.principal(w, r) if !ok { return } page := pageNumber(r) list, err := a.b.ListGroups(r.Context(), page) if err != nil { a.failed(w, r, "list groups", err) return } rows := list.Groups[:min(len(list.Groups), AdminGroupsPerPage)] d := adminGroupsData{Viewer: p.Username, Pager: newPager("/groups", "", page, AdminGroupsPerPage, len(rows), list.Total)} for _, g := range rows { d.Rows = append(d.Rows, adminGroupRowView{ID: Ident(g.ID), Name: Ident(g.Name), Description: g.Description, Members: g.Members}) } a.render(w, r, http.StatusOK, "admin-groups", d) } func (a *admin) newGroup(w http.ResponseWriter, r *http.Request) { if p, ok := a.kit.principal(w, r); ok { a.render(w, r, http.StatusOK, "admin-group-new", adminNewGroupData{Viewer: p.Username}) } } func (a *admin) createGroup(w http.ResponseWriter, r *http.Request) { p, ok := a.kit.principal(w, r) if !ok { return } f := AdminNewGroup{Name: r.PostFormValue("name"), Description: r.PostFormValue("description")} id, err := a.b.CreateGroup(r.Context(), p.UserID, f) if key, ok := adminProblemKey(err); ok { 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}) return } if err == nil && !reAdminID.MatchString(id) { err = errors.New("web: the backend returned a malformed group id") } if err != nil { a.failed(w, r, "create group", err) return } a.done(w, r, text.FlashAdminGroupCreated, Ident(f.Name), "/groups/"+id) } func (a *admin) loadGroup(w http.ResponseWriter, r *http.Request, page int) (Principal, adminGroupData, bool) { p, ok := a.kit.principal(w, r) if !ok { return p, adminGroupData{}, false } id, ok := a.id(w, r) if !ok { return p, adminGroupData{}, false } g, err := a.b.Group(r.Context(), id, page) if err != nil { a.failed(w, r, "group", err) return p, adminGroupData{}, false } shown := g.Members[:min(len(g.Members), AdminMembersPerPage)] d := adminGroupData{ Viewer: p.Username, ID: Ident(id), Name: Ident(g.Name), Description: g.Description, GIDNumber: g.GIDNumber, Primary: g.Primary, Referenced: g.Referenced, Pager: newPager("/groups/"+id, "", page, AdminMembersPerPage, len(shown), g.Total), } for _, m := range shown { d.Members = append(d.Members, adminMemberView{ID: Ident(m.ID), Username: Ident(m.Username), DisplayName: m.DisplayName}) } off := g.Disabled[:min(len(g.Disabled), AdminDetailBadges)] for _, m := range off { d.Disabled = append(d.Disabled, adminMemberView{ID: Ident(m.ID), Username: Ident(m.Username), DisplayName: m.DisplayName, Pending: m.Pending}) } d.DisabledMore = max(g.DisabledTotal, len(g.Disabled)) - len(off) return p, d, true } func findQuery(r *http.Request) string { q := strings.TrimSpace(r.URL.Query().Get("find")) if len(q) > adminFindMax { q = strings.ToValidUTF8(q[:adminFindMax], "") } return q } func (a *admin) search(w http.ResponseWriter, r *http.Request, d *adminGroupData) bool { d.Find = findQuery(r) if d.Find == "" || d.Primary { return true } found, err := a.b.GroupCandidates(r.Context(), string(d.ID), d.Find) if err != nil { a.failed(w, r, "group candidates", err) return false } d.Searched = true for _, m := range found[:min(len(found), AdminCandidatesShown)] { d.Candidates = append(d.Candidates, adminMemberView{ID: Ident(m.ID), Username: Ident(m.Username), DisplayName: m.DisplayName}) } return true } func (a *admin) group(w http.ResponseWriter, r *http.Request) { if _, d, ok := a.loadGroup(w, r, pageNumber(r)); ok { a.render(w, r, http.StatusOK, "admin-group", d) } } func (a *admin) description(w http.ResponseWriter, r *http.Request) { if _, d, ok := a.loadGroup(w, r, 1); ok { a.render(w, r, http.StatusOK, "admin-group-description", d) } } func (a *admin) saveDescription(w http.ResponseWriter, r *http.Request) { p, d, ok := a.loadGroup(w, r, 1) if !ok { return } description := r.PostFormValue("description") err := a.b.DescribeGroup(r.Context(), p.UserID, string(d.ID), description) if key, ok := adminProblemKey(err); ok { d.Description, d.Message = echoed(description, echoDescriptionBytes), key a.render(w, r, http.StatusOK, "admin-group-description", d) return } if err != nil { a.failed(w, r, "save description", err) return } a.done(w, r, text.FlashAdminDescriptionSaved, d.Name, "/groups/"+string(d.ID)) } func (a *admin) members(w http.ResponseWriter, r *http.Request) { if _, d, ok := a.loadGroup(w, r, pageNumber(r)); ok { d.Pager.Action += "/members" d.Edit = true if !a.search(w, r, &d) { return } a.render(w, r, http.StatusOK, "admin-group-members", d) } } func (a *admin) changeMember(w http.ResponseWriter, r *http.Request) { p, d, ok := a.loadGroup(w, r, 1) if !ok { return } gid := string(d.ID) var err error username := r.PostFormValue("username") flash := text.FlashAdminMemberAdded switch r.PostFormValue("op") { case "add": err = a.b.AddMember(r.Context(), p.UserID, gid, username) case "remove": uid := r.PostFormValue("user_id") if !reAdminID.MatchString(uid) { a.badOp(w, r) return } username, flash = "", text.FlashAdminMemberRemoved err = a.b.RemoveMembership(r.Context(), p.UserID, uid, gid) default: a.badOp(w, r) return } if key, ok := adminProblemKey(err); ok { d.Find, d.Message = echoed(username, echoUsernameBytes), key d.Pager.Action += "/members" d.Edit = true a.render(w, r, http.StatusOK, "admin-group-members", d) return } if err != nil { a.failed(w, r, "change member", err) return } a.done(w, r, flash, Ident(username), "/groups/"+gid+"/members") } func (a *admin) groupDelete() adminConfirm { return adminConfirm{ a: a, title: text.AdminGroupDeleteTitle, body: text.AdminGroupDeleteBody, button: text.AdminGroupDelete, flash: text.FlashAdminGroupDeleted, load: func(r *http.Request, id string) (Ident, error) { g, err := a.b.Group(r.Context(), id, 1) return Ident(g.Name), err }, act: func(r *http.Request, actor, id string) error { return a.b.DeleteGroup(r.Context(), actor, id) }, action: func(id string) string { return "/groups/" + id + "/delete" }, cancel: func(id string) string { return "/groups/" + id }, after: func(string) string { return "/groups" }, } }