From f5da1c21d8766a0c48a23cf76f24059e944e3b1b Mon Sep 17 00:00:00 2001 From: constf03 Date: Thu, 27 Aug 2026 17:06:24 +0300 Subject: [PATCH 1/8] add helper method for retrieving Stock's clan_id. --- src/clanInventory/stock/stock.service.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/clanInventory/stock/stock.service.ts b/src/clanInventory/stock/stock.service.ts index 24c5ca07f..d96bf1adb 100644 --- a/src/clanInventory/stock/stock.service.ts +++ b/src/clanInventory/stock/stock.service.ts @@ -116,6 +116,23 @@ export class StockService { return [{ ...stockObject, FleaMarketItem: fleaMarketItems ?? [] }, null]; } + /** + * Retrieves the clan ID that owns the Stock. + * + * @param _id - The Mongo _id of the Clan Stock. + * @returns A promise that resolves to a tuple where the first element is the clan ID (string) or null and the second element is either null or an array of ServiceErrors. + */ + async getStockClanId( + _id: string, + ): Promise<[string | null, ServiceError[] | null]> { + const [stock, errors] = await this.basicService.readOneById(_id, { + select: ['clan_id'], + }); + if (errors) return [null, errors]; + + return [stock.clan_id.toString(), null]; + } + /** * Reads all Items stored in the specified Stock. * From 45058b74f4a54676b586d574e8f78b4ada7da37a Mon Sep 17 00:00:00 2001 From: constf03 Date: Fri, 28 Aug 2026 14:53:33 +0300 Subject: [PATCH 2/8] restrict get /item:id to logged in player's clan --- src/clanInventory/item/item.controller.ts | 24 +++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/clanInventory/item/item.controller.ts b/src/clanInventory/item/item.controller.ts index 967a22f77..d5943663a 100644 --- a/src/clanInventory/item/item.controller.ts +++ b/src/clanInventory/item/item.controller.ts @@ -87,12 +87,32 @@ export class ItemController { dto: ItemDto, modelName: ModelName.ITEM, }, - errors: [400, 404], + errors: [400, 403, 404], }) @Get('/:_id') @Authorize({ action: Action.read, subject: ItemDto }) @UniformResponse(ModelName.ITEM) - public get(@Param() param: _idDto) { + public async get(@Param() param: _idDto, @LoggedUser() user: User) { + const [itemClan_id, errors] = await this.itemHelper.getItemClanId( + param._id, + ); + if (errors || !itemClan_id) return [null, errors]; + + const playerClan = await this.playerModel.findById(user.player_id); + + if (playerClan.clan_id.toString() !== itemClan_id) + return [ + null, + [ + new APIError({ + reason: APIErrorReason.NOT_AUTHORIZED, + field: '_id', + value: param._id, + message: "Cannot view another Clan's Items", + }), + ], + ]; + return this.itemService.readOneById(param._id); } From 4a040c40bf18f3aad6880832ed78d149d5e48d1a Mon Sep 17 00:00:00 2001 From: constf03 Date: Fri, 28 Aug 2026 14:59:52 +0300 Subject: [PATCH 3/8] restrict stock reading to logged in player scope --- src/clanInventory/stock/stock.controller.ts | 36 ++++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/clanInventory/stock/stock.controller.ts b/src/clanInventory/stock/stock.controller.ts index 0c9efe50d..223d3f096 100644 --- a/src/clanInventory/stock/stock.controller.ts +++ b/src/clanInventory/stock/stock.controller.ts @@ -1,10 +1,15 @@ import { Controller, Get, Param, ParseIntPipe, Query } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; import { StockService } from './stock.service'; import { StockDto } from './dto/stock.dto'; import { ItemDto } from '../item/dto/item.dto'; import { User } from '../../auth/user'; +import { Player } from '../../player/schemas/player.schema'; import { Authorize } from '../../authorization/decorator/Authorize'; import { Action } from '../../authorization/enum/action.enum'; +import { APIError } from '../../common/controller/APIError'; +import { APIErrorReason } from '../../common/controller/APIErrorReason'; import { GetAllQuery } from '../../common/decorator/param/GetAllQuery'; import { LoggedUser } from '../../common/decorator/param/LoggedUser.decorator'; import { UniformResponse } from '../../common/decorator/response/UniformResponse'; @@ -19,12 +24,15 @@ import { Environment } from '../../common/enum/environment.enum'; @Controller('stock') export class StockController { - public constructor(private readonly service: StockService) {} + public constructor( + private readonly service: StockService, + @InjectModel(ModelName.PLAYER) private readonly playerModel: Model, + ) {} /** - * Get stock items by Stock _id + * Get logged-in Player's Clan stock items by Stock _id * - * @remarks Returns the list of Items currently stored in the Stock with the given _id. + * @remarks Returns the list of Items currently stored in the logged-in Player's Clan Stock with the given _id. */ @ApiResponseDescription({ success: { @@ -32,12 +40,30 @@ export class StockController { modelName: ModelName.ITEM, returnsArray: true, }, - errors: [400, 401, 404], + errors: [400, 401, 403, 404], }) @Get('/:_id') @Authorize({ action: Action.read, subject: StockDto }) @UniformResponse(ModelName.ITEM) - public get(@Param() param: _idDto) { + public async get(@Param() param: _idDto, @LoggedUser() user: User) { + const [stockClan_id, errors] = await this.service.getStockClanId(param._id); + if (errors || !stockClan_id) return [null, errors]; + + const playerClan = await this.playerModel.findById(user.player_id); + + if (playerClan.clan_id.toString() !== stockClan_id) + return [ + null, + [ + new APIError({ + reason: APIErrorReason.NOT_AUTHORIZED, + field: '_id', + value: param._id, + message: "Cannot view another Clan's Stock", + }), + ], + ]; + return this.service.readItemsByStockId(param._id); } From ce01ce91c237c0b5744862f9d2196ac2bbf3b12a Mon Sep 17 00:00:00 2001 From: constf03 Date: Fri, 28 Aug 2026 15:03:15 +0300 Subject: [PATCH 4/8] add extra check to prevent cross clan room update via soulHome_id --- src/clanInventory/room/room.controller.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/clanInventory/room/room.controller.ts b/src/clanInventory/room/room.controller.ts index 8df6f4752..ddd829f14 100644 --- a/src/clanInventory/room/room.controller.ts +++ b/src/clanInventory/room/room.controller.ts @@ -130,6 +130,21 @@ export class RoomController { ) @UniformResponse() public async update(@Body() body: UpdateRoomDto) { + const hasSoulHome_id = 'soulHome_id' in body; + + if (hasSoulHome_id) { + return [ + null, + [ + new APIError({ + reason: APIErrorReason.BAD_REQUEST, + field: 'soulHome_id', + message: 'soulHome_id cannot be included', + }), + ], + ]; + } + const [, errors] = await this.service.updateSoulHomeRooms(body); if (errors) return [null, errors]; } From 7e801d0032cea2b0c0e4918458c4988a4e7ceb9f Mon Sep 17 00:00:00 2001 From: constf03 Date: Sat, 5 Sep 2026 14:41:33 +0300 Subject: [PATCH 5/8] Revert "add extra check to prevent cross clan room update via soulHome_id" This reverts commit ce01ce91c237c0b5744862f9d2196ac2bbf3b12a. --- src/clanInventory/room/room.controller.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/clanInventory/room/room.controller.ts b/src/clanInventory/room/room.controller.ts index ddd829f14..8df6f4752 100644 --- a/src/clanInventory/room/room.controller.ts +++ b/src/clanInventory/room/room.controller.ts @@ -130,21 +130,6 @@ export class RoomController { ) @UniformResponse() public async update(@Body() body: UpdateRoomDto) { - const hasSoulHome_id = 'soulHome_id' in body; - - if (hasSoulHome_id) { - return [ - null, - [ - new APIError({ - reason: APIErrorReason.BAD_REQUEST, - field: 'soulHome_id', - message: 'soulHome_id cannot be included', - }), - ], - ]; - } - const [, errors] = await this.service.updateSoulHomeRooms(body); if (errors) return [null, errors]; } From f58917f1571513c8c13b8e7ca4b24b906a6fb188 Mon Sep 17 00:00:00 2001 From: constf03 Date: Sat, 5 Sep 2026 16:18:47 +0300 Subject: [PATCH 6/8] add unit test for getStockClanId --- .../stock/StockService/getStockClanId.test.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/__tests__/clanInventory/stock/StockService/getStockClanId.test.ts diff --git a/src/__tests__/clanInventory/stock/StockService/getStockClanId.test.ts b/src/__tests__/clanInventory/stock/StockService/getStockClanId.test.ts new file mode 100644 index 000000000..cfd60b0dc --- /dev/null +++ b/src/__tests__/clanInventory/stock/StockService/getStockClanId.test.ts @@ -0,0 +1,78 @@ +import { ObjectId } from 'mongodb'; +import ClanInventoryBuilderFactory from '../../data/clanInventoryBuilderFactory'; +import StockModule from '../../modules/stock.module'; +import { StockService } from '../../../../clanInventory/stock/stock.service'; +import { getNonExisting_id } from '../../../test_utils/util/getNonExisting_id'; + +describe('StockService.getStockClanId() test suite', () => { + let stockService: StockService; + const stockBuilder = ClanInventoryBuilderFactory.getBuilder('Stock'); + const stockModel = StockModule.getStockModel(); + + beforeEach(async () => { + stockService = await StockModule.getStockService(); + }); + + it('Should find clan _id of the stock', async () => { + const clan_id = new ObjectId(); + const stockToCreate = stockBuilder.setClanId(clan_id).build(); + const createdStock = await stockModel.create(stockToCreate); + + const [stockClan_id, errors] = await stockService.getStockClanId( + createdStock._id, + ); + + expect(errors).toBeNull(); + expect(stockClan_id).toBe(clan_id.toString()); + }); + + it('Should return the clan _id as a string', async () => { + const clan_id = new ObjectId(); + const stockToCreate = stockBuilder.setClanId(clan_id).build(); + const createdStock = await stockModel.create(stockToCreate); + + const [stockClan_id] = await stockService.getStockClanId(createdStock._id); + + expect(typeof stockClan_id).toBe('string'); + }); + + it('Should find clan _id of the specified stock only', async () => { + const clan1_id = new ObjectId(); + const clan2_id = new ObjectId(); + + const createdStock1 = await stockModel.create( + stockBuilder.setClanId(clan1_id).build(), + ); + const createdStock2 = await stockModel.create( + stockBuilder.setClanId(clan2_id).build(), + ); + + const [stock1Clan_id, stock1Errors] = await stockService.getStockClanId( + createdStock1._id, + ); + const [stock2Clan_id, stock2Errors] = await stockService.getStockClanId( + createdStock2._id, + ); + + expect(stock1Errors).toBeNull(); + expect(stock2Errors).toBeNull(); + expect(stock1Clan_id).toBe(clan1_id.toString()); + expect(stock2Clan_id).toBe(clan2_id.toString()); + }); + + it('Should return NOT_FOUND SE if stock with provided _id does not exists', async () => { + const [stockClan_id, errors] = + await stockService.getStockClanId(getNonExisting_id()); + + expect(stockClan_id).toBeNull(); + expect(errors).toContainSE_NOT_FOUND(); + }); + + it('Should return VALIDATION SE if provided _id param is not a mongo id', async () => { + const [stockClan_id, errors] = + await stockService.getStockClanId('not-mongo-id'); + + expect(stockClan_id).toBeNull(); + expect(errors).toContainSE_VALIDATION(); + }); +}); From ba1acc92f85b5aa7c13a0ce283a7e9af82b36cdb Mon Sep 17 00:00:00 2001 From: constf03 Date: Sat, 5 Sep 2026 16:30:04 +0300 Subject: [PATCH 7/8] "empty" commit (fix stuck ci/cd) --- .../clanInventory/stock/StockService/getStockClanId.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/clanInventory/stock/StockService/getStockClanId.test.ts b/src/__tests__/clanInventory/stock/StockService/getStockClanId.test.ts index cfd60b0dc..cbdde5a65 100644 --- a/src/__tests__/clanInventory/stock/StockService/getStockClanId.test.ts +++ b/src/__tests__/clanInventory/stock/StockService/getStockClanId.test.ts @@ -1,8 +1,8 @@ import { ObjectId } from 'mongodb'; import ClanInventoryBuilderFactory from '../../data/clanInventoryBuilderFactory'; import StockModule from '../../modules/stock.module'; -import { StockService } from '../../../../clanInventory/stock/stock.service'; import { getNonExisting_id } from '../../../test_utils/util/getNonExisting_id'; +import { StockService } from '../../../../clanInventory/stock/stock.service'; describe('StockService.getStockClanId() test suite', () => { let stockService: StockService; From e6caa01571507fa7c1d3904bfcb30401bf77230d Mon Sep 17 00:00:00 2001 From: constf03 Date: Tue, 8 Sep 2026 12:59:02 +0300 Subject: [PATCH 8/8] "empty commit" (fix stuck ci/cd) --- .../clanInventory/stock/StockService/getStockClanId.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/clanInventory/stock/StockService/getStockClanId.test.ts b/src/__tests__/clanInventory/stock/StockService/getStockClanId.test.ts index cbdde5a65..cfd60b0dc 100644 --- a/src/__tests__/clanInventory/stock/StockService/getStockClanId.test.ts +++ b/src/__tests__/clanInventory/stock/StockService/getStockClanId.test.ts @@ -1,8 +1,8 @@ import { ObjectId } from 'mongodb'; import ClanInventoryBuilderFactory from '../../data/clanInventoryBuilderFactory'; import StockModule from '../../modules/stock.module'; -import { getNonExisting_id } from '../../../test_utils/util/getNonExisting_id'; import { StockService } from '../../../../clanInventory/stock/stock.service'; +import { getNonExisting_id } from '../../../test_utils/util/getNonExisting_id'; describe('StockService.getStockClanId() test suite', () => { let stockService: StockService;