Raw
  1package admin
  2
  3import (
  4	"cmp"
  5	"context"
  6	"errors"
  7	"slices"
  8
  9	"tug/internal/storage"
 10)
 11
 12func (s *Service) Dashboard(ctx context.Context) (Dashboard, error) {
 13	counts, err := s.store.Dashboard(ctx, s.now())
 14	if err != nil {
 15		return Dashboard{}, failed("read dashboard counts", err)
 16	}
 17	events, err := s.store.ListAudit(ctx, storage.AuditFilter{Limit: foldWindow})
 18	if err != nil {
 19		return Dashboard{}, failed("read recent audit", err)
 20	}
 21	recent, err := s.resolver(ctx).recent(events)
 22	if err != nil {
 23		return Dashboard{}, err
 24	}
 25	return Dashboard{
 26		Users:              counts.Users,
 27		DisabledUsers:      counts.DisabledUsers,
 28		Groups:             counts.Groups,
 29		PendingInvitations: counts.PendingInvitations,
 30		LiveSessions:       counts.LiveSessions,
 31		Recent:             recent,
 32	}, nil
 33}
 34
 35func (s *Service) activity(ctx context.Context, userID string) ([]AuditEntry, error) {
 36	byActor, err := s.store.ListAudit(ctx, storage.AuditFilter{Actor: userID, Limit: foldWindow})
 37	if err != nil {
 38		return nil, failed("read user's activity", err)
 39	}
 40	bySubject, err := s.store.ListAudit(ctx, storage.AuditFilter{Subject: userID, Limit: foldWindow})
 41	if err != nil {
 42		return nil, failed("read user's activity", err)
 43	}
 44	events := append(byActor, bySubject...)
 45	slices.SortFunc(events, func(a, b storage.AuditEvent) int {
 46		if c := b.OccurredAt.Compare(a.OccurredAt); c != 0 {
 47			return c
 48		}
 49		return cmp.Compare(b.ID, a.ID)
 50	})
 51	events = slices.CompactFunc(events, func(a, b storage.AuditEvent) bool { return a.ID == b.ID })
 52	return s.resolver(ctx).recent(events)
 53}
 54
 55type resolver struct {
 56	ctx    context.Context
 57	store  *storage.Store
 58	users  map[string]string
 59	groups map[string]string
 60}
 61
 62func (s *Service) resolver(ctx context.Context) *resolver {
 63	return &resolver{ctx: ctx, store: s.store, users: map[string]string{}, groups: map[string]string{}}
 64}
 65
 66func (r *resolver) entries(events []storage.AuditEvent) ([]AuditEntry, error) {
 67	out := make([]AuditEntry, 0, len(events))
 68	for _, e := range events {
 69		actor, err := r.actor(e.Actor)
 70		if err != nil {
 71			return nil, err
 72		}
 73		subject, err := r.subjectOf(e)
 74		if err != nil {
 75			return nil, err
 76		}
 77		out = append(out, AuditEntry{At: e.OccurredAt, Actor: actor, Action: e.Action, Subject: subject})
 78	}
 79	return out, nil
 80}
 81
 82func (r *resolver) actor(id string) (string, error) {
 83	name, found, err := r.username(id)
 84	if err != nil || found {
 85		return name, err
 86	}
 87	return id, nil
 88}
 89
 90func (r *resolver) subject(id string) (string, error) {
 91	if name, found, err := r.username(id); err != nil || found {
 92		return name, err
 93	}
 94	if name, found, err := r.groupName(id); err != nil || found {
 95		return name, err
 96	}
 97	return id, nil
 98}
 99
100func (r *resolver) username(id string) (string, bool, error) {
101	if name, seen := r.users[id]; seen {
102		return name, name != "", nil
103	}
104	u, err := r.store.UserByID(r.ctx, id)
105	if errors.Is(err, storage.ErrNotFound) {
106		r.users[id] = ""
107		return "", false, nil
108	}
109	if err != nil {
110		return "", false, failed("resolve an audit user", err)
111	}
112	r.users[id] = u.Username
113	return u.Username, true, nil
114}
115
116func (r *resolver) groupName(id string) (string, bool, error) {
117	if name, seen := r.groups[id]; seen {
118		return name, name != "", nil
119	}
120	g, err := r.store.GroupByID(r.ctx, id)
121	if errors.Is(err, storage.ErrNotFound) {
122		r.groups[id] = ""
123		return "", false, nil
124	}
125	if err != nil {
126		return "", false, failed("resolve an audit group", err)
127	}
128	r.groups[id] = g.Name
129	return g.Name, true, nil
130}