package admin import ( "context" "errors" "slices" "tug/internal/storage" ) func (s *Service) Memberships(ctx context.Context, userID string, page int) (Memberships, error) { u, err := s.store.UserByID(ctx, userID) if err != nil { return Memberships{}, target("read user", err) } groups, err := s.store.GroupsForUser(ctx, u.ID) if err != nil { return Memberships{}, failed("read user's groups", err) } primary, err := s.primary(ctx) if err != nil { return Memberships{}, err } candidates, err := s.candidates(ctx, groups) if err != nil { return Memberships{}, err } badges := refs(groups, primary) return Memberships{Username: u.Username, Groups: window(badges, page, MembersPerPage), Total: len(badges), Candidates: candidates}, nil } func (s *Service) AddMembership(ctx context.Context, actor, userID, groupName string) error { if _, err := s.store.UserByID(ctx, userID); err != nil { return target("read user", err) } group, err := s.store.GroupByName(ctx, groupName) switch { case errors.Is(err, storage.ErrNotFound), errors.Is(err, storage.ErrInvalid): return ProblemUnknownGroup case err != nil: return failed("read group", err) } return s.add(ctx, actor, userID, group) } func (s *Service) AddMember(ctx context.Context, actor, groupID, username string) error { group, err := s.store.GroupByID(ctx, groupID) if err != nil { return target("read group", err) } if group.Name == s.primaryGroup { return ProblemPrimaryGroup } u, err := s.store.UserByUsername(ctx, username) switch { case errors.Is(err, storage.ErrNotFound), errors.Is(err, storage.ErrInvalid): return ProblemUnknownUser case err != nil: return failed("read user", err) } return s.add(ctx, actor, u.ID, group) } func (s *Service) add(ctx context.Context, actor, userID string, group storage.Group) error { if group.Name == s.primaryGroup { return ProblemPrimaryGroup } err := s.store.AddMembershipAudited(ctx, userID, group.ID, actor) if errors.Is(err, storage.ErrConflict) { return ProblemAlreadyMember } if err != nil { return target("add membership", err) } return nil } func (s *Service) RemoveMembership(ctx context.Context, actor, userID, groupID string) error { group, err := s.store.GroupByID(ctx, groupID) if err != nil { return target("read group", err) } if _, err := s.store.UserByID(ctx, userID); err != nil { return target("read user", err) } if group.Name == s.primaryGroup { return ProblemPrimaryGroup } if actor == userID && group.Name == s.adminGroup { return ProblemSelf } err = s.store.RemoveMembershipAudited(ctx, userID, group.ID, actor, s.adminGroup) switch { case err == nil: return nil case errors.Is(err, storage.ErrLastAdmin): return ProblemLastAdmin case errors.Is(err, storage.ErrNotFound): return ProblemNotMember } return failed("remove membership", err) } func (s *Service) ListGroups(ctx context.Context, page int) (GroupList, error) { groups, err := s.store.ListGroups(ctx) if err != nil { return GroupList{}, failed("list groups", err) } shown := window(groups, page, GroupsPerPage) rows := make([]GroupRow, 0, len(shown)) for _, g := range shown { members, err := s.members(ctx, g) if err != nil { return GroupList{}, err } enabled, _ := split(members) rows = append(rows, GroupRow{ID: g.ID, Name: g.Name, Description: g.Description, Members: len(enabled)}) } return GroupList{Groups: rows, Total: len(groups)}, nil } func (s *Service) Group(ctx context.Context, id string, page int) (Group, error) { g, err := s.store.GroupByID(ctx, id) if err != nil { return Group{}, target("read group", err) } users, err := s.members(ctx, g) if err != nil { return Group{}, err } enabled, disabled := split(users) pending, err := s.pending(ctx) if err != nil { return Group{}, err } _, referenced := s.configuredGroups() return Group{ ID: g.ID, Name: g.Name, Description: g.Description, GIDNumber: g.GIDNumber, Primary: g.Name == s.primaryGroup, Referenced: slices.Contains(referenced, g.Name), Members: membersOf(window(enabled, page, MembersPerPage)), Total: len(enabled), Disabled: markPending(membersOf(disabled[:min(len(disabled), DisabledShown)]), pending), DisabledTotal: len(disabled), }, nil } func (s *Service) members(ctx context.Context, g storage.Group) ([]storage.User, error) { var users []storage.User var err error if g.Name == s.primaryGroup { users, err = s.store.ListUsers(ctx) } else { users, err = s.store.UsersInGroup(ctx, g.ID) } if err != nil { return nil, failed("read group members", err) } return users, nil } func (s *Service) CreateGroup(ctx context.Context, actor string, g NewGroup) (string, error) { name, err := storage.NormalizeGroupName(g.Name) if err != nil { return "", ProblemGroupNameGrammar } if storage.CheckDisplayName(g.Description) != nil { return "", ProblemDescriptionInvalid } created, err := s.store.CreateGroupAudited(ctx, storage.CreateGroupInput{Name: name, Description: g.Description}, actor) switch { case errors.Is(err, storage.ErrGroupTaken): return "", ProblemGroupNameTaken case err != nil: return "", failed("create group", err) } return created.ID, nil } func (s *Service) DescribeGroup(ctx context.Context, actor, id, description string) error { if storage.CheckDisplayName(description) != nil { return ProblemDescriptionInvalid } if _, err := s.store.UpdateGroupDescriptionAudited(ctx, id, description, actor); err != nil { return target("describe group", err) } return nil } func (s *Service) DeleteGroup(ctx context.Context, actor, id string) error { _, referenced := s.configuredGroups() err := s.store.DeleteGroupAudited(ctx, id, actor, referenced) switch { case err == nil: return nil case errors.Is(err, storage.ErrReferencedGroup): return ProblemReferencedGroup } return target("delete group", err) }