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}