Raw
  1package adminui
  2
  3import (
  4	"context"
  5	"errors"
  6
  7	"tug/internal/service/admin"
  8	"tug/internal/transport/web"
  9)
 10
 11type RefreshCounter interface {
 12	RefreshTokenCount(ctx context.Context, userID string) (int, error)
 13}
 14
 15type Backend struct {
 16	service *admin.Service
 17	refresh RefreshCounter
 18}
 19
 20var _ web.AdminBackend = Backend{}
 21
 22func New(service *admin.Service, refresh RefreshCounter) Backend {
 23	if service == nil || refresh == nil {
 24		panic("adminui: the backend needs the admin service and a refresh-token counter")
 25	}
 26	return Backend{service: service, refresh: refresh}
 27}
 28
 29var problems = map[admin.Problem]web.AdminProblem{
 30	admin.ProblemUsernameGrammar:    web.AdminUsernameGrammar,
 31	admin.ProblemUsernameTaken:      web.AdminUsernameTaken,
 32	admin.ProblemDisplayNameInvalid: web.AdminDisplayNameInvalid,
 33	admin.ProblemGroupNameGrammar:   web.AdminGroupNameGrammar,
 34	admin.ProblemGroupNameTaken:     web.AdminGroupNameTaken,
 35	admin.ProblemDescriptionInvalid: web.AdminDescriptionInvalid,
 36	admin.ProblemUnknownUser:        web.AdminUnknownUser,
 37	admin.ProblemUnknownGroup:       web.AdminUnknownGroup,
 38	admin.ProblemAlreadyMember:      web.AdminAlreadyMember,
 39	admin.ProblemNotMember:          web.AdminNotMember,
 40	admin.ProblemPrimaryGroup:       web.AdminPrimaryGroup,
 41	admin.ProblemSelf:               web.AdminSelf,
 42	admin.ProblemLastAdmin:          web.AdminLastAdmin,
 43	admin.ProblemReferencedGroup:    web.AdminReferencedGroup,
 44	admin.ProblemInvitationClosed:   web.AdminInvitationClosed,
 45	admin.ProblemGoogleNotLinked:    web.AdminGoogleNotLinked,
 46	admin.ProblemResetLinkDisabled:  web.AdminResetLinkDisabled,
 47	admin.ProblemResetLinkPending:   web.AdminResetLinkPending,
 48	admin.ProblemLabelInvalid:       web.AdminLabelInvalid,
 49	admin.ProblemExpiryInvalid:      web.AdminExpiryInvalid,
 50	admin.ProblemAdminGroupGrant:    web.AdminAdminGroupGrant,
 51	admin.ProblemBatchSize:          web.AdminBatchSize,
 52	admin.ProblemUserPending:        web.AdminUserPending,
 53	admin.ProblemUnknownBatch:       web.AdminUnknownBatch,
 54	admin.ProblemSSHKeyDisabled:     web.AdminSSHKeyDisabled,
 55	admin.ProblemSSHKeyPending:      web.AdminSSHKeyPending,
 56}
 57
 58var states = map[admin.InvitationState]web.AdminInvitationState{
 59	admin.InvitationPending: web.AdminInvitationPending,
 60	admin.InvitationUsed:    web.AdminInvitationUsed,
 61	admin.InvitationRevoked: web.AdminInvitationRevoked,
 62	admin.InvitationExpired: web.AdminInvitationExpired,
 63}
 64
 65func mapError(err error) error {
 66	if err == nil {
 67		return nil
 68	}
 69	if errors.Is(err, admin.ErrNotFound) {
 70		return web.ErrAdminNotFound
 71	}
 72	var p admin.Problem
 73	if errors.As(err, &p) {
 74		if q, ok := problems[p]; ok {
 75			return q
 76		}
 77	}
 78	return err
 79}
 80
 81func audit(entries []admin.AuditEntry) []web.AdminAuditEntry {
 82	out := make([]web.AdminAuditEntry, 0, len(entries))
 83	for _, e := range entries {
 84		out = append(out, web.AdminAuditEntry(e))
 85	}
 86	return out
 87}
 88
 89func groupRefs(refs []admin.GroupRef) []web.AdminGroupRef {
 90	out := make([]web.AdminGroupRef, 0, len(refs))
 91	for _, g := range refs {
 92		out = append(out, web.AdminGroupRef(g))
 93	}
 94	return out
 95}
 96
 97func invitation(i admin.Invitation) (web.AdminInvitation, error) {
 98	state, ok := states[i.State]
 99	if !ok {
100		return web.AdminInvitation{}, errors.New("adminui: unknown invitation state")
101	}
102	out := web.AdminInvitation{ID: i.ID, CreatedBy: i.CreatedBy, CreatedAt: i.CreatedAt, ExpiresAt: i.ExpiresAt, State: state, Label: i.Label, UsedBy: i.UsedBy, UsedByID: i.UsedByID, UsedByPending: i.UsedByPending, UsedByDeleted: i.State == admin.InvitationUsed && i.UsedBy == "", Batch: i.Batch}
103	for _, g := range i.Groups {
104		out.Groups = append(out.Groups, web.AdminInvitationGroup{Name: g.Name, Deleted: g.Deleted})
105	}
106	return out, nil
107}
108
109func (b Backend) Dashboard(ctx context.Context) (web.AdminDashboard, error) {
110	d, err := b.service.Dashboard(ctx)
111	if err != nil {
112		return web.AdminDashboard{}, mapError(err)
113	}
114	return web.AdminDashboard{
115		Users:              d.Users,
116		DisabledUsers:      d.DisabledUsers,
117		Groups:             d.Groups,
118		PendingInvitations: d.PendingInvitations,
119		LiveSessions:       d.LiveSessions,
120		Recent:             audit(d.Recent),
121	}, nil
122}
123
124func (b Backend) ListUsers(ctx context.Context, query string, page int) (web.AdminUserList, error) {
125	l, err := b.service.ListUsers(ctx, query, page)
126	if err != nil {
127		return web.AdminUserList{}, mapError(err)
128	}
129	users := make([]web.AdminUserRow, 0, len(l.Users))
130	for _, u := range l.Users {
131		users = append(users, web.AdminUserRow(u))
132	}
133	return web.AdminUserList{Users: users, Total: l.Total}, nil
134}
135
136func (b Backend) User(ctx context.Context, id string) (web.AdminUser, error) {
137	u, err := b.service.User(ctx, id)
138	if err != nil {
139		return web.AdminUser{}, mapError(err)
140	}
141	tokens, err := b.refresh.RefreshTokenCount(ctx, u.ID)
142	if err != nil {
143		return web.AdminUser{}, err
144	}
145	out := web.AdminUser{
146		ID:            u.ID,
147		Username:      u.Username,
148		DisplayName:   u.DisplayName,
149		UIDNumber:     u.UIDNumber,
150		Disabled:      u.Disabled,
151		Pending:       u.Pending,
152		Admin:         u.Admin,
153		CreatedAt:     u.CreatedAt,
154		LastLoginAt:   u.LastLoginAt,
155		Groups:        groupRefs(u.Groups),
156		GroupCount:    u.GroupCount,
157		Sessions:      u.Sessions,
158		RefreshTokens: tokens,
159		Activity:      audit(u.Activity),
160		SSHKeys:       sshKeys(u.SSHKeys),
161	}
162	if u.Google != nil {
163		out.Google = &web.AdminGoogleLink{LinkedAt: u.Google.LinkedAt}
164	}
165	return out, nil
166}
167
168func (b Backend) CreateUser(ctx context.Context, actor string, u web.AdminNewUser) (string, error) {
169	id, err := b.service.CreateUser(ctx, actor, admin.NewUser{Username: u.Username, DisplayName: u.DisplayName})
170	return id, mapError(err)
171}
172
173func (b Backend) UpdateProfile(ctx context.Context, actor, id string, p web.AdminProfile) error {
174	return mapError(b.service.UpdateProfile(ctx, actor, id, admin.Profile{DisplayName: p.DisplayName}))
175}
176
177func (b Backend) DisableUser(ctx context.Context, actor, id string) error {
178	return mapError(b.service.DisableUser(ctx, actor, id))
179}
180
181func (b Backend) EnableUser(ctx context.Context, actor, id string) error {
182	return mapError(b.service.EnableUser(ctx, actor, id))
183}
184
185func (b Backend) RevokeAllSessions(ctx context.Context, actor, id string) error {
186	return mapError(b.service.RevokeAllSessions(ctx, actor, id))
187}
188
189func (b Backend) DeleteUser(ctx context.Context, actor, id string) error {
190	return mapError(b.service.DeleteUser(ctx, actor, id))
191}
192
193func (b Backend) Memberships(ctx context.Context, userID string, page int) (web.AdminMemberships, error) {
194	m, err := b.service.Memberships(ctx, userID, page)
195	if err != nil {
196		return web.AdminMemberships{}, mapError(err)
197	}
198	return web.AdminMemberships{Username: m.Username, Groups: groupRefs(m.Groups), Total: m.Total, Candidates: m.Candidates}, nil
199}
200
201func (b Backend) AddMembership(ctx context.Context, actor, userID, groupName string) error {
202	return mapError(b.service.AddMembership(ctx, actor, userID, groupName))
203}
204
205func (b Backend) AddMember(ctx context.Context, actor, groupID, username string) error {
206	return mapError(b.service.AddMember(ctx, actor, groupID, username))
207}
208
209func (b Backend) RemoveMembership(ctx context.Context, actor, userID, groupID string) error {
210	return mapError(b.service.RemoveMembership(ctx, actor, userID, groupID))
211}
212
213func (b Backend) ListGroups(ctx context.Context, page int) (web.AdminGroupList, error) {
214	l, err := b.service.ListGroups(ctx, page)
215	if err != nil {
216		return web.AdminGroupList{}, mapError(err)
217	}
218	groups := make([]web.AdminGroupRow, 0, len(l.Groups))
219	for _, g := range l.Groups {
220		groups = append(groups, web.AdminGroupRow(g))
221	}
222	return web.AdminGroupList{Groups: groups, Total: l.Total}, nil
223}
224
225func (b Backend) Group(ctx context.Context, id string, page int) (web.AdminGroup, error) {
226	g, err := b.service.Group(ctx, id, page)
227	if err != nil {
228		return web.AdminGroup{}, mapError(err)
229	}
230	return web.AdminGroup{
231		ID:            g.ID,
232		Name:          g.Name,
233		Description:   g.Description,
234		GIDNumber:     g.GIDNumber,
235		Primary:       g.Primary,
236		Referenced:    g.Referenced,
237		Members:       adminMembers(g.Members),
238		Total:         g.Total,
239		Disabled:      adminMembers(g.Disabled),
240		DisabledTotal: g.DisabledTotal,
241	}, nil
242}
243
244func (b Backend) CreateGroup(ctx context.Context, actor string, g web.AdminNewGroup) (string, error) {
245	id, err := b.service.CreateGroup(ctx, actor, admin.NewGroup(g))
246	return id, mapError(err)
247}
248
249func (b Backend) DescribeGroup(ctx context.Context, actor, id, description string) error {
250	return mapError(b.service.DescribeGroup(ctx, actor, id, description))
251}
252
253func (b Backend) DeleteGroup(ctx context.Context, actor, id string) error {
254	return mapError(b.service.DeleteGroup(ctx, actor, id))
255}
256
257func (b Backend) ListInvitations(ctx context.Context, page int) (web.AdminInvitationList, error) {
258	l, err := b.service.ListInvitations(ctx, page)
259	if err != nil {
260		return web.AdminInvitationList{}, mapError(err)
261	}
262	out := make([]web.AdminInvitation, 0, len(l.Invitations))
263	for _, i := range l.Invitations {
264		v, err := invitation(i)
265		if err != nil {
266			return web.AdminInvitationList{}, err
267		}
268		out = append(out, v)
269	}
270	return web.AdminInvitationList{Invitations: out, Total: l.Total}, nil
271}
272
273func (b Backend) Invitation(ctx context.Context, id string) (web.AdminInvitation, error) {
274	i, err := b.service.Invitation(ctx, id)
275	if err != nil {
276		return web.AdminInvitation{}, mapError(err)
277	}
278	return invitation(i)
279}
280
281func (b Backend) CreateInvitations(ctx context.Context, actor string, n web.AdminNewInvitations) (web.AdminCreatedInvitations, error) {
282	created, err := b.service.CreateInvitations(ctx, actor, admin.NewInvitations{Labels: n.Labels, GroupIDs: n.GroupIDs, GroupNames: n.GroupNames, Expiry: n.Expiry})
283	if err != nil {
284		return web.AdminCreatedInvitations{}, mapError(err)
285	}
286	out := web.AdminCreatedInvitations{ExpiresAt: created.ExpiresAt, Groups: created.Groups}
287	for _, i := range created.Invitations {
288		out.Invitations = append(out.Invitations, web.AdminIssuedInvitation{Label: i.Label, URL: i.URL})
289	}
290	return out, nil
291}
292
293func (b Backend) InvitationGroupChoices(ctx context.Context) (web.AdminGroupChoices, error) {
294	c, err := b.service.InvitationGroupChoices(ctx)
295	if err != nil {
296		return web.AdminGroupChoices{}, mapError(err)
297	}
298	out := web.AdminGroupChoices{More: c.More, Defaults: c.Defaults}
299	for _, g := range c.Groups {
300		out.Groups = append(out.Groups, web.AdminGroupChoice{ID: g.ID, Name: g.Name})
301	}
302	return out, nil
303}
304
305func (b Backend) RevokeInvitation(ctx context.Context, actor, id string) error {
306	return mapError(b.service.RevokeInvitation(ctx, actor, id))
307}
308
309func (b Backend) RevokeInvitationBatch(ctx context.Context, actor, batch string) error {
310	_, err := b.service.RevokeInvitationBatch(ctx, actor, batch)
311	return mapError(err)
312}
313
314func adminMembers(in []admin.Member) []web.AdminMember {
315	out := make([]web.AdminMember, 0, len(in))
316	for _, m := range in {
317		out = append(out, web.AdminMember(m))
318	}
319	return out
320}
321
322func (b Backend) GroupCandidates(ctx context.Context, groupID, query string) ([]web.AdminMember, error) {
323	found, err := b.service.GroupCandidates(ctx, groupID, query)
324	if err != nil {
325		return nil, mapError(err)
326	}
327	return adminMembers(found), nil
328}