package admin import ( "cmp" "context" "errors" "slices" "tug/internal/storage" ) func (s *Service) Dashboard(ctx context.Context) (Dashboard, error) { counts, err := s.store.Dashboard(ctx, s.now()) if err != nil { return Dashboard{}, failed("read dashboard counts", err) } events, err := s.store.ListAudit(ctx, storage.AuditFilter{Limit: foldWindow}) if err != nil { return Dashboard{}, failed("read recent audit", err) } recent, err := s.resolver(ctx).recent(events) if err != nil { return Dashboard{}, err } return Dashboard{ Users: counts.Users, DisabledUsers: counts.DisabledUsers, Groups: counts.Groups, PendingInvitations: counts.PendingInvitations, LiveSessions: counts.LiveSessions, Recent: recent, }, nil } func (s *Service) activity(ctx context.Context, userID string) ([]AuditEntry, error) { byActor, err := s.store.ListAudit(ctx, storage.AuditFilter{Actor: userID, Limit: foldWindow}) if err != nil { return nil, failed("read user's activity", err) } bySubject, err := s.store.ListAudit(ctx, storage.AuditFilter{Subject: userID, Limit: foldWindow}) if err != nil { return nil, failed("read user's activity", err) } events := append(byActor, bySubject...) slices.SortFunc(events, func(a, b storage.AuditEvent) int { if c := b.OccurredAt.Compare(a.OccurredAt); c != 0 { return c } return cmp.Compare(b.ID, a.ID) }) events = slices.CompactFunc(events, func(a, b storage.AuditEvent) bool { return a.ID == b.ID }) return s.resolver(ctx).recent(events) } type resolver struct { ctx context.Context store *storage.Store users map[string]string groups map[string]string } func (s *Service) resolver(ctx context.Context) *resolver { return &resolver{ctx: ctx, store: s.store, users: map[string]string{}, groups: map[string]string{}} } func (r *resolver) entries(events []storage.AuditEvent) ([]AuditEntry, error) { out := make([]AuditEntry, 0, len(events)) for _, e := range events { actor, err := r.actor(e.Actor) if err != nil { return nil, err } subject, err := r.subjectOf(e) if err != nil { return nil, err } out = append(out, AuditEntry{At: e.OccurredAt, Actor: actor, Action: e.Action, Subject: subject}) } return out, nil } func (r *resolver) actor(id string) (string, error) { name, found, err := r.username(id) if err != nil || found { return name, err } return id, nil } func (r *resolver) subject(id string) (string, error) { if name, found, err := r.username(id); err != nil || found { return name, err } if name, found, err := r.groupName(id); err != nil || found { return name, err } return id, nil } func (r *resolver) username(id string) (string, bool, error) { if name, seen := r.users[id]; seen { return name, name != "", nil } u, err := r.store.UserByID(r.ctx, id) if errors.Is(err, storage.ErrNotFound) { r.users[id] = "" return "", false, nil } if err != nil { return "", false, failed("resolve an audit user", err) } r.users[id] = u.Username return u.Username, true, nil } func (r *resolver) groupName(id string) (string, bool, error) { if name, seen := r.groups[id]; seen { return name, name != "", nil } g, err := r.store.GroupByID(r.ctx, id) if errors.Is(err, storage.ErrNotFound) { r.groups[id] = "" return "", false, nil } if err != nil { return "", false, failed("resolve an audit group", err) } r.groups[id] = g.Name return g.Name, true, nil }