package admin import ( "context" "errors" "time" "tug/internal/storage" ) func (s *Service) ListInvitations(ctx context.Context, page int) (InvitationList, error) { page = max(page, 1) got, err := s.store.ListInvitationPage(ctx, (page-1)*InvitationsPerPage, InvitationsPerPage) if err != nil { return InvitationList{}, failed("list invitations", err) } pending, err := s.pending(ctx) if err != nil { return InvitationList{}, err } now := s.now() out := InvitationList{Total: got.Total, Invitations: make([]Invitation, 0, len(got.Records))} for _, r := range got.Records { inv := Invitation{ID: r.ID, Label: r.Label, CreatedAt: r.CreatedAt, ExpiresAt: r.ExpiresAt, State: state(r.Invitation, now), UsedAt: r.UsedAt} if r.CreatedByName != nil { inv.CreatedBy = *r.CreatedByName } if r.UsedBy != nil && r.UsedByName != nil { inv.UsedBy, inv.UsedByID = *r.UsedByName, *r.UsedBy inv.UsedByPending = isPendingIn(pending, *r.UsedBy) } if r.BatchID != nil { inv.Batch = *r.BatchID } for _, g := range r.Groups { inv.Groups = append(inv.Groups, InvitationGroup{Name: g.GroupName, Deleted: g.GroupID == nil}) } out.Invitations = append(out.Invitations, inv) } return out, nil } func (s *Service) Invitation(ctx context.Context, id string) (Invitation, error) { inv, err := s.store.InvitationByID(ctx, id) if err != nil { return Invitation{}, target("read invitation", err) } return s.resolver(ctx).invitation(inv, s.now()) } func (s *Service) RevokeInvitation(ctx context.Context, actor, id string) error { inv, err := s.store.InvitationByID(ctx, id) if err != nil { return target("read invitation", err) } if state(inv, s.now()) != InvitationPending { return ProblemInvitationClosed } err = s.store.RevokeInvitationAudited(ctx, id, actor) if errors.Is(err, storage.ErrConflict) { return ProblemInvitationClosed } if err != nil { return target("revoke invitation", err) } return nil } func state(inv storage.Invitation, now time.Time) InvitationState { switch { case inv.UsedAt != nil: return InvitationUsed case inv.RevokedAt != nil: return InvitationRevoked case !inv.ExpiresAt.After(now): return InvitationExpired } return InvitationPending } func (r *resolver) invitation(inv storage.Invitation, now time.Time) (Invitation, error) { view := Invitation{ID: inv.ID, CreatedAt: inv.CreatedAt, ExpiresAt: inv.ExpiresAt, State: state(inv, now)} if inv.CreatedBy != nil { creator, err := r.actor(*inv.CreatedBy) if err != nil { return Invitation{}, err } view.CreatedBy = creator } return view, nil } func (s *Service) RevokeInvitationBatch(ctx context.Context, actor, batch string) (int, error) { n, err := s.store.RevokeInvitationBatchAudited(ctx, batch, actor) if errors.Is(err, storage.ErrNotFound) { return 0, ProblemUnknownBatch } if err != nil { return 0, failed("revoke invitation batch", err) } return n, nil }