From 57be05cdcb926313f26eaaf0feea9c4dde2c5354 Mon Sep 17 00:00:00 2001 From: Mir Arif Hasan Date: Wed, 25 Feb 2026 00:02:43 +0600 Subject: [PATCH] fix(backend): prevent IDOR in user collection and request endpoints (#5902) --- packages/hoppscotch-backend/src/errors.ts | 6 +- .../team-collection.service.spec.ts | 31 ++-- .../team-collection.service.ts | 114 +++++++----- .../src/team-request/team-request.service.ts | 49 +++-- .../user-collection.resolver.ts | 7 +- .../user-collection.service.spec.ts | 117 +++++++----- .../user-collection.service.ts | 169 ++++++++++-------- .../user-environments.resolver.ts | 2 +- .../user-request/user-request.service.spec.ts | 1 + .../src/user-request/user-request.service.ts | 63 ++++--- 10 files changed, 329 insertions(+), 230 deletions(-) diff --git a/packages/hoppscotch-backend/src/errors.ts b/packages/hoppscotch-backend/src/errors.ts index 723f013f..c3a64aff 100644 --- a/packages/hoppscotch-backend/src/errors.ts +++ b/packages/hoppscotch-backend/src/errors.ts @@ -787,15 +787,13 @@ export const INFRA_CONFIG_OPERATION_NOT_ALLOWED = * Error message for when the onboarding status fetch fails * (InfraConfigService) */ -export const INFRA_CONFIG_FETCH_FAILED = - 'infra_config/fetch_failed' as const; +export const INFRA_CONFIG_FETCH_FAILED = 'infra_config/fetch_failed' as const; /** * Onboarding has already been completed and cannot be re-run * (OnboardingController) */ -export const ONBOARDING_CANNOT_BE_RERUN = - 'onboarding/cannot_be_rerun' as const; +export const ONBOARDING_CANNOT_BE_RERUN = 'onboarding/cannot_be_rerun' as const; /** * Error message for when the database table does not exist diff --git a/packages/hoppscotch-backend/src/team-collection/team-collection.service.spec.ts b/packages/hoppscotch-backend/src/team-collection/team-collection.service.spec.ts index 92c6cf9a..8488da37 100644 --- a/packages/hoppscotch-backend/src/team-collection/team-collection.service.spec.ts +++ b/packages/hoppscotch-backend/src/team-collection/team-collection.service.spec.ts @@ -893,7 +893,10 @@ describe('deleteCollection', () => { .spyOn(teamCollectionService, 'getCollection') .mockResolvedValueOnce(E.right(rootTeamCollection)); jest - .spyOn(teamCollectionService as any, 'deleteCollectionAndUpdateSiblingsOrderIndex') + .spyOn( + teamCollectionService as any, + 'deleteCollectionAndUpdateSiblingsOrderIndex', + ) .mockResolvedValueOnce(E.left(TEAM_COL_REORDERING_FAILED)); const result = await teamCollectionService.deleteCollection( @@ -1667,23 +1670,6 @@ describe('FIX: updateMany queries now include teamID filter for root collections mockReset(mockPrisma); }); - const team2: Team = { - id: 'team_2', - name: 'Team 2', - }; - - // Team 2's root collection - should NOT be affected by Team 1's operations - const team2RootCollection: DBTeamCollection = { - id: 'team2-root-coll', - orderIndex: 1, - parentID: null, - title: 'Team 2 Root Collection', - teamID: team2.id, - data: {}, - createdOn: currentTime, - updatedOn: currentTime, - }; - test('FIX: deleteCollection - updateMany now correctly filters by teamID for root collections', async () => { /** * Scenario: Team 1 deletes a root collection @@ -1717,7 +1703,8 @@ describe('FIX: updateMany queries now include teamID filter for root collections await teamCollectionService.deleteCollection(team1RootToDelete.id); // Get the updateMany call from the transaction - const updateManyCall = mockPrisma.teamCollection.updateMany.mock.calls[0][0]; + const updateManyCall = + mockPrisma.teamCollection.updateMany.mock.calls[0][0]; // FIX VERIFICATION: The query now correctly includes teamID // This ensures only Team 1's root collections are affected @@ -1769,7 +1756,8 @@ describe('FIX: updateMany queries now include teamID filter for root collections ); // Get the actual updateMany call arguments - const updateManyCall = mockPrisma.teamCollection.updateMany.mock.calls[0][0]; + const updateManyCall = + mockPrisma.teamCollection.updateMany.mock.calls[0][0]; // FIX VERIFICATION: The query now correctly includes teamID expect(updateManyCall.where).toEqual({ @@ -1830,7 +1818,8 @@ describe('FIX: updateMany queries now include teamID filter for root collections ); // Get the actual updateMany call arguments - const updateManyCall = mockPrisma.teamCollection.updateMany.mock.calls[0][0]; + const updateManyCall = + mockPrisma.teamCollection.updateMany.mock.calls[0][0]; // FIX VERIFICATION: The query now correctly includes teamID expect(updateManyCall.where).toEqual({ diff --git a/packages/hoppscotch-backend/src/team-collection/team-collection.service.ts b/packages/hoppscotch-backend/src/team-collection/team-collection.service.ts index 3f4e6511..ba9258de 100644 --- a/packages/hoppscotch-backend/src/team-collection/team-collection.service.ts +++ b/packages/hoppscotch-backend/src/team-collection/team-collection.service.ts @@ -216,7 +216,11 @@ export class TeamCollectionService { await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockTeamCollectionByTeamAndParent(tx, teamID, parentID); + await this.prisma.lockTeamCollectionByTeamAndParent( + tx, + teamID, + parentID, + ); // Get the last order index const lastEntry = await tx.teamCollection.findFirst({ @@ -397,15 +401,18 @@ export class TeamCollectionService { * @param collectionID The collection ID * @returns An Either of the Collection details */ - async getCollection(collectionID: string, tx: Prisma.TransactionClient | null = null) { + async getCollection( + collectionID: string, + tx: Prisma.TransactionClient | null = null, + ) { try { - const teamCollection = await (tx || this.prisma).teamCollection.findUniqueOrThrow( - { - where: { - id: collectionID, - }, + const teamCollection = await ( + tx || this.prisma + ).teamCollection.findUniqueOrThrow({ + where: { + id: collectionID, }, - ); + }); return E.right(teamCollection); } catch (error) { return E.left(TEAM_COLL_NOT_FOUND); @@ -469,7 +476,11 @@ export class TeamCollectionService { teamCollection = await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockTeamCollectionByTeamAndParent(tx, teamID, parentID); + await this.prisma.lockTeamCollectionByTeamAndParent( + tx, + teamID, + parentID, + ); // fetch last collection const lastCollection = await tx.teamCollection.findFirst({ @@ -552,15 +563,19 @@ export class TeamCollectionService { await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockTeamCollectionByTeamAndParent(tx, collection.teamID, collection.parentID); + await this.prisma.lockTeamCollectionByTeamAndParent( + tx, + collection.teamID, + collection.parentID, + ); const deletedCollection = await tx.teamCollection.delete({ where: { id: collection.id }, }); - + // if collection is deleted, update siblings orderIndexes // if collection was deleted before the transaction started (race condition), do not update siblings orderIndexes - if (deletedCollection) { + if (deletedCollection) { // update siblings orderIndexes await tx.teamCollection.updateMany({ where: { @@ -623,7 +638,7 @@ export class TeamCollectionService { ); return E.right(true); - } + } /** * Change parentID of TeamCollection's @@ -638,11 +653,10 @@ export class TeamCollectionService { newParentID: string | null, ) { // fetch last collection - const lastCollectionUnderNewParent = - await tx.teamCollection.findFirst({ - where: { teamID: collection.teamID, parentID: newParentID }, - orderBy: { orderIndex: 'desc' }, - }); + const lastCollectionUnderNewParent = await tx.teamCollection.findFirst({ + where: { teamID: collection.teamID, parentID: newParentID }, + orderBy: { orderIndex: 'desc' }, + }); // decrement orderIndex of all next sibling collections from original collection await tx.teamCollection.updateMany({ @@ -736,7 +750,11 @@ export class TeamCollectionService { const collection = await this.getCollection(collectionID, tx); if (E.isLeft(collection)) return E.left(collection.left); // lock the rows of the collection and its siblings - await this.prisma.lockTeamCollectionByTeamAndParent(tx, collection.right.teamID, collection.right.parentID); + await this.prisma.lockTeamCollectionByTeamAndParent( + tx, + collection.right.teamID, + collection.right.parentID, + ); // destCollectionID == null i.e move collection to root if (!destCollectionID) { if (!collection.right.parentID) { @@ -744,7 +762,7 @@ export class TeamCollectionService { // Throw error if collection is already a root collection return E.left(TEAM_COL_ALREADY_ROOT); } - + // Change parent from child to root i.e child collection becomes a root collection // Move child collection into root and update orderIndexes for root teamCollections const updatedCollection = await this.changeParentAndUpdateOrderIndex( @@ -752,31 +770,32 @@ export class TeamCollectionService { collection.right, null, ); - if (E.isLeft(updatedCollection)) return E.left(updatedCollection.left); - + if (E.isLeft(updatedCollection)) + return E.left(updatedCollection.left); + this.pubsub.publish( `team_coll/${collection.right.teamID}/coll_moved`, updatedCollection.right, ); - + return E.right(updatedCollection.right); } - + // destCollectionID != null i.e move into another collection if (collectionID === destCollectionID) { // Throw error if collectionID and destCollectionID are the same return E.left(TEAM_COLL_DEST_SAME); } - + // Get collection details of destCollectionID const destCollection = await this.getCollection(destCollectionID, tx); if (E.isLeft(destCollection)) return E.left(TEAM_COLL_NOT_FOUND); - + // Check if collection and destCollection belong to the same user account if (collection.right.teamID !== destCollection.right.teamID) { return E.left(TEAM_COLL_NOT_SAME_TEAM); } - + // Check if collection is present on the parent tree for destCollection const checkIfParent = await this.isParent( collection.right, @@ -788,8 +807,12 @@ export class TeamCollectionService { } // lock the rows of the destination collection and its siblings - await this.prisma.lockTeamCollectionByTeamAndParent(tx, destCollection.right.teamID, destCollection.right.parentID); - + await this.prisma.lockTeamCollectionByTeamAndParent( + tx, + destCollection.right.teamID, + destCollection.right.parentID, + ); + // Change parent from null to teamCollection i.e collection becomes a child collection // Move root/child collection into another child collection and update orderIndexes of the previous parent const updatedCollection = await this.changeParentAndUpdateOrderIndex( @@ -807,11 +830,7 @@ export class TeamCollectionService { return E.right(updatedCollection.right); }); } catch (error) { - - console.error( - 'Error from TeamCollectionService.moveCollection', - error, - ); + console.error('Error from TeamCollectionService.moveCollection', error); return E.left(TEAM_COL_REORDERING_FAILED); } } @@ -823,7 +842,11 @@ export class TeamCollectionService { * @param teamID The Team ID (required when collectionID is null for root collections) * @returns Number of collections */ - getCollectionCount(collectionID: string, teamID: string, tx: Prisma.TransactionClient | null = null): Promise { + getCollectionCount( + collectionID: string, + teamID: string, + tx: Prisma.TransactionClient | null = null, + ): Promise { return (tx || this.prisma).teamCollection.count({ where: { parentID: collectionID, teamID: teamID }, }); @@ -867,7 +890,7 @@ export class TeamCollectionService { // if collection is found, update orderIndexes of siblings // if collection was deleted before the transaction started (race condition), do not update siblings orderIndexes - if(collectionInTx) { + if (collectionInTx) { // Step 1: Decrement orderIndex of all items that come after collection.orderIndex till end of list of items await tx.teamCollection.updateMany({ where: { @@ -881,7 +904,7 @@ export class TeamCollectionService { orderIndex: { decrement: 1 }, }, }); - + // Step 2: Update orderIndex of collection to length of list await tx.teamCollection.update({ where: { id: collection.right.id }, @@ -894,7 +917,6 @@ export class TeamCollectionService { }, }); } - } catch (error) { throw new ConflictException(error); } @@ -927,7 +949,11 @@ export class TeamCollectionService { await this.prisma.$transaction(async (tx) => { try { // Step 0: lock the rows - await this.prisma.lockTeamCollectionByTeamAndParent(tx, collection.right.teamID, collection.right.parentID); + await this.prisma.lockTeamCollectionByTeamAndParent( + tx, + collection.right.teamID, + collection.right.parentID, + ); const collectionInTx = await tx.teamCollection.findFirst({ where: { id: collectionID }, @@ -940,10 +966,10 @@ export class TeamCollectionService { // if collection and subsequentCollection are found, update orderIndexes of siblings // if collection or subsequentCollection was deleted before the transaction started (race condition), do not update siblings orderIndexes - if(collectionInTx && subsequentCollectionInTx) { + if (collectionInTx && subsequentCollectionInTx) { // Step 1: Determine if we are moving collection up or down the list const isMovingUp = - subsequentCollectionInTx.orderIndex < collectionInTx.orderIndex; + subsequentCollectionInTx.orderIndex < collectionInTx.orderIndex; // Step 2: Update OrderIndex of items in list depending on moving up or down const updateFrom = isMovingUp @@ -1490,7 +1516,11 @@ export class TeamCollectionService { try { await this.prisma.$transaction(async (tx) => { - await this.prisma.lockTeamCollectionByTeamAndParent(tx, teamID, parentID); + await this.prisma.lockTeamCollectionByTeamAndParent( + tx, + teamID, + parentID, + ); const collections = await tx.teamCollection.findMany({ where: { teamID, parentID }, diff --git a/packages/hoppscotch-backend/src/team-request/team-request.service.ts b/packages/hoppscotch-backend/src/team-request/team-request.service.ts index 941223c2..a0ff29ea 100644 --- a/packages/hoppscotch-backend/src/team-request/team-request.service.ts +++ b/packages/hoppscotch-backend/src/team-request/team-request.service.ts @@ -120,7 +120,9 @@ export class TeamRequestService { await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockTeamRequestByCollections(tx, dbTeamReq.teamID, [dbTeamReq.collectionID]); + await this.prisma.lockTeamRequestByCollections(tx, dbTeamReq.teamID, [ + dbTeamReq.collectionID, + ]); const deletedTeamRequest = await tx.teamRequest.delete({ where: { id: requestID }, @@ -128,7 +130,7 @@ export class TeamRequestService { // if request is deleted, update orderIndexes of siblings // if request was deleted before the transaction started (race condition), do not update siblings orderIndexes - if(deletedTeamRequest) { + if (deletedTeamRequest) { await tx.teamRequest.updateMany({ where: { collectionID: dbTeamReq.collectionID, @@ -181,7 +183,9 @@ export class TeamRequestService { dbTeamRequest = await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockTeamRequestByCollections(tx, teamID, [collectionID]); + await this.prisma.lockTeamRequestByCollections(tx, teamID, [ + collectionID, + ]); // fetch last team request const lastTeamRequest = await tx.teamRequest.findFirst({ @@ -385,7 +389,10 @@ export class TeamRequestService { * A helper function to get the number of requests in a collection * @param collectionID Collection ID to fetch */ - private async getRequestsCountInCollection(collectionID: string, tx: Prisma.TransactionClient | null = null) { + private async getRequestsCountInCollection( + collectionID: string, + tx: Prisma.TransactionClient | null = null, + ) { return (tx || this.prisma).teamRequest.count({ where: { collectionID }, }); @@ -409,7 +416,10 @@ export class TeamRequestService { E.Left | E.Right >(async (tx) => { // lock the rows - await this.prisma.lockTeamRequestByCollections(tx, request.teamID, [srcCollID, destCollID]); + await this.prisma.lockTeamRequestByCollections(tx, request.teamID, [ + srcCollID, + destCollID, + ]); request = await tx.teamRequest.findUnique({ where: { id: request.id }, @@ -422,22 +432,24 @@ export class TeamRequestService { // if request is found in transaction, update orderIndexes of siblings // if request was deleted before the transaction started (race condition), do not update siblings orderIndexes - if(request) { + if (request) { const isSameCollection = srcCollID === destCollID; const isMovingUp = nextRequest?.orderIndex < request.orderIndex; // false, if nextRequest is null - + const nextReqOrderIndex = nextRequest?.orderIndex; const reqCountInDestColl = nextRequest ? undefined : await this.getRequestsCountInCollection(destCollID, tx); - + // Updating order indexes of other requests in collection(s) if (isSameCollection) { const updateFrom = isMovingUp ? nextReqOrderIndex : request.orderIndex + 1; - const updateTo = isMovingUp ? request.orderIndex : nextReqOrderIndex; - + const updateTo = isMovingUp + ? request.orderIndex + : nextReqOrderIndex; + await tx.teamRequest.updateMany({ where: { collectionID: srcCollID, @@ -455,7 +467,7 @@ export class TeamRequestService { }, data: { orderIndex: { decrement: 1 } }, }); - + if (nextRequest) { await tx.teamRequest.updateMany({ where: { @@ -466,20 +478,21 @@ export class TeamRequestService { }); } } - + // Updating order index of the request let adjust: number; - if (isSameCollection) adjust = nextRequest ? (isMovingUp ? 0 : -1) : 0; + if (isSameCollection) + adjust = nextRequest ? (isMovingUp ? 0 : -1) : 0; else adjust = nextRequest ? 0 : 1; - + const newOrderIndex = (nextReqOrderIndex ?? reqCountInDestColl) + adjust; - + const updatedRequest = await tx.teamRequest.update({ where: { id: request.id }, data: { orderIndex: newOrderIndex, collectionID: destCollID }, }); - + return E.right(updatedRequest); } }); @@ -539,7 +552,9 @@ export class TeamRequestService { try { await this.prisma.$transaction(async (tx) => { // lock the rows - await this.prisma.lockTeamRequestByCollections(tx, teamID, [collectionID]); + await this.prisma.lockTeamRequestByCollections(tx, teamID, [ + collectionID, + ]); const teamRequests = await tx.teamRequest.findMany({ where: { teamID, collectionID }, orderBy, diff --git a/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts b/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts index 504bef06..e3870d9a 100644 --- a/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts +++ b/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts @@ -132,6 +132,7 @@ export class UserCollectionResolver { }) @UseGuards(GqlAuthGuard) async userCollection( + @GqlUser() user: AuthUser, @Args({ type: () => ID, name: 'userCollectionID', @@ -139,8 +140,10 @@ export class UserCollectionResolver { }) userCollectionID: string, ) { - const userCollection = - await this.userCollectionService.getUserCollection(userCollectionID); + const userCollection = await this.userCollectionService.getUserCollection( + userCollectionID, + user.uid, + ); if (E.isLeft(userCollection)) throwErr(userCollection.left); return { diff --git a/packages/hoppscotch-backend/src/user-collection/user-collection.service.spec.ts b/packages/hoppscotch-backend/src/user-collection/user-collection.service.spec.ts index 007d8b7e..22f32234 100644 --- a/packages/hoppscotch-backend/src/user-collection/user-collection.service.spec.ts +++ b/packages/hoppscotch-backend/src/user-collection/user-collection.service.spec.ts @@ -653,6 +653,7 @@ describe('getUserCollection', () => { const result = await userCollectionService.getUserCollection( rootRESTUserCollection.id, + user.uid, ); expect(result).toEqualRight(rootRESTUserCollection); }); @@ -661,7 +662,20 @@ describe('getUserCollection', () => { 'NotFoundError', ); - const result = await userCollectionService.getUserCollection('123'); + const result = await userCollectionService.getUserCollection( + '123', + user.uid, + ); + expect(result).toEqualLeft(USER_COLL_NOT_FOUND); + }); + test('should throw USER_COLL_NOT_FOUND when collectionID belongs to a different user', async () => { + mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValueOnce( + 'NotFoundError', + ); + const result = await userCollectionService.getUserCollection( + rootRESTUserCollection.id, + 'another-user', + ); expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); }); @@ -729,16 +743,11 @@ describe('createUserCollection', () => { expect(result).toEqualLeft(USER_COLL_SHORT_TITLE); }); - test('should throw USER_NOT_OWNER when user is not the owner of the collection', async () => { + test('should throw USER_COLLECTION_CREATION_FAILED when user is not the owner of the parent collection', async () => { mockPrisma.$transaction.mockImplementation(async (fn) => fn(mockPrisma)); jest .spyOn(userCollectionService, 'getUserCollection') - .mockResolvedValueOnce( - E.right({ - ...rootRESTUserCollection, - userUid: 'other-user-uid', - }), - ); + .mockResolvedValueOnce(E.left(USER_COLL_NOT_FOUND)); const result = await userCollectionService.createUserCollection( user, @@ -1041,23 +1050,26 @@ describe('deleteUserCollection', () => { ); expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); - test('should throw USER_NOT_OWNER when collectionID is invalid ', async () => { - // getUserCollection - mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( - rootRESTUserCollection, + test('should throw USER_COLL_NOT_FOUND when collectionID belongs to a different user', async () => { + // getUserCollection (userUid is now part of the where clause, so it rejects for wrong user) + mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValueOnce( + 'NotFoundError', ); const result = await userCollectionService.deleteUserCollection( rootRESTUserCollection.id, 'op09', ); - expect(result).toEqualLeft(USER_NOT_OWNER); + expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); test('should throw USER_COLL_REORDERING_FAILED when removeCollectionAndUpdateSiblingsOrderIndex fails', async () => { jest .spyOn(userCollectionService, 'getUserCollection') .mockResolvedValueOnce(E.right(rootRESTUserCollection)); jest - .spyOn(userCollectionService as any, 'removeCollectionAndUpdateSiblingsOrderIndex') + .spyOn( + userCollectionService as any, + 'removeCollectionAndUpdateSiblingsOrderIndex', + ) .mockResolvedValueOnce(E.left(USER_COLL_REORDERING_FAILED)); const result = await userCollectionService.deleteUserCollection( @@ -1115,11 +1127,11 @@ describe('moveUserCollection', () => { expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); - test('should throw USER_NOT_OWNER if user is not owner of collection', async () => { + test('should throw USER_COLL_NOT_FOUND if user is not owner of collection', async () => { mockPrisma.$transaction.mockImplementation(async (fn) => fn(mockPrisma)); - // getUserCollection - mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( - rootRESTUserCollection, + // getUserCollection (userUid is now part of the where clause, so it rejects for wrong user) + mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValueOnce( + 'NotFoundError', ); const result = await userCollectionService.moveUserCollection( @@ -1127,7 +1139,7 @@ describe('moveUserCollection', () => { '009', 'op09', ); - expect(result).toEqualLeft(USER_NOT_OWNER); + expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); test('should throw USER_COLL_DEST_SAME if userCollectionID and destCollectionID is the same', async () => { @@ -1183,24 +1195,23 @@ describe('moveUserCollection', () => { expect(result).toEqualLeft(USER_COLL_NOT_SAME_TYPE); }); - test('should throw USER_COLL_NOT_SAME_USER if userCollectionID and destCollectionID are not from the same user', async () => { + test('should throw USER_COLL_NOT_FOUND if destCollectionID belongs to a different user', async () => { mockPrisma.$transaction.mockImplementation(async (fn) => fn(mockPrisma)); - // getUserCollection + // getUserCollection for source collection mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( rootRESTUserCollection, ); - // getUserCollection for destCollection - mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce({ - ...childRESTUserCollection_2, - userUid: 'differentUserUid', - }); + // getUserCollection for destCollection (userUid is now part of the where clause, so it rejects for wrong user) + mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValueOnce( + 'NotFoundError', + ); const result = await userCollectionService.moveUserCollection( rootRESTUserCollection.id, childRESTUserCollection_2.id, user.uid, ); - expect(result).toEqualLeft(USER_COLL_NOT_SAME_USER); + expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); test('should throw USER_COLL_IS_PARENT_COLL if userCollectionID is parent of destCollectionID ', async () => { @@ -1416,10 +1427,10 @@ describe('updateUserCollectionOrder', () => { expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); - test('should throw USER_NOT_OWNER if userUID is of a different user', async () => { - // getUserCollection; - mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( - childRESTUserCollectionList[4], + test('should throw USER_COLL_NOT_FOUND if userUID is of a different user', async () => { + // getUserCollection (userUid is now part of the where clause, so it rejects for wrong user) + mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValueOnce( + 'NotFoundError', ); const result = await userCollectionService.updateUserCollectionOrder( @@ -1427,7 +1438,7 @@ describe('updateUserCollectionOrder', () => { null, 'op09', ); - expect(result).toEqualLeft(USER_NOT_OWNER); + expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); test('should successfully move the child user-collection to the end of the list', async () => { @@ -1803,10 +1814,13 @@ describe('FIX: updateMany queries now include userUid filter for root collection ); // Verify Alice's delete only affected Alice's collections - const aliceDeleteCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const aliceDeleteCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; expect(aliceDeleteCall.where.userUid).toBe(alice.uid); expect(aliceDeleteCall.where.parentID).toBe(null); - expect(aliceDeleteCall.where.orderIndex).toEqual({ gt: aliceCollection2.orderIndex }); + expect(aliceDeleteCall.where.orderIndex).toEqual({ + gt: aliceCollection2.orderIndex, + }); // Reset mocks for Bob's operation mockReset(mockPrisma); @@ -1831,7 +1845,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection ); // Verify Bob's reorder only affected Bob's collections - const bobReorderCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const bobReorderCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; expect(bobReorderCall.where.userUid).toBe(bob.uid); expect(bobReorderCall.where.parentID).toBe(null); }); @@ -1873,7 +1888,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection ); // Verify Alice's operation is scoped to Alice - const aliceReorderCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const aliceReorderCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; expect(aliceReorderCall.where.userUid).toBe(alice.uid); expect(aliceReorderCall.where.parentID).toBe(null); @@ -1903,7 +1919,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection ); // Verify Bob's operation is scoped to Bob - const bobReorderCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const bobReorderCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; expect(bobReorderCall.where.userUid).toBe(bob.uid); expect(bobReorderCall.where.parentID).toBe(null); }); @@ -1937,7 +1954,9 @@ describe('FIX: updateMany queries now include userUid filter for root collection .mockResolvedValueOnce(E.right(aliceChildCollection)); mockPrisma.$transaction.mockImplementation(async (fn) => fn(mockPrisma)); - mockPrisma.userCollection.findFirst.mockResolvedValueOnce(aliceCollection3); // Last root + mockPrisma.userCollection.findFirst.mockResolvedValueOnce( + aliceCollection3, + ); // Last root mockPrisma.userCollection.update.mockResolvedValueOnce({ ...aliceChildCollection, parentID: null, @@ -1952,7 +1971,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection ); // Verify Alice's move-to-root only affects Alice's collections - const aliceMoveCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const aliceMoveCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; expect(aliceMoveCall.where.userUid).toBe(alice.uid); expect(aliceMoveCall.where.parentID).toBe(aliceChildCollection.parentID); @@ -1976,10 +1996,13 @@ describe('FIX: updateMany queries now include userUid filter for root collection ); // Verify Bob's delete only affects Bob's collections - const bobDeleteCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const bobDeleteCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; expect(bobDeleteCall.where.userUid).toBe(bob.uid); expect(bobDeleteCall.where.parentID).toBe(null); - expect(bobDeleteCall.where.orderIndex).toEqual({ gt: bobCollection2.orderIndex }); + expect(bobDeleteCall.where.orderIndex).toEqual({ + gt: bobCollection2.orderIndex, + }); }); }); @@ -2016,7 +2039,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection ); expect(mockPrisma.userCollection.updateMany).toHaveBeenCalled(); - const updateManyCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const updateManyCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; // FIXED: The where clause now includes userUid to prevent cross-user data corruption expect(updateManyCall.where).toEqual({ @@ -2061,7 +2085,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection user.uid, ); - const updateManyCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const updateManyCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; // FIXED: Now includes userUid - only affects current user's root collections expect(updateManyCall.where).toEqual({ @@ -2117,7 +2142,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection user.uid, ); - const updateManyCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const updateManyCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; // FIXED: Now includes userUid - only affects current user's root collections expect(updateManyCall.where).toEqual({ @@ -2159,7 +2185,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection user.uid, ); - const updateManyCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const updateManyCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; // FIXED: Now includes userUid - only affects current user's root collections expect(updateManyCall.where).toEqual({ diff --git a/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts b/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts index 4ab6e615..ad399092 100644 --- a/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts +++ b/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts @@ -175,11 +175,17 @@ export class UserCollectionService { * @param collectionID The collection ID * @returns An Either of the Collection details */ - async getUserCollection(collectionID: string, tx: Prisma.TransactionClient | null = null) { + async getUserCollection( + collectionID: string, + userUid: string, + tx: Prisma.TransactionClient | null = null, + ) { try { - const userCollection = await (tx || this.prisma).userCollection.findUniqueOrThrow( - { where: { id: collectionID } }, - ); + const userCollection = await ( + tx || this.prisma + ).userCollection.findUniqueOrThrow({ + where: { id: collectionID, userUid }, + }); return E.right(userCollection); } catch (error) { return E.left(USER_COLL_NOT_FOUND); @@ -212,21 +218,19 @@ export class UserCollectionService { data = jsonReq.right; } - - let userCollection: UserCollection = null; try { userCollection = await this.prisma.$transaction(async (tx) => { try { // If creating a child collection if (parentID !== null) { - const parentCollection = await this.getUserCollection(parentID, tx); + const parentCollection = await this.getUserCollection( + parentID, + user.uid, + tx, + ); if (E.isLeft(parentCollection)) throw Error(parentCollection.left); - // Check to see if parentUserCollectionID belongs to this User - if (parentCollection.right.userUid !== user.uid) - throw Error(USER_NOT_OWNER); - // Check to see if parent collection is of the same type of new collection being created if (parentCollection.right.type !== type) throw Error(USER_COLL_NOT_SAME_TYPE); @@ -234,7 +238,6 @@ export class UserCollectionService { // lock the rows await this.prisma.lockUserCollectionByParent(tx, user.uid, parentID); - // fetch last user collection const lastUserCollection = await tx.userCollection.findFirst({ where: { userUid: user.uid, parentID }, @@ -391,12 +394,9 @@ export class UserCollectionService { */ async deleteUserCollection(collectionID: string, userID: string) { // Get collection details of collectionID - const collection = await this.getUserCollection(collectionID); + const collection = await this.getUserCollection(collectionID, userID); if (E.isLeft(collection)) return E.left(USER_COLL_NOT_FOUND); - // Check to see is the collection belongs to the user - if (collection.right.userUid !== userID) return E.left(USER_NOT_OWNER); - // Delete all child collections and requests in the collection const isDeleted = await this.removeCollectionAndUpdateSiblingsOrderIndex( collection.right, @@ -427,11 +427,10 @@ export class UserCollectionService { newParentID: string | null, ) { // fetch last collection - const lastCollectionUnderNewParent = - await tx.userCollection.findFirst({ - where: { userUid: collection.userUid, parentID: newParentID }, - orderBy: { orderIndex: 'desc' }, - }); + const lastCollectionUnderNewParent = await tx.userCollection.findFirst({ + where: { userUid: collection.userUid, parentID: newParentID }, + orderBy: { orderIndex: 'desc' }, + }); // update collection's parentID and orderIndex const updatedCollection = await tx.userCollection.update({ @@ -483,6 +482,7 @@ export class UserCollectionService { // Get collection details of collection one step above in the tree i.e the parent collection const parentCollection = await this.getUserCollection( destCollection.parentID, + destCollection.userUid, tx, ); if (E.isLeft(parentCollection)) { @@ -514,7 +514,11 @@ export class UserCollectionService { await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockUserCollectionByParent(tx, userID, collection.parentID); + await this.prisma.lockUserCollectionByParent( + tx, + userID, + collection.parentID, + ); const deletedCollection = await tx.userCollection.delete({ where: { id: collection.id }, @@ -577,12 +581,13 @@ export class UserCollectionService { try { return await this.prisma.$transaction(async (tx) => { // Get collection details of collectionID - const collection = await this.getUserCollection(userCollectionID, tx); + const collection = await this.getUserCollection( + userCollectionID, + userID, + tx, + ); if (E.isLeft(collection)) return E.left(USER_COLL_NOT_FOUND); - - // Check to see is the collection belongs to the user - if (collection.right.userUid !== userID) return E.left(USER_NOT_OWNER); - + // destCollectionID == null i.e move collection to root if (!destCollectionID) { if (!collection.right.parentID) { @@ -590,7 +595,7 @@ export class UserCollectionService { // Throw error if collection is already a root collection return E.left(USER_COLL_ALREADY_ROOT); } - + // Change parent from child to root i.e child collection becomes a root collection // Move child collection into root and update orderIndexes for child userCollections const updatedCollection = await this.changeParentAndUpdateOrderIndex( @@ -598,36 +603,36 @@ export class UserCollectionService { collection.right, null, ); - if (E.isLeft(updatedCollection)) return E.left(updatedCollection.left); - + if (E.isLeft(updatedCollection)) + return E.left(updatedCollection.left); + this.pubsub.publish( `user_coll/${collection.right.userUid}/moved`, this.cast(updatedCollection.right), ); - + return E.right(this.cast(updatedCollection.right)); } - + // destCollectionID != null i.e move into another collection if (userCollectionID === destCollectionID) { // Throw error if collectionID and destCollectionID are the same return E.left(USER_COLL_DEST_SAME); } - + // Get collection details of destCollectionID - const destCollection = await this.getUserCollection(destCollectionID, tx); + const destCollection = await this.getUserCollection( + destCollectionID, + userID, + tx, + ); if (E.isLeft(destCollection)) return E.left(USER_COLL_NOT_FOUND); - + // Check if collection and destCollection belong to the same collection type if (collection.right.type !== destCollection.right.type) { return E.left(USER_COLL_NOT_SAME_TYPE); } - - // Check if collection and destCollection belong to the same user account - if (collection.right.userUid !== destCollection.right.userUid) { - return E.left(USER_COLL_NOT_SAME_USER); - } - + // Check if collection is present on the parent tree for destCollection const checkIfParent = await this.isParent( collection.right, @@ -637,7 +642,7 @@ export class UserCollectionService { if (O.isNone(checkIfParent)) { return E.left(USER_COLL_IS_PARENT_COLL); } - + // Change parent from null to teamCollection i.e collection becomes a child collection // Move root/child collection into another child collection and update orderIndexes of the previous parent const updatedCollection = await this.changeParentAndUpdateOrderIndex( @@ -646,16 +651,15 @@ export class UserCollectionService { destCollection.right.id, ); if (E.isLeft(updatedCollection)) return E.left(updatedCollection.left); - + this.pubsub.publish( `user_coll/${collection.right.userUid}/moved`, this.cast(updatedCollection.right), ); - + return E.right(this.cast(updatedCollection.right)); }); } catch (error) { - console.error( 'Error from UserCollectionService.moveUserCollection', error, @@ -671,7 +675,11 @@ export class UserCollectionService { * @param userUid The User UID * @returns Number of collections */ - getCollectionCount(collectionID: string, userUid: string, tx: Prisma.TransactionClient | null = null): Promise { + getCollectionCount( + collectionID: string, + userUid: string, + tx: Prisma.TransactionClient | null = null, + ): Promise { return (tx || this.prisma).userCollection.count({ where: { parentID: collectionID, @@ -698,19 +706,20 @@ export class UserCollectionService { return E.left(USER_COLL_SAME_NEXT_COLL); // Get collection details of collectionID - const collection = await this.getUserCollection(collectionID); + const collection = await this.getUserCollection(collectionID, userID); if (E.isLeft(collection)) return E.left(USER_COLL_NOT_FOUND); - // Check to see is the collection belongs to the user - if (collection.right.userUid !== userID) return E.left(USER_NOT_OWNER); - if (!nextCollectionID) { // nextCollectionID == null i.e move collection to the end of the list try { await this.prisma.$transaction(async (tx) => { try { // Step 0: lock the rows - await this.prisma.lockUserCollectionByParent(tx, userID, collection.right.parentID); + await this.prisma.lockUserCollectionByParent( + tx, + userID, + collection.right.parentID, + ); const collectionInTx = await tx.userCollection.findFirst({ where: { id: collectionID }, @@ -719,7 +728,7 @@ export class UserCollectionService { // if collection is found, update orderIndexes of siblings // if collection was deleted before the transaction started (race condition), do not update siblings orderIndexes - if(collectionInTx) { + if (collectionInTx) { // Step 1: Decrement orderIndex of all items that come after collection.orderIndex till end of list of items await tx.userCollection.updateMany({ where: { @@ -729,7 +738,7 @@ export class UserCollectionService { }, data: { orderIndex: { decrement: 1 } }, }); - + // Step 2: Update orderIndex of collection to length of list await tx.userCollection.update({ where: { id: collection.right.id }, @@ -742,7 +751,6 @@ export class UserCollectionService { }, }); } - } catch (error) { throw new ConflictException(error); } @@ -764,7 +772,10 @@ export class UserCollectionService { // nextCollectionID != null i.e move to a certain position // Get collection details of nextCollectionID - const subsequentCollection = await this.getUserCollection(nextCollectionID); + const subsequentCollection = await this.getUserCollection( + nextCollectionID, + userID, + ); if (E.isLeft(subsequentCollection)) return E.left(USER_COLL_NOT_FOUND); if (collection.right.userUid !== subsequentCollection.right.userUid) @@ -779,7 +790,11 @@ export class UserCollectionService { await this.prisma.$transaction(async (tx) => { try { // Step 0: lock the rows - await this.prisma.lockUserCollectionByParent(tx, userID, subsequentCollection.right.parentID); + await this.prisma.lockUserCollectionByParent( + tx, + userID, + subsequentCollection.right.parentID, + ); // subsequentCollectionInTx and subsequentCollection are same, just to make sure, orderIndex value is concrete const collectionInTx = await tx.userCollection.findFirst({ @@ -793,20 +808,20 @@ export class UserCollectionService { // if collection and subsequentCollection are found, update orderIndexes of siblings // if collection or subsequentCollection was deleted before the transaction started (race condition), do not update siblings orderIndexes - if(collectionInTx && subsequentCollectionInTx) { + if (collectionInTx && subsequentCollectionInTx) { // Step 1: Determine if we are moving collection up or down the list const isMovingUp = subsequentCollectionInTx.orderIndex < collectionInTx.orderIndex; - + // Step 2: Update OrderIndex of items in list depending on moving up or down const updateFrom = isMovingUp ? subsequentCollectionInTx.orderIndex : collectionInTx.orderIndex + 1; - + const updateTo = isMovingUp ? collectionInTx.orderIndex - 1 : subsequentCollectionInTx.orderIndex - 1; - + await tx.userCollection.updateMany({ where: { userUid: collection.right.userUid, @@ -817,7 +832,7 @@ export class UserCollectionService { orderIndex: isMovingUp ? { increment: 1 } : { decrement: 1 }, }, }); - + // Step 3: Update OrderIndex of collection await tx.userCollection.update({ where: { id: collection.right.id }, @@ -828,7 +843,6 @@ export class UserCollectionService { }, }); } - } catch (error) { throw new ConflictException(error); } @@ -860,7 +874,7 @@ export class UserCollectionService { collectionID: string, ): Promise | E.Right> { // Get Collection details - const collection = await this.getUserCollection(collectionID); + const collection = await this.getUserCollection(collectionID, userUID); if (E.isLeft(collection)) return E.left(collection.left); const childrenCollectionObjects: CollectionFolder[] = []; @@ -955,7 +969,10 @@ export class UserCollectionService { // If collectionID is not null, return JSON stringified data for specific collection if (collectionID) { // Get Details of collection - const parentCollection = await this.getUserCollection(collectionID); + const parentCollection = await this.getUserCollection( + collectionID, + userUID, + ); if (E.isLeft(parentCollection)) return E.left(parentCollection.left); if (parentCollection.right.type !== reqType) @@ -1083,13 +1100,12 @@ export class UserCollectionService { // Check to see if destCollectionID belongs to this User if (destCollectionID) { - const parentCollection = await this.getUserCollection(destCollectionID); + const parentCollection = await this.getUserCollection( + destCollectionID, + userID, + ); if (E.isLeft(parentCollection)) return E.left(parentCollection.left); - // Check to see if parentUserCollectionID belongs to this User - if (parentCollection.right.userUid !== userID) - return E.left(USER_NOT_OWNER); - // Check to see if parent collection is of the same type of new collection being created if (parentCollection.right.type !== reqType) return E.left(USER_COLL_NOT_SAME_TYPE); @@ -1101,7 +1117,11 @@ export class UserCollectionService { await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockUserCollectionByParent(tx, userID, destCollectionID); + await this.prisma.lockUserCollectionByParent( + tx, + userID, + destCollectionID, + ); // Get the last order index const lastCollection = await tx.userCollection.findFirst({ @@ -1147,6 +1167,7 @@ export class UserCollectionService { if (isCollectionDuplication) { const duplicatedCollectionData = await this.fetchCollectionData( userCollections[0].id, + userID, ); if (E.isRight(duplicatedCollectionData)) { this.pubsub.publish( @@ -1233,10 +1254,9 @@ export class UserCollectionService { userID: string, reqType: DBReqType, ) { - const collection = await this.getUserCollection(collectionID); + const collection = await this.getUserCollection(collectionID, userID); if (E.isLeft(collection)) return E.left(USER_COLL_NOT_FOUND); - if (collection.right.userUid !== userID) return E.left(USER_NOT_OWNER); if (collection.right.type !== reqType) return E.left(USER_COLL_NOT_SAME_TYPE); @@ -1272,8 +1292,9 @@ export class UserCollectionService { */ private async fetchCollectionData( collectionID: string, + userID: string, ): Promise | E.Right> { - const collection = await this.getUserCollection(collectionID); + const collection = await this.getUserCollection(collectionID, userID); if (E.isLeft(collection)) return E.left(collection.left); const { id, title, data, type, parentID, userUid } = collection.right; @@ -1291,7 +1312,7 @@ export class UserCollectionService { ]); const childCollectionDataList = await Promise.all( - childCollections.map(({ id }) => this.fetchCollectionData(id)), + childCollections.map(({ id }) => this.fetchCollectionData(id, userID)), ); const failedChildData = childCollectionDataList.find(E.isLeft); diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts index 6c5128ad..e0559a20 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts @@ -104,7 +104,7 @@ export class UserEnvironmentsResolver { id, name, variables, - user + user, ); if (E.isLeft(userEnvironment)) throwErr(userEnvironment.left); return userEnvironment.right; diff --git a/packages/hoppscotch-backend/src/user-request/user-request.service.spec.ts b/packages/hoppscotch-backend/src/user-request/user-request.service.spec.ts index ff604517..a4e6f9d7 100644 --- a/packages/hoppscotch-backend/src/user-request/user-request.service.spec.ts +++ b/packages/hoppscotch-backend/src/user-request/user-request.service.spec.ts @@ -849,6 +849,7 @@ describe('UserRequestService', () => { destCollID, userRequests[0], userRequests[1], + user, ); expect(result).resolves.toEqualRight(true); diff --git a/packages/hoppscotch-backend/src/user-request/user-request.service.ts b/packages/hoppscotch-backend/src/user-request/user-request.service.ts index 2f45c60a..27f069a0 100644 --- a/packages/hoppscotch-backend/src/user-request/user-request.service.ts +++ b/packages/hoppscotch-backend/src/user-request/user-request.service.ts @@ -8,7 +8,6 @@ import { UserRequest as DbUserRequest, } from 'src/generated/prisma/client'; import { - USER_COLLECTION_NOT_FOUND, USER_REQUEST_CREATION_FAILED, USER_REQUEST_INVALID_TYPE, USER_REQUEST_NOT_FOUND, @@ -99,7 +98,10 @@ export class UserRequestService { * @param user User who owns the collection * @returns Number of requests in the collection */ - getRequestsCountInCollection(collectionID: string, tx: Prisma.TransactionClient | null = null): Promise { + getRequestsCountInCollection( + collectionID: string, + tx: Prisma.TransactionClient | null = null, + ): Promise { return (tx || this.prisma).userRequest.count({ where: { collectionID }, }); @@ -124,13 +126,12 @@ export class UserRequestService { const jsonRequest = stringToJson(request); if (E.isLeft(jsonRequest)) return E.left(jsonRequest.left); - const collection = - await this.userCollectionService.getUserCollection(collectionID); + const collection = await this.userCollectionService.getUserCollection( + collectionID, + user.uid, + ); if (E.isLeft(collection)) return E.left(collection.left); - if (collection.right.userUid !== user.uid) - return E.left(USER_COLLECTION_NOT_FOUND); - if (collection.right.type !== ReqType[type]) return E.left(USER_REQUEST_INVALID_TYPE); @@ -139,7 +140,9 @@ export class UserRequestService { newRequest = await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockUserRequestByCollections(tx, user.uid, [collectionID]); + await this.prisma.lockUserRequestByCollections(tx, user.uid, [ + collectionID, + ]); // fetch last user request const lastUserRequest = await tx.userRequest.findFirst({ @@ -235,13 +238,15 @@ export class UserRequestService { await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockUserRequestByCollections(tx, user.uid, [request.collectionID]); + await this.prisma.lockUserRequestByCollections(tx, user.uid, [ + request.collectionID, + ]); const deletedRequest = await tx.userRequest.delete({ where: { id } }); // if request is found, update orderIndexes of siblings // if request was deleted before the transaction started (race condition), do not update siblings orderIndexes - if(deletedRequest) { + if (deletedRequest) { await tx.userRequest.updateMany({ where: { collectionID: request.collectionID, @@ -298,6 +303,7 @@ export class UserRequestService { destCollID, dbRequest, dbNextRequest, + user, ); if (E.isLeft(isTypeValidate)) return E.left(isTypeValidate.left); @@ -332,10 +338,11 @@ export class UserRequestService { destCollID, request, nextRequest, + user: AuthUser, ) { const collections = await Promise.all([ - this.userCollectionService.getUserCollection(srcCollID), - this.userCollectionService.getUserCollection(destCollID), + this.userCollectionService.getUserCollection(srcCollID, user.uid), + this.userCollectionService.getUserCollection(destCollID, user.uid), ]); const srcColl = collections[0]; @@ -416,7 +423,10 @@ export class UserRequestService { E.Left | E.Right >(async (tx) => { // lock the rows - await this.prisma.lockUserRequestByCollections(tx, request.userUid, [srcCollID, destCollID]); + await this.prisma.lockUserRequestByCollections(tx, request.userUid, [ + srcCollID, + destCollID, + ]); request = await tx.userRequest.findUnique({ where: { id: request.id }, @@ -429,22 +439,24 @@ export class UserRequestService { // Check again if request is found in transaction, update orderIndexes of siblings // if request was deleted before the transaction started (race condition), do not update siblings orderIndexes - if(request) { + if (request) { const isSameCollection = srcCollID === destCollID; const isMovingUp = nextRequest?.orderIndex < request.orderIndex; // false, if nextRequest is null - + const nextReqOrderIndex = nextRequest?.orderIndex; const reqCountInDestColl = nextRequest ? undefined : await this.getRequestsCountInCollection(destCollID); - + // Updating order indexes of other requests in collection(s) if (isSameCollection) { const updateFrom = isMovingUp ? nextReqOrderIndex : request.orderIndex + 1; - const updateTo = isMovingUp ? request.orderIndex : nextReqOrderIndex; - + const updateTo = isMovingUp + ? request.orderIndex + : nextReqOrderIndex; + await tx.userRequest.updateMany({ where: { collectionID: srcCollID, @@ -462,7 +474,7 @@ export class UserRequestService { }, data: { orderIndex: { decrement: 1 } }, }); - + if (nextRequest) { await tx.userRequest.updateMany({ where: { @@ -473,15 +485,16 @@ export class UserRequestService { }); } } - + // Updating order index of the request let adjust: number; - if (isSameCollection) adjust = nextRequest ? (isMovingUp ? 0 : -1) : 0; + if (isSameCollection) + adjust = nextRequest ? (isMovingUp ? 0 : -1) : 0; else adjust = nextRequest ? 0 : 1; - + const newOrderIndex = (nextReqOrderIndex ?? reqCountInDestColl) + adjust; - + const updatedRequest = await tx.userRequest.update({ where: { id: request.id }, data: { orderIndex: newOrderIndex, collectionID: destCollID }, @@ -520,7 +533,9 @@ export class UserRequestService { try { await this.prisma.$transaction(async (tx) => { - await this.prisma.lockUserRequestByCollections(tx, userUid, [collectionID]); + await this.prisma.lockUserRequestByCollections(tx, userUid, [ + collectionID, + ]); const userRequests = await tx.userRequest.findMany({ where: { userUid, collectionID },