From 8fae03f88af32e58f4593941efb12135c51a7350 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 4 Sep 2026 08:29:41 +0200 Subject: [PATCH] Harden validation of assetClasses and dataSource in endpoints with filters --- apps/api/src/app/endpoints/ai/ai.controller.ts | 5 ++++- apps/api/src/app/endpoints/ai/ai.module.ts | 2 ++ apps/api/src/app/endpoints/mcp/mcp.controller.ts | 4 ++-- apps/api/src/dtos/filter.dto.ts | 16 +++++++++++----- apps/api/src/services/api/api.service.ts | 6 +++--- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/apps/api/src/app/endpoints/ai/ai.controller.ts b/apps/api/src/app/endpoints/ai/ai.controller.ts index 1a3fd3ba2a..9691e211de 100644 --- a/apps/api/src/app/endpoints/ai/ai.controller.ts +++ b/apps/api/src/app/endpoints/ai/ai.controller.ts @@ -1,6 +1,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { FilterDto } from '@ghostfolio/api/dtos/filter.dto'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { AiPromptResponse } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; @@ -12,7 +13,8 @@ import { Inject, Param, Query, - UseGuards + UseGuards, + UseInterceptors } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; @@ -30,6 +32,7 @@ export class AiController { @Get('prompt/:mode') @HasPermission(permissions.readAiPrompt) @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + @UseInterceptors(TransformDataSourceInRequestInterceptor) public async getPrompt( @Param('mode') mode: AiPromptMode, @Query() diff --git a/apps/api/src/app/endpoints/ai/ai.module.ts b/apps/api/src/app/endpoints/ai/ai.module.ts index d5cf0e3e95..844737cc60 100644 --- a/apps/api/src/app/endpoints/ai/ai.module.ts +++ b/apps/api/src/app/endpoints/ai/ai.module.ts @@ -7,6 +7,7 @@ import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.servic import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module'; +import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { ApiModule } from '@ghostfolio/api/services/api/api.module'; import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; @@ -46,6 +47,7 @@ import { AiService } from './ai.service'; RedisCacheModule, SymbolProfileModule, TagModule, + TransformDataSourceInRequestModule, UserModule ], providers: [ diff --git a/apps/api/src/app/endpoints/mcp/mcp.controller.ts b/apps/api/src/app/endpoints/mcp/mcp.controller.ts index baf68659b0..47e2804f9d 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.controller.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.controller.ts @@ -176,7 +176,7 @@ export class GhostfolioMcpController { ) { const filters = this.apiService.buildFiltersFromQueryParams({ filterByAccounts: accountIds?.join(','), - filterByAssetClasses: assetClasses?.join(','), + filterByAssetClasses: assetClasses, filterByDataSource: holding?.dataSource, filterBySymbol: holding?.symbol }); @@ -222,7 +222,7 @@ export class GhostfolioMcpController { } const filters = this.apiService.buildFiltersFromQueryParams({ - filterByAssetClasses: assetClasses?.join(','), + filterByAssetClasses: assetClasses, filterByDataSource: holding?.dataSource, filterBySymbol: holding?.symbol }); diff --git a/apps/api/src/dtos/filter.dto.ts b/apps/api/src/dtos/filter.dto.ts index cb26d582bc..a34cb3a3af 100644 --- a/apps/api/src/dtos/filter.dto.ts +++ b/apps/api/src/dtos/filter.dto.ts @@ -1,17 +1,23 @@ -import { IsOptional, IsString } from 'class-validator'; +import { AssetClass, DataSource } from '@prisma/client'; +import { Transform, TransformFnParams } from 'class-transformer'; +import { IsEnum, IsOptional, IsString } from 'class-validator'; +import { isString } from 'lodash'; export class FilterDto { @IsOptional() @IsString() accounts?: string; + @IsEnum(AssetClass, { each: true }) @IsOptional() - @IsString() - assetClasses?: string; + @Transform(({ value }: TransformFnParams) => { + return isString(value) ? value.split(',') : value; + }) + assetClasses?: AssetClass[]; + @IsEnum(DataSource) @IsOptional() - @IsString() - dataSource?: string; + dataSource?: DataSource; @IsOptional() @IsString() diff --git a/apps/api/src/services/api/api.service.ts b/apps/api/src/services/api/api.service.ts index 11074870ef..d262fc7942 100644 --- a/apps/api/src/services/api/api.service.ts +++ b/apps/api/src/services/api/api.service.ts @@ -15,7 +15,7 @@ export class ApiService { filterByTags }: { filterByAccounts?: string; - filterByAssetClasses?: string; + filterByAssetClasses?: string[]; filterByAssetSubClasses?: string; filterByDataSource?: string; filterByHoldingType?: string; @@ -24,7 +24,7 @@ export class ApiService { filterByTags?: string; }): Filter[] { const accountIds = filterByAccounts?.split(',') ?? []; - const assetClasses = filterByAssetClasses?.split(',') ?? []; + const assetClasses = filterByAssetClasses ?? []; const assetSubClasses = filterByAssetSubClasses?.split(',') ?? []; const dataSource = filterByDataSource; const holdingType = filterByHoldingType; @@ -97,7 +97,7 @@ export class ApiService { }): Filter[] { return this.buildFiltersFromQueryParams({ filterByAccounts: userSettings?.['filters.accounts']?.[0], - filterByAssetClasses: userSettings?.['filters.assetClasses']?.[0], + filterByAssetClasses: userSettings?.['filters.assetClasses'], filterByDataSource: userSettings?.['filters.dataSource'], filterBySymbol: userSettings?.['filters.symbol'], filterByTags: userSettings?.['filters.tags']?.[0]