Browse Source

Harden validation of assetClasses and dataSource in endpoints with filters

pull/7809/head
Thomas Kaul 1 month ago
parent
commit
8fae03f88a
  1. 5
      apps/api/src/app/endpoints/ai/ai.controller.ts
  2. 2
      apps/api/src/app/endpoints/ai/ai.module.ts
  3. 4
      apps/api/src/app/endpoints/mcp/mcp.controller.ts
  4. 16
      apps/api/src/dtos/filter.dto.ts
  5. 6
      apps/api/src/services/api/api.service.ts

5
apps/api/src/app/endpoints/ai/ai.controller.ts

@ -1,6 +1,7 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { FilterDto } from '@ghostfolio/api/dtos/filter.dto'; import { FilterDto } from '@ghostfolio/api/dtos/filter.dto';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; 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 { ApiService } from '@ghostfolio/api/services/api/api.service';
import { AiPromptResponse } from '@ghostfolio/common/interfaces'; import { AiPromptResponse } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
@ -12,7 +13,8 @@ import {
Inject, Inject,
Param, Param,
Query, Query,
UseGuards UseGuards,
UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
@ -30,6 +32,7 @@ export class AiController {
@Get('prompt/:mode') @Get('prompt/:mode')
@HasPermission(permissions.readAiPrompt) @HasPermission(permissions.readAiPrompt)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getPrompt( public async getPrompt(
@Param('mode') mode: AiPromptMode, @Param('mode') mode: AiPromptMode,
@Query() @Query()

2
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 { RulesService } from '@ghostfolio/api/app/portfolio/rules.service';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.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 { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
@ -46,6 +47,7 @@ import { AiService } from './ai.service';
RedisCacheModule, RedisCacheModule,
SymbolProfileModule, SymbolProfileModule,
TagModule, TagModule,
TransformDataSourceInRequestModule,
UserModule UserModule
], ],
providers: [ providers: [

4
apps/api/src/app/endpoints/mcp/mcp.controller.ts

@ -176,7 +176,7 @@ export class GhostfolioMcpController {
) { ) {
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts: accountIds?.join(','), filterByAccounts: accountIds?.join(','),
filterByAssetClasses: assetClasses?.join(','), filterByAssetClasses: assetClasses,
filterByDataSource: holding?.dataSource, filterByDataSource: holding?.dataSource,
filterBySymbol: holding?.symbol filterBySymbol: holding?.symbol
}); });
@ -222,7 +222,7 @@ export class GhostfolioMcpController {
} }
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAssetClasses: assetClasses?.join(','), filterByAssetClasses: assetClasses,
filterByDataSource: holding?.dataSource, filterByDataSource: holding?.dataSource,
filterBySymbol: holding?.symbol filterBySymbol: holding?.symbol
}); });

16
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 { export class FilterDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
accounts?: string; accounts?: string;
@IsEnum(AssetClass, { each: true })
@IsOptional() @IsOptional()
@IsString() @Transform(({ value }: TransformFnParams) => {
assetClasses?: string; return isString(value) ? value.split(',') : value;
})
assetClasses?: AssetClass[];
@IsEnum(DataSource)
@IsOptional() @IsOptional()
@IsString() dataSource?: DataSource;
dataSource?: string;
@IsOptional() @IsOptional()
@IsString() @IsString()

6
apps/api/src/services/api/api.service.ts

@ -15,7 +15,7 @@ export class ApiService {
filterByTags filterByTags
}: { }: {
filterByAccounts?: string; filterByAccounts?: string;
filterByAssetClasses?: string; filterByAssetClasses?: string[];
filterByAssetSubClasses?: string; filterByAssetSubClasses?: string;
filterByDataSource?: string; filterByDataSource?: string;
filterByHoldingType?: string; filterByHoldingType?: string;
@ -24,7 +24,7 @@ export class ApiService {
filterByTags?: string; filterByTags?: string;
}): Filter[] { }): Filter[] {
const accountIds = filterByAccounts?.split(',') ?? []; const accountIds = filterByAccounts?.split(',') ?? [];
const assetClasses = filterByAssetClasses?.split(',') ?? []; const assetClasses = filterByAssetClasses ?? [];
const assetSubClasses = filterByAssetSubClasses?.split(',') ?? []; const assetSubClasses = filterByAssetSubClasses?.split(',') ?? [];
const dataSource = filterByDataSource; const dataSource = filterByDataSource;
const holdingType = filterByHoldingType; const holdingType = filterByHoldingType;
@ -97,7 +97,7 @@ export class ApiService {
}): Filter[] { }): Filter[] {
return this.buildFiltersFromQueryParams({ return this.buildFiltersFromQueryParams({
filterByAccounts: userSettings?.['filters.accounts']?.[0], filterByAccounts: userSettings?.['filters.accounts']?.[0],
filterByAssetClasses: userSettings?.['filters.assetClasses']?.[0], filterByAssetClasses: userSettings?.['filters.assetClasses'],
filterByDataSource: userSettings?.['filters.dataSource'], filterByDataSource: userSettings?.['filters.dataSource'],
filterBySymbol: userSettings?.['filters.symbol'], filterBySymbol: userSettings?.['filters.symbol'],
filterByTags: userSettings?.['filters.tags']?.[0] filterByTags: userSettings?.['filters.tags']?.[0]

Loading…
Cancel
Save