diff --git a/internal/adapters/graphql/helpers.go b/internal/adapters/graphql/helpers.go index b33f2e9..550acbb 100644 --- a/internal/adapters/graphql/helpers.go +++ b/internal/adapters/graphql/helpers.go @@ -34,3 +34,12 @@ func (r *queryResolver) resolveWorkContent(ctx context.Context, workID uint, pre return nil } + +func toInt32(i int64) *int32 { + val := int32(i) + return &val +} + +func toInt(i int) *int { + return &i +} diff --git a/internal/adapters/graphql/integration_test.go b/internal/adapters/graphql/integration_test.go index 01e20a6..7f633e0 100644 --- a/internal/adapters/graphql/integration_test.go +++ b/internal/adapters/graphql/integration_test.go @@ -1048,7 +1048,7 @@ func (s *GraphQLIntegrationSuite) TestTrendingWorksQuery() { func (s *GraphQLIntegrationSuite) TestCollectionMutations() { // Create users for testing authorization - _, ownerToken := s.CreateAuthenticatedUser("collectionowner", "owner@test.com", domain.UserRoleReader) + owner, ownerToken := s.CreateAuthenticatedUser("collectionowner", "owner@test.com", domain.UserRoleReader) otherUser, otherToken := s.CreateAuthenticatedUser("otheruser", "other@test.com", domain.UserRoleReader) _ = otherUser @@ -1175,6 +1175,7 @@ func (s *GraphQLIntegrationSuite) TestCollectionMutations() { err = s.App.Collection.Commands.AddWorkToCollection(context.Background(), collection.AddWorkToCollectionInput{ CollectionID: uint(collectionIDInt), WorkID: work.ID, + UserID: owner.ID, }) s.Require().NoError(err) diff --git a/internal/adapters/graphql/schema.resolvers.go b/internal/adapters/graphql/schema.resolvers.go index 268fb69..95f4630 100644 --- a/internal/adapters/graphql/schema.resolvers.go +++ b/internal/adapters/graphql/schema.resolvers.go @@ -395,24 +395,11 @@ func (r *mutationResolver) UpdateCollection(ctx context.Context, id string, inpu return nil, fmt.Errorf("invalid collection ID: %v", err) } - // Fetch the existing collection - collectionModel, err := r.App.Collection.Queries.Collection(ctx, uint(collectionID)) - if err != nil { - return nil, err - } - if collectionModel == nil { - return nil, fmt.Errorf("collection not found") - } - - // Check ownership - if collectionModel.UserID != userID { - return nil, fmt.Errorf("unauthorized") - } - // Call collection service updateInput := collection.UpdateCollectionInput{ - ID: uint(collectionID), - Name: input.Name, + ID: uint(collectionID), + Name: input.Name, + UserID: userID, } if input.Description != nil { updateInput.Description = *input.Description @@ -447,22 +434,8 @@ func (r *mutationResolver) DeleteCollection(ctx context.Context, id string) (boo return false, fmt.Errorf("invalid collection ID: %v", err) } - // Fetch the existing collection - collection, err := r.App.Collection.Queries.Collection(ctx, uint(collectionID)) - if err != nil { - return false, err - } - if collection == nil { - return false, fmt.Errorf("collection not found") - } - - // Check ownership - if collection.UserID != userID { - return false, fmt.Errorf("unauthorized") - } - - // Call collection repository - err = r.App.Collection.Commands.DeleteCollection(ctx, uint(collectionID)) + // Call collection service + err = r.App.Collection.Commands.DeleteCollection(ctx, uint(collectionID), userID) if err != nil { return false, err } @@ -488,24 +461,11 @@ func (r *mutationResolver) AddWorkToCollection(ctx context.Context, collectionID return nil, fmt.Errorf("invalid work ID: %v", err) } - // Fetch the existing collection - collectionModel, err := r.App.Collection.Queries.Collection(ctx, uint(collID)) - if err != nil { - return nil, err - } - if collectionModel == nil { - return nil, fmt.Errorf("collection not found") - } - - // Check ownership - if collectionModel.UserID != userID { - return nil, fmt.Errorf("unauthorized") - } - // Add work to collection addInput := collection.AddWorkToCollectionInput{ CollectionID: uint(collID), WorkID: uint(wID), + UserID: userID, } err = r.App.Collection.Commands.AddWorkToCollection(ctx, addInput) if err != nil { @@ -544,24 +504,11 @@ func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collect return nil, fmt.Errorf("invalid work ID: %v", err) } - // Fetch the existing collection - collectionModel, err := r.App.Collection.Queries.Collection(ctx, uint(collID)) - if err != nil { - return nil, err - } - if collectionModel == nil { - return nil, fmt.Errorf("collection not found") - } - - // Check ownership - if collectionModel.UserID != userID { - return nil, fmt.Errorf("unauthorized") - } - // Remove work from collection removeInput := collection.RemoveWorkFromCollectionInput{ CollectionID: uint(collID), WorkID: uint(wID), + UserID: userID, } err = r.App.Collection.Commands.RemoveWorkFromCollection(ctx, removeInput) if err != nil { @@ -1325,16 +1272,27 @@ type queryResolver struct{ *Resolver } // it when you're done. // - You have helper methods in this file. Move them out to keep these resolver files clean. /* - func (r *Resolver) Work() WorkResolver { return &workResolver{r} } -func (r *Resolver) Translation() TranslationResolver { return &translationResolver{r} } -type workResolver struct{ *Resolver } -type translationResolver struct{ *Resolver } -func toInt32(i int64) *int { - val := int(i) - return &val -} -func toInt(i int) *int { - return &i + func (r *translationResolver) Stats(ctx context.Context, obj *model.Translation) (*model.TranslationStats, error) { + translationID, err := strconv.ParseUint(obj.ID, 10, 32) + if err != nil { + return nil, fmt.Errorf("invalid translation ID: %v", err) + } + + stats, err := r.App.Analytics.GetOrCreateTranslationStats(ctx, uint(translationID)) + if err != nil { + return nil, err + } + + // Convert domain model to GraphQL model + return &model.TranslationStats{ + ID: fmt.Sprintf("%d", stats.ID), + Views: toInt32(stats.Views), + Likes: toInt32(stats.Likes), + Comments: toInt32(stats.Comments), + Shares: toInt32(stats.Shares), + ReadingTime: toInt32(int64(stats.ReadingTime)), + Sentiment: &stats.Sentiment, + }, nil } func (r *workResolver) Stats(ctx context.Context, obj *model.Work) (*model.WorkStats, error) { workID, err := strconv.ParseUint(obj.ID, 10, 32) @@ -1356,31 +1314,13 @@ func (r *workResolver) Stats(ctx context.Context, obj *model.Work) (*model.WorkS Bookmarks: toInt32(stats.Bookmarks), Shares: toInt32(stats.Shares), TranslationCount: toInt32(stats.TranslationCount), - ReadingTime: toInt(stats.ReadingTime), + ReadingTime: toInt32(int64(stats.ReadingTime)), Complexity: &stats.Complexity, Sentiment: &stats.Sentiment, }, nil } -func (r *translationResolver) Stats(ctx context.Context, obj *model.Translation) (*model.TranslationStats, error) { - translationID, err := strconv.ParseUint(obj.ID, 10, 32) - if err != nil { - return nil, fmt.Errorf("invalid translation ID: %v", err) - } - - stats, err := r.App.Analytics.GetOrCreateTranslationStats(ctx, uint(translationID)) - if err != nil { - return nil, err - } - - // Convert domain model to GraphQL model - return &model.TranslationStats{ - ID: fmt.Sprintf("%d", stats.ID), - Views: toInt32(stats.Views), - Likes: toInt32(stats.Likes), - Comments: toInt32(stats.Comments), - Shares: toInt32(stats.Shares), - ReadingTime: toInt(stats.ReadingTime), - Sentiment: &stats.Sentiment, - }, nil -} +func (r *Resolver) Translation() TranslationResolver { return &translationResolver{r} } +func (r *Resolver) Work() WorkResolver { return &workResolver{r} } +type translationResolver struct{ *Resolver } +type workResolver struct{ *Resolver } */ diff --git a/internal/app/collection/commands.go b/internal/app/collection/commands.go index 99b4f90..626d09e 100644 --- a/internal/app/collection/commands.go +++ b/internal/app/collection/commands.go @@ -2,6 +2,7 @@ package collection import ( "context" + "fmt" "tercul/internal/domain" ) @@ -47,6 +48,7 @@ type UpdateCollectionInput struct { Description string IsPublic bool CoverImageURL string + UserID uint } // UpdateCollection updates an existing collection. @@ -55,6 +57,9 @@ func (c *CollectionCommands) UpdateCollection(ctx context.Context, input UpdateC if err != nil { return nil, err } + if collection.UserID != input.UserID { + return nil, fmt.Errorf("unauthorized: user %d cannot update collection %d", input.UserID, input.ID) + } collection.Name = input.Name collection.Description = input.Description collection.IsPublic = input.IsPublic @@ -67,7 +72,14 @@ func (c *CollectionCommands) UpdateCollection(ctx context.Context, input UpdateC } // DeleteCollection deletes a collection by ID. -func (c *CollectionCommands) DeleteCollection(ctx context.Context, id uint) error { +func (c *CollectionCommands) DeleteCollection(ctx context.Context, id uint, userID uint) error { + collection, err := c.repo.GetByID(ctx, id) + if err != nil { + return err + } + if collection.UserID != userID { + return fmt.Errorf("unauthorized: user %d cannot delete collection %d", userID, id) + } return c.repo.Delete(ctx, id) } @@ -75,10 +87,18 @@ func (c *CollectionCommands) DeleteCollection(ctx context.Context, id uint) erro type AddWorkToCollectionInput struct { CollectionID uint WorkID uint + UserID uint } // AddWorkToCollection adds a work to a collection. func (c *CollectionCommands) AddWorkToCollection(ctx context.Context, input AddWorkToCollectionInput) error { + collection, err := c.repo.GetByID(ctx, input.CollectionID) + if err != nil { + return err + } + if collection.UserID != input.UserID { + return fmt.Errorf("unauthorized: user %d cannot add work to collection %d", input.UserID, input.CollectionID) + } return c.repo.AddWorkToCollection(ctx, input.CollectionID, input.WorkID) } @@ -86,9 +106,17 @@ func (c *CollectionCommands) AddWorkToCollection(ctx context.Context, input AddW type RemoveWorkFromCollectionInput struct { CollectionID uint WorkID uint + UserID uint } // RemoveWorkFromCollection removes a work from a collection. func (c *CollectionCommands) RemoveWorkFromCollection(ctx context.Context, input RemoveWorkFromCollectionInput) error { + collection, err := c.repo.GetByID(ctx, input.CollectionID) + if err != nil { + return err + } + if collection.UserID != input.UserID { + return fmt.Errorf("unauthorized: user %d cannot remove work from collection %d", input.UserID, input.CollectionID) + } return c.repo.RemoveWorkFromCollection(ctx, input.CollectionID, input.WorkID) }