From a0d5beaa06402876ae126b6df2cb7efdc78655ab Mon Sep 17 00:00:00 2001 From: Thomas <4159106+dtslvr@users.noreply.github.com> Date: Mon, 31 Jan 2022 18:31:08 +0100 Subject: [PATCH] Transform data source --- apps/api/src/app/order/order.controller.ts | 5 +- .../src/app/portfolio/portfolio.controller.ts | 79 ++++++++++--------- .../subscription/subscription.controller.ts | 14 ++-- ...form-data-source-in-request.interceptor.ts | 37 +++++++++ ...orm-data-source-in-response.interceptor.ts | 52 ++++++++++++ 5 files changed, 138 insertions(+), 49 deletions(-) create mode 100644 apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts create mode 100644 apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index 3d315a849..eed7561ad 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -1,5 +1,6 @@ import { UserService } from '@ghostfolio/api/app/user/user.service'; import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper'; +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 +15,8 @@ import { Param, Post, Put, - UseGuards + UseGuards, + UseInterceptors } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; @@ -57,6 +59,7 @@ export class OrderController { } @Get() + @UseInterceptors(TransformDataSourceInResponseInterceptor) @UseGuards(AuthGuard('jwt')) public async getAllOrders( @Headers('impersonation-id') impersonationId diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 0cb0fb61b..f7b3452fd 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') + @UseInterceptors(TransformDataSourceInResponseInterceptor) @UseGuards(AuthGuard('jwt')) 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 531e798b9..1f68c8f72 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/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 000000000..d4d9d3e4d --- /dev/null +++ b/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts @@ -0,0 +1,37 @@ +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) { + // Decode data source + if (request.params.dataSource) { + request.params.dataSource = Buffer.from( + request.params.dataSource, + 'hex' + ).toString(); + } + } + + return next.handle(); + } +} 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 000000000..b51143be3 --- /dev/null +++ b/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts @@ -0,0 +1,52 @@ +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.dataSource = this.encodeDataSource(activity.dataSource); + return activity; + }); + } + + 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'); + } +}