package service import ( "context" "strings" "gitea.stevedudenhoeffer.com/steve/pansy/internal/domain" ) // PublicShareLink is the owner-visible state of a garden's public read-only link. // Token is omitted (and empty) when the link is disabled. type PublicShareLink struct { Enabled bool `json:"enabled"` Token string `json:"token,omitempty"` } func linkState(token *string) *PublicShareLink { if token == nil { return &PublicShareLink{Enabled: false} } return &PublicShareLink{Enabled: true, Token: *token} } // PublicGarden returns the read-only /full payload for the garden addressed by a // public share token. The token is the capability — anyone holding a valid one // may read — so there is no actor or ACL check. An unknown, empty, or disabled // token yields domain.ErrNotFound (existence is masked, same as a private row). func (s *Service) PublicGarden(ctx context.Context, token string) (*FullGarden, error) { token = strings.TrimSpace(token) if token == "" { return nil, domain.ErrNotFound } g, err := s.store.GetGardenByPublicToken(ctx, token) if err != nil { return nil, err } full, err := s.assembleFull(ctx, g) if err != nil { return nil, err } // Minimize what an anonymous viewer learns: no role, and no owner user id — // neither the garden's owner nor the owner of any referenced custom plant // (built-ins already have a nil OwnerID). The page renders fine without them. full.Garden.MyRole = "" full.Garden.OwnerID = 0 for i := range full.Plants { full.Plants[i].OwnerID = nil } return full, nil } // GetPublicShareLink reports whether the garden's public link is on, and (to the // owner) the current token. Owner only. func (s *Service) GetPublicShareLink(ctx context.Context, actorID, gardenID int64) (*PublicShareLink, error) { if _, err := s.requireGardenRole(ctx, actorID, gardenID, roleOwner); err != nil { return nil, err } token, err := s.store.GetGardenPublicToken(ctx, gardenID) if err != nil { return nil, err } return linkState(token), nil } // EnablePublicShareLink turns the public link on and returns the token. It is // idempotent when rotate is false (an existing link keeps its token); when rotate // is true it always issues a fresh token, invalidating the previous URL. Owner // only. func (s *Service) EnablePublicShareLink(ctx context.Context, actorID, gardenID int64, rotate bool) (*PublicShareLink, error) { if _, err := s.requireGardenRole(ctx, actorID, gardenID, roleOwner); err != nil { return nil, err } token, err := s.store.GetGardenPublicToken(ctx, gardenID) if err != nil { return nil, err } if token == nil || rotate { fresh, err := newPublicToken() if err != nil { return nil, err } if err := s.store.SetGardenPublicToken(ctx, gardenID, &fresh); err != nil { return nil, err } token = &fresh } return linkState(token), nil } // DisablePublicShareLink turns the public link off (clears the token). Owner // only; idempotent (disabling an already-disabled link is a no-op success). func (s *Service) DisablePublicShareLink(ctx context.Context, actorID, gardenID int64) error { if _, err := s.requireGardenRole(ctx, actorID, gardenID, roleOwner); err != nil { return err } return s.store.SetGardenPublicToken(ctx, gardenID, nil) }