Raw
  1package admin
  2
  3import (
  4	"context"
  5	"errors"
  6	"slices"
  7	"strings"
  8	"time"
  9
 10	"tug/internal/storage"
 11)
 12
 13const (
 14	InvitationBatchMax          = 50
 15	InvitationGroupChoicesShown = 100
 16	InvitationExpiryDefault     = 7 * 24 * time.Hour
 17)
 18
 19var InvitationExpiries = []time.Duration{24 * time.Hour, InvitationExpiryDefault, 30 * 24 * time.Hour}
 20
 21type NewInvitations struct {
 22	Labels     []string
 23	GroupIDs   []string
 24	GroupNames []string
 25	Expiry     time.Duration
 26}
 27
 28type IssuedInvitation struct {
 29	Label string
 30	URL   string
 31}
 32
 33type CreatedInvitations struct {
 34	Invitations []IssuedInvitation
 35	ExpiresAt   time.Time
 36	Groups      []string
 37}
 38
 39type GroupChoice struct {
 40	ID   string
 41	Name string
 42}
 43
 44type InvitationGroupChoices struct {
 45	Groups   []GroupChoice
 46	More     bool
 47	Defaults []string
 48}
 49
 50func (s *Service) CreateInvitations(ctx context.Context, actor string, n NewInvitations) (CreatedInvitations, error) {
 51	var labels []string
 52	for _, label := range n.Labels {
 53		if label = strings.TrimSpace(label); label != "" {
 54			labels = append(labels, label)
 55		}
 56	}
 57	if len(labels) == 0 || len(labels) > InvitationBatchMax {
 58		return CreatedInvitations{}, ProblemBatchSize
 59	}
 60	for _, label := range labels {
 61		if storage.CheckInvitationLabel(label) != nil {
 62			return CreatedInvitations{}, ProblemLabelInvalid
 63		}
 64	}
 65	if !slices.Contains(InvitationExpiries, n.Expiry) {
 66		return CreatedInvitations{}, ProblemExpiryInvalid
 67	}
 68	var ids []string
 69	defaults, _ := s.configuredGroups()
 70	for i, name := range append(slices.Clone(defaults), n.GroupNames...) {
 71		group, err := s.store.GroupByName(ctx, name)
 72		if errors.Is(err, storage.ErrNotFound) && i < len(defaults) {
 73			continue
 74		}
 75		if errors.Is(err, storage.ErrNotFound) {
 76			return CreatedInvitations{}, ProblemUnknownGroup
 77		}
 78		if err != nil {
 79			return CreatedInvitations{}, failed("read a group by name", err)
 80		}
 81		ids = append(ids, group.ID)
 82	}
 83	ids = append(ids, n.GroupIDs...)
 84	issued, err := s.invites.CreateMany(ctx, actor, labels, ids, n.Expiry)
 85	switch {
 86	case errors.Is(err, storage.ErrAdminGroupGrant):
 87		return CreatedInvitations{}, ProblemAdminGroupGrant
 88	case errors.Is(err, storage.ErrNotFound):
 89		return CreatedInvitations{}, ProblemUnknownGroup
 90	case err != nil:
 91		return CreatedInvitations{}, failed("create invitations", err)
 92	}
 93	out := CreatedInvitations{ExpiresAt: issued[0].ExpiresAt}
 94	for _, i := range issued {
 95		out.Invitations = append(out.Invitations, IssuedInvitation{Label: i.Label, URL: i.URL})
 96	}
 97	if out.Groups, err = s.invitationGroupNames(ctx, ids); err != nil {
 98		return CreatedInvitations{}, err
 99	}
100	return out, nil
101}
102
103func (s *Service) invitationGroupNames(ctx context.Context, ids []string) ([]string, error) {
104	configured, _ := s.configuredGroups()
105	var defaults, chosen []string
106	for _, id := range ids {
107		group, err := s.store.GroupByID(ctx, id)
108		if err != nil {
109			return nil, failed("read a granted group", err)
110		}
111		switch {
112		case slices.Contains(configured, group.Name):
113			if !slices.Contains(defaults, group.Name) {
114				defaults = append(defaults, group.Name)
115			}
116		case !slices.Contains(chosen, group.Name):
117			chosen = append(chosen, group.Name)
118		}
119	}
120	slices.Sort(chosen)
121	return append(defaults, chosen...), nil
122}
123
124func (s *Service) InvitationGroupChoices(ctx context.Context) (InvitationGroupChoices, error) {
125	all, err := s.store.ListGroups(ctx)
126	if err != nil {
127		return InvitationGroupChoices{}, failed("list groups", err)
128	}
129	defaults, _ := s.configuredGroups()
130	out := InvitationGroupChoices{Defaults: slices.Clone(defaults)}
131	for _, g := range all {
132		if g.Name == s.adminGroup || g.Name == s.primaryGroup || slices.Contains(defaults, g.Name) {
133			continue
134		}
135		if len(out.Groups) == InvitationGroupChoicesShown {
136			out.More = true
137			break
138		}
139		out.Groups = append(out.Groups, GroupChoice{ID: g.ID, Name: g.Name})
140	}
141	return out, nil
142}