import { AllowDuringImpersonation } from '@ghostfolio/api/decorators/allow-during-impersonation.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; 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 { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { CreateAssetProfileSplitDto, UpdateAssetProfileDataDto } from '@ghostfolio/common/dtos'; import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper'; import { AssetProfileResponse } from '@ghostfolio/common/interfaces'; import { AssetProfilesResponse, EnhancedAssetProfile } from '@ghostfolio/common/interfaces'; import { hasPermission } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions'; import { MarketDataPreset, RequestWithUser } from '@ghostfolio/common/types'; import { Body, Controller, Delete, Get, HttpException, Inject, Param, ParseIntPipe, Patch, Post, Query, UseGuards, UseInterceptors } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import { AssetProfileSplit, DataSource, Prisma } from '@prisma/client'; import { parseISO } from 'date-fns'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { AssetProfilesService } from './asset-profiles.service'; @AllowDuringImpersonation() @Controller('asset-profiles') export class AssetProfilesController { public constructor( private readonly apiService: ApiService, private readonly assetProfilesService: AssetProfilesService, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly symbolProfileService: SymbolProfileService ) {} @Get() @HasPermission(permissions.accessAdminControl) @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getAssetProfiles( @Query('assetSubClasses') filterByAssetSubClasses?: string, @Query('dataSource') filterByDataSource?: string, @Query('presetId') presetId?: MarketDataPreset, @Query('query') filterBySearchQuery?: string, @Query('skip', new ParseIntPipe({ optional: true })) skip?: number, @Query('sortColumn') sortColumn?: string, @Query('sortDirection') sortDirection?: Prisma.SortOrder, @Query('take', new ParseIntPipe({ optional: true })) take?: number ): Promise { const filters = this.apiService.buildFiltersFromQueryParams({ filterByAssetSubClasses, filterByDataSource, filterBySearchQuery }); return this.assetProfilesService.getAssetProfiles({ filters, presetId, skip, sortColumn, sortDirection, take }); } @Get(':dataSource/:symbol') @UseGuards(AuthGuard('jwt')) @UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getAssetProfile( @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string ): Promise { const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([ { dataSource, symbol } ]); if (!assetProfile && !isCurrency(getCurrencyFromSymbol(symbol))) { throw new HttpException( getReasonPhrase(StatusCodes.NOT_FOUND), StatusCodes.NOT_FOUND ); } const canReadAllAssetProfiles = hasPermission( this.request.user.permissions, permissions.readMarketData ); const canReadOwnAssetProfile = assetProfile?.userId === this.request.user.id && hasPermission( this.request.user.permissions, permissions.readMarketDataOfOwnAssetProfile ); if (!canReadAllAssetProfiles && !canReadOwnAssetProfile) { throw new HttpException( assetProfile?.userId ? getReasonPhrase(StatusCodes.NOT_FOUND) : getReasonPhrase(StatusCodes.FORBIDDEN), assetProfile?.userId ? StatusCodes.NOT_FOUND : StatusCodes.FORBIDDEN ); } return this.assetProfilesService.getAssetProfile({ dataSource, symbol }); } @Post(':dataSource/:symbol/splits') @UseGuards(AuthGuard('jwt')) @UseInterceptors(TransformDataSourceInRequestInterceptor) public async createSplit( @Body() data: CreateAssetProfileSplitDto, @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string ): Promise { const { id: symbolProfileId } = await this.validateAccessToSplits({ dataSource, symbol, permission: permissions.createAssetProfileSplit, permissionOfOwnAssetProfile: permissions.createAssetProfileSplitOfOwnAssetProfile }); return this.assetProfilesService.createSplit({ dataSource, symbol, symbolProfileId, date: parseISO(data.date), denominator: data.denominator, numerator: data.numerator }); } @Delete(':dataSource/:symbol/splits/:id') @UseGuards(AuthGuard('jwt')) @UseInterceptors(TransformDataSourceInRequestInterceptor) public async deleteSplit( @Param('dataSource') dataSource: DataSource, @Param('id') id: string, @Param('symbol') symbol: string ): Promise { const { id: symbolProfileId } = await this.validateAccessToSplits({ dataSource, symbol, permission: permissions.deleteAssetProfileSplit, permissionOfOwnAssetProfile: permissions.deleteAssetProfileSplitOfOwnAssetProfile }); return this.assetProfilesService.deleteSplit({ id, symbolProfileId }); } @HasPermission(permissions.accessAdminControl) @Patch(':dataSource/:symbol') @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async updateAssetProfileData( @Body() assetProfileData: UpdateAssetProfileDataDto, @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string ): Promise { if (!this.request.user.settings.settings.isExperimentalFeatures) { throw new HttpException( getReasonPhrase(StatusCodes.NOT_FOUND), StatusCodes.NOT_FOUND ); } return this.assetProfilesService.updateAssetProfileData( { dataSource, symbol }, assetProfileData ); } private async validateAccessToSplits({ dataSource, permission, permissionOfOwnAssetProfile, symbol }: { dataSource: DataSource; permission: string; permissionOfOwnAssetProfile: string; symbol: string; }) { const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([ { dataSource, symbol } ]); if (!assetProfile) { throw new HttpException( getReasonPhrase(StatusCodes.NOT_FOUND), StatusCodes.NOT_FOUND ); } const canAccessAllAssetProfiles = hasPermission( this.request.user.permissions, permission ); const canAccessOwnAssetProfile = assetProfile.userId === this.request.user.id && hasPermission(this.request.user.permissions, permissionOfOwnAssetProfile); if (!canAccessAllAssetProfiles && !canAccessOwnAssetProfile) { throw new HttpException( assetProfile.userId ? getReasonPhrase(StatusCodes.NOT_FOUND) : getReasonPhrase(StatusCodes.FORBIDDEN), assetProfile.userId ? StatusCodes.NOT_FOUND : StatusCodes.FORBIDDEN ); } return assetProfile; } }