package service import ( "context" "errors" "strings" "gitea.stevedudenhoeffer.com/steve/pansy/internal/domain" ) // isShareRole reports whether role is a grantable share role (owner is implicit, // never a share row). func isShareRole(role string) bool { return role == domain.RoleViewer || role == domain.RoleEditor } // ListShares returns a garden's shares (each with the recipient's identity). // Owner only — sharing is managed by the owner alone. func (s *Service) ListShares(ctx context.Context, actorID, gardenID int64) ([]domain.ShareWithUser, error) { if _, err := s.requireGardenRole(ctx, actorID, gardenID, roleOwner); err != nil { return nil, err } return s.store.ListSharesForGarden(ctx, gardenID) } // AddShare grants a user viewer/editor access to a garden, targeting them by the // exact email of an existing account (v1 has no invitation emails). Owner only. // Unknown email → ErrShareUserNotFound; the owner's own email → // ErrCannotShareWithSelf; an already-shared user → ErrShareExists. func (s *Service) AddShare(ctx context.Context, actorID, gardenID int64, email, role string) (*domain.GardenShare, error) { if _, err := s.requireGardenRole(ctx, actorID, gardenID, roleOwner); err != nil { return nil, err } if !isShareRole(role) { return nil, domain.ErrInvalidInput } target, err := s.store.GetUserByEmail(ctx, strings.TrimSpace(email)) if errors.Is(err, domain.ErrNotFound) { return nil, domain.ErrShareUserNotFound } if err != nil { return nil, err } if target.ID == actorID { return nil, domain.ErrCannotShareWithSelf } return s.store.CreateShare(ctx, &domain.GardenShare{ GardenID: gardenID, UserID: target.ID, Role: role, CreatedBy: actorID, }) } // UpdateShareRole changes an existing share's role. Owner only. func (s *Service) UpdateShareRole(ctx context.Context, actorID, gardenID, targetUserID int64, role string) (*domain.GardenShare, error) { if _, err := s.requireGardenRole(ctx, actorID, gardenID, roleOwner); err != nil { return nil, err } if !isShareRole(role) { return nil, domain.ErrInvalidInput } return s.store.UpdateShareRole(ctx, gardenID, targetUserID, role) } // RemoveShare revokes a share. The garden owner may remove anyone; a recipient // may remove themselves ("leave garden"). Any other actor gets the garden's // masked ErrNotFound (existence isn't revealed to non-participants). func (s *Service) RemoveShare(ctx context.Context, actorID, gardenID, targetUserID int64) error { g, err := s.store.GetGarden(ctx, gardenID) if err != nil { return err // ErrNotFound } if g.OwnerID != actorID && actorID != targetUserID { return domain.ErrNotFound } // Owner path removes any share; self-leave removes the actor's own (a missing // row is ErrNotFound either way). return s.store.DeleteShare(ctx, gardenID, targetUserID) }