1package admin
2
3import (
4 "context"
5 "errors"
6 "time"
7
8 "tug/internal/storage"
9)
10
11func (s *Service) ListInvitations(ctx context.Context, page int) (InvitationList, error) {
12 page = max(page, 1)
13 got, err := s.store.ListInvitationPage(ctx, (page-1)*InvitationsPerPage, InvitationsPerPage)
14 if err != nil {
15 return InvitationList{}, failed("list invitations", err)
16 }
17 pending, err := s.pending(ctx)
18 if err != nil {
19 return InvitationList{}, err
20 }
21 now := s.now()
22 out := InvitationList{Total: got.Total, Invitations: make([]Invitation, 0, len(got.Records))}
23 for _, r := range got.Records {
24 inv := Invitation{ID: r.ID, Label: r.Label, CreatedAt: r.CreatedAt, ExpiresAt: r.ExpiresAt, State: state(r.Invitation, now), UsedAt: r.UsedAt}
25 if r.CreatedByName != nil {
26 inv.CreatedBy = *r.CreatedByName
27 }
28 if r.UsedBy != nil && r.UsedByName != nil {
29 inv.UsedBy, inv.UsedByID = *r.UsedByName, *r.UsedBy
30 inv.UsedByPending = isPendingIn(pending, *r.UsedBy)
31 }
32 if r.BatchID != nil {
33 inv.Batch = *r.BatchID
34 }
35 for _, g := range r.Groups {
36 inv.Groups = append(inv.Groups, InvitationGroup{Name: g.GroupName, Deleted: g.GroupID == nil})
37 }
38 out.Invitations = append(out.Invitations, inv)
39 }
40 return out, nil
41}
42
43func (s *Service) Invitation(ctx context.Context, id string) (Invitation, error) {
44 inv, err := s.store.InvitationByID(ctx, id)
45 if err != nil {
46 return Invitation{}, target("read invitation", err)
47 }
48 return s.resolver(ctx).invitation(inv, s.now())
49}
50
51func (s *Service) RevokeInvitation(ctx context.Context, actor, id string) error {
52 inv, err := s.store.InvitationByID(ctx, id)
53 if err != nil {
54 return target("read invitation", err)
55 }
56 if state(inv, s.now()) != InvitationPending {
57 return ProblemInvitationClosed
58 }
59 err = s.store.RevokeInvitationAudited(ctx, id, actor)
60 if errors.Is(err, storage.ErrConflict) {
61 return ProblemInvitationClosed
62 }
63 if err != nil {
64 return target("revoke invitation", err)
65 }
66 return nil
67}
68
69func state(inv storage.Invitation, now time.Time) InvitationState {
70 switch {
71 case inv.UsedAt != nil:
72 return InvitationUsed
73 case inv.RevokedAt != nil:
74 return InvitationRevoked
75 case !inv.ExpiresAt.After(now):
76 return InvitationExpired
77 }
78 return InvitationPending
79}
80
81func (r *resolver) invitation(inv storage.Invitation, now time.Time) (Invitation, error) {
82 view := Invitation{ID: inv.ID, CreatedAt: inv.CreatedAt, ExpiresAt: inv.ExpiresAt, State: state(inv, now)}
83 if inv.CreatedBy != nil {
84 creator, err := r.actor(*inv.CreatedBy)
85 if err != nil {
86 return Invitation{}, err
87 }
88 view.CreatedBy = creator
89 }
90 return view, nil
91}
92
93func (s *Service) RevokeInvitationBatch(ctx context.Context, actor, batch string) (int, error) {
94 n, err := s.store.RevokeInvitationBatchAudited(ctx, batch, actor)
95 if errors.Is(err, storage.ErrNotFound) {
96 return 0, ProblemUnknownBatch
97 }
98 if err != nil {
99 return 0, failed("revoke invitation batch", err)
100 }
101 return n, nil
102}