diff --git a/CHANGELOG.md b/CHANGELOG.md index 39f2a2b0..83df4164 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for the (optional) `accountId` in the import functionality for activities - Added support for the (optional) `dataSource` in the import functionality for activities +- Added support for the data source transformation - Added support for the cryptocurrency _Mina Protocol_ (`MINA-USD`) ### Changed @@ -18,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the consistent use of `symbol` in combination with `dataSource` - Removed the primary data source from the client +### Removed + +- Removed the unused endpoint `GET api/order/:id` + ## 1.108.0 - 27.01.2022 ### Changed diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index 3d315a84..58a043b8 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -1,5 +1,7 @@ import { UserService } from '@ghostfolio/api/app/user/user.service'; import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper'; +import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; +import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; @@ -14,7 +16,8 @@ import { Param, Post, Put, - UseGuards + UseGuards, + UseInterceptors } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; @@ -58,6 +61,7 @@ export class OrderController { @Get() @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getAllOrders( @Headers('impersonation-id') impersonationId ): Promise { @@ -91,19 +95,9 @@ export class OrderController { return { activities }; } - @Get(':id') - @UseGuards(AuthGuard('jwt')) - public async getOrderById(@Param('id') id: string): Promise { - return this.orderService.order({ - id_userId: { - id, - userId: this.request.user.id - } - }); - } - @Post() @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInRequestInterceptor) public async createOrder(@Body() data: CreateOrderDto): Promise { if ( !hasPermission(this.request.user.permissions, permissions.createOrder) @@ -138,6 +132,7 @@ export class OrderController { @Put(':id') @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInRequestInterceptor) public async update(@Param('id') id: string, @Body() data: UpdateOrderDto) { if ( !hasPermission(this.request.user.permissions, permissions.updateOrder) diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 0cb0fb61..6a2351a4 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -4,9 +4,12 @@ import { hasNotDefinedValuesInObject, nullifyValuesInObject } from '@ghostfolio/api/helper/object.helper'; +import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; +import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { baseCurrency } from '@ghostfolio/common/config'; +import { parseDate } from '@ghostfolio/common/helper'; import { PortfolioChart, PortfolioDetails, @@ -25,13 +28,11 @@ import { Inject, Param, Query, - Res, - UseGuards + UseGuards, + UseInterceptors } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; -import { DataSource } from '@prisma/client'; -import { Response } from 'express'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface'; @@ -53,8 +54,7 @@ export class PortfolioController { @UseGuards(AuthGuard('jwt')) public async getChart( @Headers('impersonation-id') impersonationId: string, - @Query('range') range, - @Res() res: Response + @Query('range') range ): Promise { const historicalDataContainer = await this.portfolioServiceStrategy .get() @@ -90,27 +90,29 @@ export class PortfolioController { }); } - return res.json({ + return { hasError, chart: chartData, isAllTimeHigh: historicalDataContainer.isAllTimeHigh, isAllTimeLow: historicalDataContainer.isAllTimeLow - }); + }; } @Get('details') @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getDetails( @Headers('impersonation-id') impersonationId: string, - @Query('range') range, - @Res() res: Response - ): Promise { + @Query('range') range + ): Promise { if ( this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.request.user.subscription.type === 'Basic' ) { - res.status(StatusCodes.FORBIDDEN); - return res.json({ accounts: {}, holdings: {} }); + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); } let hasError = false; @@ -159,21 +161,22 @@ export class PortfolioController { } } - return res.json({ accounts, hasError, holdings }); + return { accounts, hasError, holdings }; } @Get('investments') @UseGuards(AuthGuard('jwt')) public async getInvestments( - @Headers('impersonation-id') impersonationId: string, - @Res() res: Response + @Headers('impersonation-id') impersonationId: string ): Promise { if ( this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.request.user.subscription.type === 'Basic' ) { - res.status(StatusCodes.FORBIDDEN); - return res.json({}); + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); } let investments = await this.portfolioServiceStrategy @@ -195,15 +198,14 @@ export class PortfolioController { })); } - return res.json({ firstOrderDate: investments[0]?.date, investments }); + return { firstOrderDate: parseDate(investments[0]?.date), investments }; } @Get('performance') @UseGuards(AuthGuard('jwt')) public async getPerformance( @Headers('impersonation-id') impersonationId: string, - @Query('range') range, - @Res() res: Response + @Query('range') range ): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> { const performanceInformation = await this.portfolioServiceStrategy .get() @@ -219,15 +221,15 @@ export class PortfolioController { ); } - return res.json(performanceInformation); + return performanceInformation; } @Get('positions') @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getPositions( @Headers('impersonation-id') impersonationId: string, - @Query('range') range, - @Res() res: Response + @Query('range') range ): Promise { const result = await this.portfolioServiceStrategy .get() @@ -247,13 +249,12 @@ export class PortfolioController { }); } - return res.json(result); + return result; } @Get('public/:accessId') public async getPublic( - @Param('accessId') accessId, - @Res() res: Response + @Param('accessId') accessId ): Promise { const access = await this.accessService.access({ id: accessId }); const user = await this.userService.user({ @@ -261,8 +262,10 @@ export class PortfolioController { }); if (!access) { - res.status(StatusCodes.NOT_FOUND); - return res.json({ accounts: {}, holdings: {} }); + throw new HttpException( + getReasonPhrase(StatusCodes.NOT_FOUND), + StatusCodes.NOT_FOUND + ); } let hasDetails = true; @@ -305,7 +308,7 @@ export class PortfolioController { } } - return res.json(portfolioPublicDetails); + return portfolioPublicDetails; } @Get('summary') @@ -339,6 +342,7 @@ export class PortfolioController { } @Get('position/:dataSource/:symbol') + @UseInterceptors(TransformDataSourceInRequestInterceptor) @UseGuards(AuthGuard('jwt')) public async getPosition( @Headers('impersonation-id') impersonationId: string, @@ -376,21 +380,18 @@ export class PortfolioController { @Get('report') @UseGuards(AuthGuard('jwt')) public async getReport( - @Headers('impersonation-id') impersonationId: string, - @Res() res: Response + @Headers('impersonation-id') impersonationId: string ): Promise { if ( this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.request.user.subscription.type === 'Basic' ) { - res.status(StatusCodes.FORBIDDEN); - return res.json({ rules: [] }); + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); } - return ( - res.json( - await this.portfolioServiceStrategy.get().getReport(impersonationId) - ) - ); + return await this.portfolioServiceStrategy.get().getReport(impersonationId); } } diff --git a/apps/api/src/app/subscription/subscription.controller.ts b/apps/api/src/app/subscription/subscription.controller.ts index 531e798b..1f68c8f7 100644 --- a/apps/api/src/app/subscription/subscription.controller.ts +++ b/apps/api/src/app/subscription/subscription.controller.ts @@ -7,6 +7,7 @@ import { Body, Controller, Get, + HttpCode, HttpException, Inject, Logger, @@ -17,7 +18,6 @@ import { } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; -import { Response } from 'express'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { SubscriptionService } from './subscription.service'; @@ -32,11 +32,9 @@ export class SubscriptionController { ) {} @Post('redeem-coupon') + @HttpCode(StatusCodes.OK) @UseGuards(AuthGuard('jwt')) - public async redeemCoupon( - @Body() { couponCode }: { couponCode: string }, - @Res() res: Response - ) { + public async redeemCoupon(@Body() { couponCode }: { couponCode: string }) { if (!this.request.user) { throw new HttpException( getReasonPhrase(StatusCodes.FORBIDDEN), @@ -74,12 +72,10 @@ export class SubscriptionController { `Subscription for user '${this.request.user.id}' has been created with coupon` ); - res.status(StatusCodes.OK); - - return res.json({ + return { message: getReasonPhrase(StatusCodes.OK), statusCode: StatusCodes.OK - }); + }; } @Get('stripe/callback') diff --git a/apps/api/src/app/symbol/symbol.controller.ts b/apps/api/src/app/symbol/symbol.controller.ts index d81ba5ca..5b3c0f03 100644 --- a/apps/api/src/app/symbol/symbol.controller.ts +++ b/apps/api/src/app/symbol/symbol.controller.ts @@ -1,3 +1,5 @@ +import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; +import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { Controller, @@ -5,7 +7,8 @@ import { HttpException, Param, Query, - UseGuards + UseGuards, + UseInterceptors } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { DataSource } from '@prisma/client'; @@ -25,6 +28,7 @@ export class SymbolController { */ @Get('lookup') @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInResponseInterceptor) public async lookupSymbol( @Query() { query = '' } ): Promise<{ items: LookupItem[] }> { @@ -43,6 +47,8 @@ export class SymbolController { */ @Get(':dataSource/:symbol') @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInRequestInterceptor) + @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getSymbolData( @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string, diff --git a/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts new file mode 100644 index 00000000..27bbe047 --- /dev/null +++ b/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts @@ -0,0 +1,45 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { ConfigurationService } from '../services/configuration.service'; + +@Injectable() +export class TransformDataSourceInRequestInterceptor + implements NestInterceptor +{ + public constructor( + private readonly configurationService: ConfigurationService + ) {} + + public intercept( + context: ExecutionContext, + next: CallHandler + ): Observable { + const http = context.switchToHttp(); + const request = http.getRequest(); + + if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') === true) { + if (request.body.dataSource) { + request.body.dataSource = this.decodeDataSource( + request.body.dataSource + ); + } + + if (request.params.dataSource) { + request.params.dataSource = this.decodeDataSource( + request.params.dataSource + ); + } + } + + return next.handle(); + } + + private decodeDataSource(encodeDataSource: string) { + return Buffer.from(encodeDataSource, 'hex').toString(); + } +} diff --git a/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts new file mode 100644 index 00000000..4f7d641a --- /dev/null +++ b/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts @@ -0,0 +1,76 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor +} from '@nestjs/common'; +import { DataSource } from '@prisma/client'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { ConfigurationService } from '../services/configuration.service'; + +@Injectable() +export class TransformDataSourceInResponseInterceptor + implements NestInterceptor +{ + public constructor( + private readonly configurationService: ConfigurationService + ) {} + + public intercept( + context: ExecutionContext, + next: CallHandler + ): Observable { + return next.handle().pipe( + map((data: any) => { + if ( + this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') === true + ) { + if (data.activities) { + data.activities.map((activity) => { + activity.SymbolProfile.dataSource = this.encodeDataSource( + activity.SymbolProfile.dataSource + ); + activity.dataSource = this.encodeDataSource(activity.dataSource); + return activity; + }); + } + + if (data.dataSource) { + data.dataSource = this.encodeDataSource(data.dataSource); + } + + if (data.holdings) { + for (const symbol of Object.keys(data.holdings)) { + if (data.holdings[symbol].dataSource) { + data.holdings[symbol].dataSource = this.encodeDataSource( + data.holdings[symbol].dataSource + ); + } + } + } + + if (data.items) { + data.items.map((item) => { + item.dataSource = this.encodeDataSource(item.dataSource); + return item; + }); + } + + if (data.positions) { + data.positions.map((position) => { + position.dataSource = this.encodeDataSource(position.dataSource); + return position; + }); + } + } + + return data; + }) + ); + } + + private encodeDataSource(aDataSource: DataSource) { + return Buffer.from(aDataSource, 'utf-8').toString('hex'); + } +}