package adminui import ( "context" "errors" "tug/internal/service/admin" "tug/internal/transport/web" ) type RefreshCounter interface { RefreshTokenCount(ctx context.Context, userID string) (int, error) } type Backend struct { service *admin.Service refresh RefreshCounter } var _ web.AdminBackend = Backend{} func New(service *admin.Service, refresh RefreshCounter) Backend { if service == nil || refresh == nil { panic("adminui: the backend needs the admin service and a refresh-token counter") } return Backend{service: service, refresh: refresh} } var problems = map[admin.Problem]web.AdminProblem{ admin.ProblemUsernameGrammar: web.AdminUsernameGrammar, admin.ProblemUsernameTaken: web.AdminUsernameTaken, admin.ProblemDisplayNameInvalid: web.AdminDisplayNameInvalid, admin.ProblemGroupNameGrammar: web.AdminGroupNameGrammar, admin.ProblemGroupNameTaken: web.AdminGroupNameTaken, admin.ProblemDescriptionInvalid: web.AdminDescriptionInvalid, admin.ProblemUnknownUser: web.AdminUnknownUser, admin.ProblemUnknownGroup: web.AdminUnknownGroup, admin.ProblemAlreadyMember: web.AdminAlreadyMember, admin.ProblemNotMember: web.AdminNotMember, admin.ProblemPrimaryGroup: web.AdminPrimaryGroup, admin.ProblemSelf: web.AdminSelf, admin.ProblemLastAdmin: web.AdminLastAdmin, admin.ProblemReferencedGroup: web.AdminReferencedGroup, admin.ProblemInvitationClosed: web.AdminInvitationClosed, admin.ProblemGoogleNotLinked: web.AdminGoogleNotLinked, admin.ProblemResetLinkDisabled: web.AdminResetLinkDisabled, admin.ProblemResetLinkPending: web.AdminResetLinkPending, admin.ProblemLabelInvalid: web.AdminLabelInvalid, admin.ProblemExpiryInvalid: web.AdminExpiryInvalid, admin.ProblemAdminGroupGrant: web.AdminAdminGroupGrant, admin.ProblemBatchSize: web.AdminBatchSize, admin.ProblemUserPending: web.AdminUserPending, admin.ProblemUnknownBatch: web.AdminUnknownBatch, admin.ProblemSSHKeyDisabled: web.AdminSSHKeyDisabled, admin.ProblemSSHKeyPending: web.AdminSSHKeyPending, } var states = map[admin.InvitationState]web.AdminInvitationState{ admin.InvitationPending: web.AdminInvitationPending, admin.InvitationUsed: web.AdminInvitationUsed, admin.InvitationRevoked: web.AdminInvitationRevoked, admin.InvitationExpired: web.AdminInvitationExpired, } func mapError(err error) error { if err == nil { return nil } if errors.Is(err, admin.ErrNotFound) { return web.ErrAdminNotFound } var p admin.Problem if errors.As(err, &p) { if q, ok := problems[p]; ok { return q } } return err } func audit(entries []admin.AuditEntry) []web.AdminAuditEntry { out := make([]web.AdminAuditEntry, 0, len(entries)) for _, e := range entries { out = append(out, web.AdminAuditEntry(e)) } return out } func groupRefs(refs []admin.GroupRef) []web.AdminGroupRef { out := make([]web.AdminGroupRef, 0, len(refs)) for _, g := range refs { out = append(out, web.AdminGroupRef(g)) } return out } func invitation(i admin.Invitation) (web.AdminInvitation, error) { state, ok := states[i.State] if !ok { return web.AdminInvitation{}, errors.New("adminui: unknown invitation state") } 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} for _, g := range i.Groups { out.Groups = append(out.Groups, web.AdminInvitationGroup{Name: g.Name, Deleted: g.Deleted}) } return out, nil } func (b Backend) Dashboard(ctx context.Context) (web.AdminDashboard, error) { d, err := b.service.Dashboard(ctx) if err != nil { return web.AdminDashboard{}, mapError(err) } return web.AdminDashboard{ Users: d.Users, DisabledUsers: d.DisabledUsers, Groups: d.Groups, PendingInvitations: d.PendingInvitations, LiveSessions: d.LiveSessions, Recent: audit(d.Recent), }, nil } func (b Backend) ListUsers(ctx context.Context, query string, page int) (web.AdminUserList, error) { l, err := b.service.ListUsers(ctx, query, page) if err != nil { return web.AdminUserList{}, mapError(err) } users := make([]web.AdminUserRow, 0, len(l.Users)) for _, u := range l.Users { users = append(users, web.AdminUserRow(u)) } return web.AdminUserList{Users: users, Total: l.Total}, nil } func (b Backend) User(ctx context.Context, id string) (web.AdminUser, error) { u, err := b.service.User(ctx, id) if err != nil { return web.AdminUser{}, mapError(err) } tokens, err := b.refresh.RefreshTokenCount(ctx, u.ID) if err != nil { return web.AdminUser{}, err } out := web.AdminUser{ ID: u.ID, Username: u.Username, DisplayName: u.DisplayName, UIDNumber: u.UIDNumber, Disabled: u.Disabled, Pending: u.Pending, Admin: u.Admin, CreatedAt: u.CreatedAt, LastLoginAt: u.LastLoginAt, Groups: groupRefs(u.Groups), GroupCount: u.GroupCount, Sessions: u.Sessions, RefreshTokens: tokens, Activity: audit(u.Activity), SSHKeys: sshKeys(u.SSHKeys), } if u.Google != nil { out.Google = &web.AdminGoogleLink{LinkedAt: u.Google.LinkedAt} } return out, nil } func (b Backend) CreateUser(ctx context.Context, actor string, u web.AdminNewUser) (string, error) { id, err := b.service.CreateUser(ctx, actor, admin.NewUser{Username: u.Username, DisplayName: u.DisplayName}) return id, mapError(err) } func (b Backend) UpdateProfile(ctx context.Context, actor, id string, p web.AdminProfile) error { return mapError(b.service.UpdateProfile(ctx, actor, id, admin.Profile{DisplayName: p.DisplayName})) } func (b Backend) DisableUser(ctx context.Context, actor, id string) error { return mapError(b.service.DisableUser(ctx, actor, id)) } func (b Backend) EnableUser(ctx context.Context, actor, id string) error { return mapError(b.service.EnableUser(ctx, actor, id)) } func (b Backend) RevokeAllSessions(ctx context.Context, actor, id string) error { return mapError(b.service.RevokeAllSessions(ctx, actor, id)) } func (b Backend) DeleteUser(ctx context.Context, actor, id string) error { return mapError(b.service.DeleteUser(ctx, actor, id)) } func (b Backend) Memberships(ctx context.Context, userID string, page int) (web.AdminMemberships, error) { m, err := b.service.Memberships(ctx, userID, page) if err != nil { return web.AdminMemberships{}, mapError(err) } return web.AdminMemberships{Username: m.Username, Groups: groupRefs(m.Groups), Total: m.Total, Candidates: m.Candidates}, nil } func (b Backend) AddMembership(ctx context.Context, actor, userID, groupName string) error { return mapError(b.service.AddMembership(ctx, actor, userID, groupName)) } func (b Backend) AddMember(ctx context.Context, actor, groupID, username string) error { return mapError(b.service.AddMember(ctx, actor, groupID, username)) } func (b Backend) RemoveMembership(ctx context.Context, actor, userID, groupID string) error { return mapError(b.service.RemoveMembership(ctx, actor, userID, groupID)) } func (b Backend) ListGroups(ctx context.Context, page int) (web.AdminGroupList, error) { l, err := b.service.ListGroups(ctx, page) if err != nil { return web.AdminGroupList{}, mapError(err) } groups := make([]web.AdminGroupRow, 0, len(l.Groups)) for _, g := range l.Groups { groups = append(groups, web.AdminGroupRow(g)) } return web.AdminGroupList{Groups: groups, Total: l.Total}, nil } func (b Backend) Group(ctx context.Context, id string, page int) (web.AdminGroup, error) { g, err := b.service.Group(ctx, id, page) if err != nil { return web.AdminGroup{}, mapError(err) } return web.AdminGroup{ ID: g.ID, Name: g.Name, Description: g.Description, GIDNumber: g.GIDNumber, Primary: g.Primary, Referenced: g.Referenced, Members: adminMembers(g.Members), Total: g.Total, Disabled: adminMembers(g.Disabled), DisabledTotal: g.DisabledTotal, }, nil } func (b Backend) CreateGroup(ctx context.Context, actor string, g web.AdminNewGroup) (string, error) { id, err := b.service.CreateGroup(ctx, actor, admin.NewGroup(g)) return id, mapError(err) } func (b Backend) DescribeGroup(ctx context.Context, actor, id, description string) error { return mapError(b.service.DescribeGroup(ctx, actor, id, description)) } func (b Backend) DeleteGroup(ctx context.Context, actor, id string) error { return mapError(b.service.DeleteGroup(ctx, actor, id)) } func (b Backend) ListInvitations(ctx context.Context, page int) (web.AdminInvitationList, error) { l, err := b.service.ListInvitations(ctx, page) if err != nil { return web.AdminInvitationList{}, mapError(err) } out := make([]web.AdminInvitation, 0, len(l.Invitations)) for _, i := range l.Invitations { v, err := invitation(i) if err != nil { return web.AdminInvitationList{}, err } out = append(out, v) } return web.AdminInvitationList{Invitations: out, Total: l.Total}, nil } func (b Backend) Invitation(ctx context.Context, id string) (web.AdminInvitation, error) { i, err := b.service.Invitation(ctx, id) if err != nil { return web.AdminInvitation{}, mapError(err) } return invitation(i) } func (b Backend) CreateInvitations(ctx context.Context, actor string, n web.AdminNewInvitations) (web.AdminCreatedInvitations, error) { created, err := b.service.CreateInvitations(ctx, actor, admin.NewInvitations{Labels: n.Labels, GroupIDs: n.GroupIDs, GroupNames: n.GroupNames, Expiry: n.Expiry}) if err != nil { return web.AdminCreatedInvitations{}, mapError(err) } out := web.AdminCreatedInvitations{ExpiresAt: created.ExpiresAt, Groups: created.Groups} for _, i := range created.Invitations { out.Invitations = append(out.Invitations, web.AdminIssuedInvitation{Label: i.Label, URL: i.URL}) } return out, nil } func (b Backend) InvitationGroupChoices(ctx context.Context) (web.AdminGroupChoices, error) { c, err := b.service.InvitationGroupChoices(ctx) if err != nil { return web.AdminGroupChoices{}, mapError(err) } out := web.AdminGroupChoices{More: c.More, Defaults: c.Defaults} for _, g := range c.Groups { out.Groups = append(out.Groups, web.AdminGroupChoice{ID: g.ID, Name: g.Name}) } return out, nil } func (b Backend) RevokeInvitation(ctx context.Context, actor, id string) error { return mapError(b.service.RevokeInvitation(ctx, actor, id)) } func (b Backend) RevokeInvitationBatch(ctx context.Context, actor, batch string) error { _, err := b.service.RevokeInvitationBatch(ctx, actor, batch) return mapError(err) } func adminMembers(in []admin.Member) []web.AdminMember { out := make([]web.AdminMember, 0, len(in)) for _, m := range in { out = append(out, web.AdminMember(m)) } return out } func (b Backend) GroupCandidates(ctx context.Context, groupID, query string) ([]web.AdminMember, error) { found, err := b.service.GroupCandidates(ctx, groupID, query) if err != nil { return nil, mapError(err) } return adminMembers(found), nil }