From 31a03d69879f649daefd5f8ef58b97b9c27ec346 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:34:19 +0200 Subject: [PATCH] Validate tags when creating or updating an activity --- .../account-balance/account-balance.module.ts | 3 +- apps/api/src/app/account/account.module.ts | 2 ++ apps/api/src/app/account/account.service.ts | 35 ++++--------------- .../app/activities/activities.controller.ts | 1 + .../src/app/activities/activities.module.ts | 2 ++ .../src/app/activities/activities.service.ts | 27 +++++++++++--- apps/api/src/app/endpoints/ai/ai.module.ts | 2 ++ .../endpoints/benchmarks/benchmarks.module.ts | 2 ++ .../src/app/endpoints/public/public.module.ts | 2 ++ .../roai/portfolio-calculator-cash.spec.ts | 2 ++ .../api/src/app/portfolio/portfolio.module.ts | 2 ++ .../app/portfolio/portfolio.service.spec.ts | 2 ++ apps/api/src/services/tag/tag.service.ts | 25 ++++++++++++- 13 files changed, 73 insertions(+), 34 deletions(-) diff --git a/apps/api/src/app/account-balance/account-balance.module.ts b/apps/api/src/app/account-balance/account-balance.module.ts index 02323acc9..f7b1efc51 100644 --- a/apps/api/src/app/account-balance/account-balance.module.ts +++ b/apps/api/src/app/account-balance/account-balance.module.ts @@ -1,6 +1,7 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; +import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; @@ -10,7 +11,7 @@ import { AccountBalanceService } from './account-balance.service'; @Module({ controllers: [AccountBalanceController], exports: [AccountBalanceService], - imports: [ExchangeRateDataModule, PrismaModule], + imports: [ExchangeRateDataModule, PrismaModule, TagModule], providers: [AccountBalanceService, AccountService] }) export class AccountBalanceModule {} diff --git a/apps/api/src/app/account/account.module.ts b/apps/api/src/app/account/account.module.ts index 253c7fb1d..47b859ba3 100644 --- a/apps/api/src/app/account/account.module.ts +++ b/apps/api/src/app/account/account.module.ts @@ -7,6 +7,7 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration/conf import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; +import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; @@ -25,6 +26,7 @@ import { AccountService } from './account.service'; PortfolioModule, PrismaModule, RedactValuesInResponseModule, + TagModule, UserModule ], providers: [AccountService] diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index d1f4009a9..b74197ae2 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -3,10 +3,11 @@ import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed. import { WHERE_ACCOUNT_NOT_EXCLUDED } from '@ghostfolio/api/helper/account.helper'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; +import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { Filter } from '@ghostfolio/common/interfaces'; -import { HttpException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { Account, @@ -19,7 +20,6 @@ import { } from '@prisma/client'; import { Big } from 'big.js'; import { format } from 'date-fns'; -import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { groupBy } from 'lodash'; import { CashDetails } from './interfaces/cash-details.interface'; @@ -30,7 +30,8 @@ export class AccountService { private readonly accountBalanceService: AccountBalanceService, private readonly eventEmitter: EventEmitter2, private readonly exchangeRateDataService: ExchangeRateDataService, - private readonly prismaService: PrismaService + private readonly prismaService: PrismaService, + private readonly tagService: TagService ) {} public async account({ @@ -127,7 +128,7 @@ export class AccountService { aUserId: string, tagIds?: string[] ): Promise { - await this.validateTagIds(tagIds, aUserId); + await this.tagService.validateTagIds(tagIds, aUserId); const account = await this.prismaService.account.create({ data: { @@ -259,15 +260,15 @@ export class AccountService { public async updateAccount( params: { - where: Prisma.AccountWhereUniqueInput; data: Prisma.AccountUpdateInput; + where: Prisma.AccountWhereUniqueInput; }, aUserId: string, tagIds?: string[] ): Promise { const { data, where } = params; - await this.validateTagIds(tagIds, aUserId); + await this.tagService.validateTagIds(tagIds, aUserId); const account = await this.prismaService.account.update({ data: { @@ -340,26 +341,4 @@ export class AccountService { }); } } - - private async validateTagIds(tagIds: string[], userId: string) { - if (!tagIds?.length) { - return; - } - - const uniqueTagIds = Array.from(new Set(tagIds)); - - const tagsCount = await this.prismaService.tag.count({ - where: { - id: { in: uniqueTagIds }, - OR: [{ userId }, { userId: null }] - } - }); - - if (tagsCount !== uniqueTagIds.length) { - throw new HttpException( - getReasonPhrase(StatusCodes.BAD_REQUEST), - StatusCodes.BAD_REQUEST - ); - } - } } diff --git a/apps/api/src/app/activities/activities.controller.ts b/apps/api/src/app/activities/activities.controller.ts index d00dd7292..f0f3a2acd 100644 --- a/apps/api/src/app/activities/activities.controller.ts +++ b/apps/api/src/app/activities/activities.controller.ts @@ -338,6 +338,7 @@ export class ActivitiesController { }), user: { connect: { id: this.request.user.id } } }, + userId: this.request.user.id, where: { id } diff --git a/apps/api/src/app/activities/activities.module.ts b/apps/api/src/app/activities/activities.module.ts index 661163ff1..34091ba5e 100644 --- a/apps/api/src/app/activities/activities.module.ts +++ b/apps/api/src/app/activities/activities.module.ts @@ -14,6 +14,7 @@ import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-da import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; +import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; @@ -36,6 +37,7 @@ import { ActivitiesService } from './activities.service'; RedactValuesInResponseModule, RedisCacheModule, SymbolProfileModule, + TagModule, TransformDataSourceInRequestModule, TransformDataSourceInResponseModule ], diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index c9fd19d1a..9fd2a8b61 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -12,6 +12,7 @@ import { MarketDataService } from '@ghostfolio/api/services/market-data/market-d import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; +import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { DATA_GATHERING_QUEUE_PRIORITY_HIGH, GATHER_ASSET_PROFILE_PROCESS_JOB_NAME, @@ -61,7 +62,8 @@ export class ActivitiesService { private readonly exchangeRateDataService: ExchangeRateDataService, private readonly marketDataService: MarketDataService, private readonly prismaService: PrismaService, - private readonly symbolProfileService: SymbolProfileService + private readonly symbolProfileService: SymbolProfileService, + private readonly tagService: TagService ) {} public areCashActivitiesExcludedByFilters(filters: Filter[] = []) { @@ -153,6 +155,15 @@ export class ActivitiesService { userId: string; } ): Promise { + const tags = data.tags ?? []; + + await this.tagService.validateTagIds( + tags.map(({ id }) => { + return id; + }), + data.userId + ); + let account: Prisma.AccountCreateNestedOneWithoutActivitiesInput; if (data.accountId) { @@ -167,7 +178,6 @@ export class ActivitiesService { } const accountId = data.accountId; - const tags = data.tags ?? []; const updateAccountBalance = data.updateAccountBalance ?? false; const userId = data.userId; @@ -932,6 +942,7 @@ export class ActivitiesService { public async updateActivity({ data, + userId, where }: { data: Prisma.OrderUpdateInput & { @@ -942,14 +953,22 @@ export class ActivitiesService { tags?: { id: string }[]; type?: ActivityType; }; + userId: string; where: Prisma.OrderWhereUniqueInput; }): Promise { + const tags = data.tags ?? []; + + await this.tagService.validateTagIds( + tags.map(({ id }) => { + return id; + }), + userId + ); + if (!data.comment) { data.comment = null; } - const tags = data.tags ?? []; - let isDraft = false; if ( diff --git a/apps/api/src/app/endpoints/ai/ai.module.ts b/apps/api/src/app/endpoints/ai/ai.module.ts index 5267f40c8..d5cf0e3e9 100644 --- a/apps/api/src/app/endpoints/ai/ai.module.ts +++ b/apps/api/src/app/endpoints/ai/ai.module.ts @@ -20,6 +20,7 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; +import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; @@ -44,6 +45,7 @@ import { AiService } from './ai.service'; PropertyModule, RedisCacheModule, SymbolProfileModule, + TagModule, UserModule ], providers: [ diff --git a/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts index 2bcd6177d..3c540c337 100644 --- a/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts +++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.module.ts @@ -23,6 +23,7 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; +import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; @@ -46,6 +47,7 @@ import { BenchmarksService } from './benchmarks.service'; RedisCacheModule, SymbolModule, SymbolProfileModule, + TagModule, TransformDataSourceInRequestModule, TransformDataSourceInResponseModule, UserModule diff --git a/apps/api/src/app/endpoints/public/public.module.ts b/apps/api/src/app/endpoints/public/public.module.ts index e8395228f..b992694c5 100644 --- a/apps/api/src/app/endpoints/public/public.module.ts +++ b/apps/api/src/app/endpoints/public/public.module.ts @@ -18,6 +18,7 @@ import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-da import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; +import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; @@ -38,6 +39,7 @@ import { PublicController } from './public.controller'; PrismaModule, RedisCacheModule, SymbolProfileModule, + TagModule, TransformDataSourceInRequestModule, UserModule ], diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts index 589168989..31e451051 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts @@ -91,6 +91,7 @@ describe('PortfolioCalculator', () => { accountBalanceService, null, exchangeRateDataService, + null, null ); @@ -122,6 +123,7 @@ describe('PortfolioCalculator', () => { exchangeRateDataService, null, null, + null, null ); diff --git a/apps/api/src/app/portfolio/portfolio.module.ts b/apps/api/src/app/portfolio/portfolio.module.ts index d818195ca..7f7a894df 100644 --- a/apps/api/src/app/portfolio/portfolio.module.ts +++ b/apps/api/src/app/portfolio/portfolio.module.ts @@ -20,6 +20,7 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; +import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; @@ -50,6 +51,7 @@ import { RulesService } from './rules.service'; RedactValuesInResponseModule, RedisCacheModule, SymbolProfileModule, + TagModule, TransformDataSourceInRequestModule, TransformDataSourceInResponseModule, UserModule diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts index adddb4013..c16590cce 100644 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -54,6 +54,7 @@ describe('PortfolioService', () => { null, null, exchangeRateDataService, + null, null ); @@ -67,6 +68,7 @@ describe('PortfolioService', () => { exchangeRateDataService, null, null, + null, null ); diff --git a/apps/api/src/services/tag/tag.service.ts b/apps/api/src/services/tag/tag.service.ts index d967ce854..4f2b5c162 100644 --- a/apps/api/src/services/tag/tag.service.ts +++ b/apps/api/src/services/tag/tag.service.ts @@ -1,7 +1,8 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; -import { Injectable } from '@nestjs/common'; +import { HttpException, Injectable } from '@nestjs/common'; import { Prisma, Tag } from '@prisma/client'; +import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @Injectable() export class TagService { @@ -124,4 +125,26 @@ export class TagService { where }); } + + public async validateTagIds(tagIds: string[], userId: string) { + if (!tagIds?.length) { + return; + } + + const uniqueTagIds = Array.from(new Set(tagIds)); + + const tagsCount = await this.prismaService.tag.count({ + where: { + id: { in: uniqueTagIds }, + OR: [{ userId }, { userId: null }] + } + }); + + if (tagsCount !== uniqueTagIds.length) { + throw new HttpException( + getReasonPhrase(StatusCodes.BAD_REQUEST), + StatusCodes.BAD_REQUEST + ); + } + } }