0e7890a465
Run gofmt -w across the backend, normalising the manually-aligned := blocks to the gofmt standard. No code behaviour changes. Add Prettier (+ prettier-plugin-svelte) to the frontend with the SvelteKit default config (tabs, single quotes) so formatting is reproducible, then run it over the whole tree. Add format / format:check npm scripts and a .prettierignore (build output, generated schema.ts, static assets). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
242 lines
6.4 KiB
Go
242 lines
6.4 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
|
||
"github.com/google/uuid"
|
||
|
||
"tanabata/backend/internal/domain"
|
||
"tanabata/backend/internal/port"
|
||
)
|
||
|
||
const poolObjectType = "pool"
|
||
const poolObjectTypeID int16 = 4 // fourth row in 007_seed_data.sql object_types
|
||
|
||
// PoolParams holds the fields for creating or patching a pool.
|
||
type PoolParams struct {
|
||
Name string
|
||
Notes *string
|
||
Metadata json.RawMessage
|
||
IsPublic *bool
|
||
}
|
||
|
||
// PoolService handles pool CRUD and pool–file management with ACL + audit.
|
||
type PoolService struct {
|
||
pools port.PoolRepo
|
||
acl *ACLService
|
||
audit *AuditService
|
||
}
|
||
|
||
// NewPoolService creates a PoolService.
|
||
func NewPoolService(
|
||
pools port.PoolRepo,
|
||
acl *ACLService,
|
||
audit *AuditService,
|
||
) *PoolService {
|
||
return &PoolService{pools: pools, acl: acl, audit: audit}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// CRUD
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// List returns a paginated list of pools the caller may see.
|
||
func (s *PoolService) List(ctx context.Context, params port.OffsetParams) (*domain.PoolOffsetPage, error) {
|
||
params.ViewerID, params.ViewerIsAdmin, _ = domain.UserFromContext(ctx)
|
||
return s.pools.List(ctx, params)
|
||
}
|
||
|
||
// Get returns a pool by ID, enforcing view ACL.
|
||
func (s *PoolService) Get(ctx context.Context, id uuid.UUID) (*domain.Pool, error) {
|
||
userID, isAdmin, _ := domain.UserFromContext(ctx)
|
||
p, err := s.pools.GetByID(ctx, id)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
ok, err := s.acl.CanView(ctx, userID, isAdmin, p.CreatorID, p.IsPublic, poolObjectTypeID, id)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if !ok {
|
||
return nil, domain.ErrForbidden
|
||
}
|
||
return p, nil
|
||
}
|
||
|
||
// authorizeView returns nil if the caller may view the pool, else ErrForbidden
|
||
// (or ErrNotFound if the pool does not exist).
|
||
func (s *PoolService) authorizeView(ctx context.Context, poolID uuid.UUID) error {
|
||
userID, isAdmin, _ := domain.UserFromContext(ctx)
|
||
p, err := s.pools.GetByID(ctx, poolID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
ok, err := s.acl.CanView(ctx, userID, isAdmin, p.CreatorID, p.IsPublic, poolObjectTypeID, poolID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !ok {
|
||
return domain.ErrForbidden
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// authorizeEdit returns nil if the caller may edit the pool, else ErrForbidden
|
||
// (or ErrNotFound if the pool does not exist).
|
||
func (s *PoolService) authorizeEdit(ctx context.Context, poolID uuid.UUID) error {
|
||
userID, isAdmin, _ := domain.UserFromContext(ctx)
|
||
p, err := s.pools.GetByID(ctx, poolID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
ok, err := s.acl.CanEdit(ctx, userID, isAdmin, p.CreatorID, poolObjectTypeID, poolID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !ok {
|
||
return domain.ErrForbidden
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Create inserts a new pool.
|
||
func (s *PoolService) Create(ctx context.Context, p PoolParams) (*domain.Pool, error) {
|
||
userID, _, _ := domain.UserFromContext(ctx)
|
||
|
||
pool := &domain.Pool{
|
||
Name: p.Name,
|
||
Notes: p.Notes,
|
||
Metadata: p.Metadata,
|
||
CreatorID: userID,
|
||
}
|
||
if p.IsPublic != nil {
|
||
pool.IsPublic = *p.IsPublic
|
||
}
|
||
|
||
created, err := s.pools.Create(ctx, pool)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
objType := poolObjectType
|
||
_ = s.audit.Log(ctx, "pool_create", &objType, &created.ID, nil)
|
||
return created, nil
|
||
}
|
||
|
||
// Update applies a partial patch to a pool.
|
||
func (s *PoolService) Update(ctx context.Context, id uuid.UUID, p PoolParams) (*domain.Pool, error) {
|
||
userID, isAdmin, _ := domain.UserFromContext(ctx)
|
||
|
||
current, err := s.pools.GetByID(ctx, id)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
ok, err := s.acl.CanEdit(ctx, userID, isAdmin, current.CreatorID, poolObjectTypeID, id)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if !ok {
|
||
return nil, domain.ErrForbidden
|
||
}
|
||
|
||
patch := *current
|
||
if p.Name != "" {
|
||
patch.Name = p.Name
|
||
}
|
||
if p.Notes != nil {
|
||
patch.Notes = p.Notes
|
||
}
|
||
if len(p.Metadata) > 0 {
|
||
patch.Metadata = p.Metadata
|
||
}
|
||
if p.IsPublic != nil {
|
||
patch.IsPublic = *p.IsPublic
|
||
}
|
||
|
||
updated, err := s.pools.Update(ctx, id, &patch)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
objType := poolObjectType
|
||
_ = s.audit.Log(ctx, "pool_edit", &objType, &id, nil)
|
||
return updated, nil
|
||
}
|
||
|
||
// Delete removes a pool by ID, enforcing edit ACL.
|
||
func (s *PoolService) Delete(ctx context.Context, id uuid.UUID) error {
|
||
userID, isAdmin, _ := domain.UserFromContext(ctx)
|
||
|
||
pool, err := s.pools.GetByID(ctx, id)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
ok, err := s.acl.CanEdit(ctx, userID, isAdmin, pool.CreatorID, poolObjectTypeID, id)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !ok {
|
||
return domain.ErrForbidden
|
||
}
|
||
|
||
if err := s.pools.Delete(ctx, id); err != nil {
|
||
return err
|
||
}
|
||
|
||
objType := poolObjectType
|
||
_ = s.audit.Log(ctx, "pool_delete", &objType, &id, nil)
|
||
return nil
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Pool–file operations
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// ListFiles returns cursor-paginated files within a pool ordered by position,
|
||
// enforcing view ACL on the pool.
|
||
func (s *PoolService) ListFiles(ctx context.Context, poolID uuid.UUID, params port.PoolFileListParams) (*domain.PoolFilePage, error) {
|
||
if err := s.authorizeView(ctx, poolID); err != nil {
|
||
return nil, err
|
||
}
|
||
return s.pools.ListFiles(ctx, poolID, params)
|
||
}
|
||
|
||
// AddFiles adds files to a pool at the given position (nil = append), enforcing
|
||
// edit ACL on the pool.
|
||
func (s *PoolService) AddFiles(ctx context.Context, poolID uuid.UUID, fileIDs []uuid.UUID, position *int) error {
|
||
if err := s.authorizeEdit(ctx, poolID); err != nil {
|
||
return err
|
||
}
|
||
if err := s.pools.AddFiles(ctx, poolID, fileIDs, position); err != nil {
|
||
return err
|
||
}
|
||
objType := poolObjectType
|
||
_ = s.audit.Log(ctx, "file_pool_add", &objType, &poolID, map[string]any{"count": len(fileIDs)})
|
||
return nil
|
||
}
|
||
|
||
// RemoveFiles removes files from a pool, enforcing edit ACL on the pool.
|
||
func (s *PoolService) RemoveFiles(ctx context.Context, poolID uuid.UUID, fileIDs []uuid.UUID) error {
|
||
if err := s.authorizeEdit(ctx, poolID); err != nil {
|
||
return err
|
||
}
|
||
if err := s.pools.RemoveFiles(ctx, poolID, fileIDs); err != nil {
|
||
return err
|
||
}
|
||
objType := poolObjectType
|
||
_ = s.audit.Log(ctx, "file_pool_remove", &objType, &poolID, map[string]any{"count": len(fileIDs)})
|
||
return nil
|
||
}
|
||
|
||
// Reorder sets the ordered sequence of file IDs within a pool, enforcing edit
|
||
// ACL on the pool.
|
||
func (s *PoolService) Reorder(ctx context.Context, poolID uuid.UUID, fileIDs []uuid.UUID) error {
|
||
if err := s.authorizeEdit(ctx, poolID); err != nil {
|
||
return err
|
||
}
|
||
return s.pools.Reorder(ctx, poolID, fileIDs)
|
||
}
|