Browse Source

Task/harden validation of query parameters in endpoints with filters (#7809)

* Harden validation of accounts, assetClasses, dataSource and tags in endpoints with filters

* Update changelog
pull/6845/merge
Thomas Kaul 4 days ago
committed by GitHub
parent
commit
4bc4c4c339
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 5
      apps/api/src/app/endpoints/ai/ai.controller.ts
  3. 2
      apps/api/src/app/endpoints/ai/ai.module.ts
  4. 6
      apps/api/src/app/endpoints/mcp/mcp.controller.ts
  5. 30
      apps/api/src/dtos/filter.dto.ts
  6. 18
      apps/api/src/services/api/api.service.ts

4
CHANGELOG.md

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Changed
- Hardened the validation of the query parameters (`accounts`, `assetClasses`, `dataSource` and `tags`) in the endpoints with filters
### Fixed
- Fixed the missing country mapping of _Congo (Dem. Rep. of the)_ and _Congo (Rep. of)_ in the _Financial Modeling Prep_ service

5
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()

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 { 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: [

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

@ -175,8 +175,8 @@ export class GhostfolioMcpController {
}: z.infer<typeof GET_ACCOUNTS_PARAMETERS>
) {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts: accountIds?.join(','),
filterByAssetClasses: assetClasses?.join(','),
filterByAccounts: accountIds,
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
});

30
apps/api/src/dtos/filter.dto.ts

@ -1,23 +1,35 @@
import { IsOptional, IsString } from 'class-validator';
import { AssetClass, DataSource } from '@prisma/client';
import { Transform, TransformFnParams } from 'class-transformer';
import { IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
import { isString } from 'lodash';
export class FilterDto {
@IsOptional()
@IsString()
accounts?: string;
@IsUUID(undefined, { each: true })
@Transform(({ value }: TransformFnParams) => {
return isString(value) ? value.split(',') : value;
})
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()
symbol?: string;
@IsOptional()
@IsString()
tags?: string;
@IsUUID(undefined, { each: true })
@Transform(({ value }: TransformFnParams) => {
return isString(value) ? value.split(',') : value;
})
tags?: string[];
}

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

@ -14,23 +14,23 @@ export class ApiService {
filterBySymbol,
filterByTags
}: {
filterByAccounts?: string;
filterByAssetClasses?: string;
filterByAccounts?: string[];
filterByAssetClasses?: string[];
filterByAssetSubClasses?: string;
filterByDataSource?: string;
filterByHoldingType?: string;
filterBySearchQuery?: string;
filterBySymbol?: string;
filterByTags?: string;
filterByTags?: string[];
}): Filter[] {
const accountIds = filterByAccounts?.split(',') ?? [];
const assetClasses = filterByAssetClasses?.split(',') ?? [];
const accountIds = filterByAccounts ?? [];
const assetClasses = filterByAssetClasses ?? [];
const assetSubClasses = filterByAssetSubClasses?.split(',') ?? [];
const dataSource = filterByDataSource;
const holdingType = filterByHoldingType;
const searchQuery = filterBySearchQuery?.toLowerCase();
const symbol = filterBySymbol;
const tagIds = filterByTags?.split(',') ?? [];
const tagIds = filterByTags ?? [];
const filters = [
...accountIds.map((accountId) => {
@ -96,11 +96,11 @@ export class ApiService {
userSettings: UserSettings;
}): Filter[] {
return this.buildFiltersFromQueryParams({
filterByAccounts: userSettings?.['filters.accounts']?.[0],
filterByAssetClasses: userSettings?.['filters.assetClasses']?.[0],
filterByAccounts: userSettings?.['filters.accounts'],
filterByAssetClasses: userSettings?.['filters.assetClasses'],
filterByDataSource: userSettings?.['filters.dataSource'],
filterBySymbol: userSettings?.['filters.symbol'],
filterByTags: userSettings?.['filters.tags']?.[0]
filterByTags: userSettings?.['filters.tags']
});
}
}

Loading…
Cancel
Save