From 69c1365c6977623e9396b43302ab003da6b379ae Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:17:30 +0100 Subject: [PATCH] Task/deprecate account and account balance endpoints in favor of accounts endpoints (#8332) * Deprecate account and account balance endpoints in favor of accounts endpoints * Update changelog --- CHANGELOG.md | 1 + .../app/__snapshots__/app.module.spec.ts.snap | 157 ++++++++++++++++++ .../account-balance/account-balance.module.ts | 13 -- apps/api/src/app/app.module.ts | 6 +- .../accounts}/account-balance.controller.ts | 5 +- .../accounts/accounts.controller.ts} | 72 +++++++- .../accounts/accounts.module.ts} | 11 +- .../accounts}/get-all-accounts.dto.ts | 0 .../account-balance.service.ts | 1 + .../account-detail-dialog.component.ts | 7 +- .../account-detail-dialog.html | 1 - ...ate-account-balance-with-account-id.dto.ts | 9 + .../lib/dtos/create-account-balance.dto.ts | 5 +- libs/common/src/lib/dtos/index.ts | 2 + libs/common/src/lib/interfaces/index.ts | 2 + .../account-balance-response.interface.ts | 6 + .../account-balances-response.interface.ts | 4 +- .../account-balances.component.ts | 2 - libs/ui/src/lib/services/data.service.ts | 42 +++-- 19 files changed, 295 insertions(+), 51 deletions(-) delete mode 100644 apps/api/src/app/account-balance/account-balance.module.ts rename apps/api/src/app/{account-balance => endpoints/accounts}/account-balance.controller.ts (94%) rename apps/api/src/app/{account/account.controller.ts => endpoints/accounts/accounts.controller.ts} (82%) rename apps/api/src/app/{account/account.module.ts => endpoints/accounts/accounts.module.ts} (73%) rename apps/api/src/app/{account => endpoints/accounts}/get-all-accounts.dto.ts (100%) create mode 100644 libs/common/src/lib/dtos/create-account-balance-with-account-id.dto.ts create mode 100644 libs/common/src/lib/interfaces/responses/account-balance-response.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f440ba5df..fef7349f68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Restricted the health check endpoint of the AI service (`GET api/v1/health/ai`) to administrators - Increased the timeout in the health check endpoint of the AI service to 30 seconds - Deprecated the `api/v1/access` endpoints in favor of the `api/v1/accesses` endpoints +- Deprecated the `api/v1/account` and `api/v1/account-balance` endpoints in favor of the `api/v1/accounts` endpoints - Improved the performance of the code style check in the build workflow by skipping the warning-level rules of _ESLint_ - Refreshed the cryptocurrencies list diff --git a/apps/api/src/app/__snapshots__/app.module.spec.ts.snap b/apps/api/src/app/__snapshots__/app.module.spec.ts.snap index 447d0bf608..9e2b9ff69b 100644 --- a/apps/api/src/app/__snapshots__/app.module.spec.ts.snap +++ b/apps/api/src/app/__snapshots__/app.module.spec.ts.snap @@ -50,6 +50,47 @@ exports[`AppModule should keep the security of each route 1`] = ` "account:delete", ], }, + "DELETE /account/:id/balances/:balanceId": { + "guards": [ + "AuthGuard('jwt')", + "HasPermissionGuard", + "ImpersonationGuard", + "ScopeGuard", + ], + "interceptors": [], + "permission": "deleteAccountBalance", + "scopes": [ + "account:update", + ], + }, + "DELETE /accounts/:id": { + "guards": [ + "AuthGuard('jwt')", + "HasPermissionGuard", + "ImpersonationGuard", + "ScopeGuard", + ], + "interceptors": [ + "RedactValuesInResponseInterceptor", + ], + "permission": "deleteAccount", + "scopes": [ + "account:delete", + ], + }, + "DELETE /accounts/:id/balances/:balanceId": { + "guards": [ + "AuthGuard('jwt')", + "HasPermissionGuard", + "ImpersonationGuard", + "ScopeGuard", + ], + "interceptors": [], + "permission": "deleteAccountBalance", + "scopes": [ + "account:update", + ], + }, "DELETE /activities": { "guards": [ "AuthGuard('jwt')", @@ -263,6 +304,49 @@ exports[`AppModule should keep the security of each route 1`] = ` "account:read", ], }, + "GET /accounts": { + "guards": [ + "AuthGuard('jwt')", + "HasPermissionGuard", + "ImpersonationGuard", + "ScopeGuard", + ], + "interceptors": [ + "TransformDataSourceInRequestInterceptor", + "RedactValuesInResponseInterceptor", + ], + "scopes": [ + "account:read", + ], + }, + "GET /accounts/:id": { + "guards": [ + "AuthGuard('jwt')", + "HasPermissionGuard", + "ImpersonationGuard", + "ScopeGuard", + ], + "interceptors": [ + "RedactValuesInResponseInterceptor", + ], + "scopes": [ + "account:read", + ], + }, + "GET /accounts/:id/balances": { + "guards": [ + "AuthGuard('jwt')", + "HasPermissionGuard", + "ImpersonationGuard", + "ScopeGuard", + ], + "interceptors": [ + "RedactValuesInResponseInterceptor", + ], + "scopes": [ + "account:read", + ], + }, "GET /activities": { "guards": [ "AuthGuard('jwt')", @@ -862,6 +946,21 @@ exports[`AppModule should keep the security of each route 1`] = ` "account:update", ], }, + "POST /account/:id/balances": { + "guards": [ + "AuthGuard('jwt')", + "HasPermissionGuard", + "ImpersonationGuard", + "ScopeGuard", + ], + "interceptors": [ + "RedactValuesInResponseInterceptor", + ], + "permission": "createAccountBalance", + "scopes": [ + "account:update", + ], + }, "POST /account/transfer-balance": { "guards": [ "AuthGuard('jwt')", @@ -875,6 +974,49 @@ exports[`AppModule should keep the security of each route 1`] = ` "account:update", ], }, + "POST /accounts": { + "guards": [ + "AuthGuard('jwt')", + "HasPermissionGuard", + "ImpersonationGuard", + "ScopeGuard", + ], + "interceptors": [ + "RedactValuesInResponseInterceptor", + ], + "permission": "createAccount", + "scopes": [ + "account:create", + ], + }, + "POST /accounts/:id/balances": { + "guards": [ + "AuthGuard('jwt')", + "HasPermissionGuard", + "ImpersonationGuard", + "ScopeGuard", + ], + "interceptors": [ + "RedactValuesInResponseInterceptor", + ], + "permission": "createAccountBalance", + "scopes": [ + "account:update", + ], + }, + "POST /accounts/transfer-balance": { + "guards": [ + "AuthGuard('jwt')", + "HasPermissionGuard", + "ImpersonationGuard", + "ScopeGuard", + ], + "interceptors": [], + "permission": "updateAccount", + "scopes": [ + "account:update", + ], + }, "POST /activities": { "guards": [ "AuthGuard('jwt')", @@ -1187,6 +1329,21 @@ exports[`AppModule should keep the security of each route 1`] = ` "account:update", ], }, + "PUT /accounts/:id": { + "guards": [ + "AuthGuard('jwt')", + "HasPermissionGuard", + "ImpersonationGuard", + "ScopeGuard", + ], + "interceptors": [ + "RedactValuesInResponseInterceptor", + ], + "permission": "updateAccount", + "scopes": [ + "account:update", + ], + }, "PUT /activities/:id": { "guards": [ "AuthGuard('jwt')", diff --git a/apps/api/src/app/account-balance/account-balance.module.ts b/apps/api/src/app/account-balance/account-balance.module.ts deleted file mode 100644 index 9e08924ca0..0000000000 --- a/apps/api/src/app/account-balance/account-balance.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { AccountBalanceModule as AccountBalanceServiceModule } from '@ghostfolio/api/services/account-balance/account-balance.module'; -import { AccountModule } from '@ghostfolio/api/services/account/account.module'; -import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; - -import { Module } from '@nestjs/common'; - -import { AccountBalanceController } from './account-balance.controller'; - -@Module({ - controllers: [AccountBalanceController], - imports: [AccountBalanceServiceModule, AccountModule, ImpersonationModule] -}) -export class AccountBalanceModule {} diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 03972300b5..417c2f50e2 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -35,12 +35,11 @@ import { ThrottlerModule } from '@nestjs/throttler'; import { getReasonPhrase, StatusCodes } from 'http-status-codes'; import { join } from 'node:path'; -import { AccountBalanceModule } from './account-balance/account-balance.module'; -import { AccountModule } from './account/account.module'; import { ActivitiesModule } from './activities/activities.module'; import { AdminModule } from './admin/admin.module'; import { AppController } from './app.controller'; import { AccessesModule } from './endpoints/accesses/accesses.module'; +import { AccountsModule } from './endpoints/accounts/accounts.module'; import { AiModule } from './endpoints/ai/ai.module'; import { ApiKeysModule } from './endpoints/api-keys/api-keys.module'; import { AssetProfilesModule } from './endpoints/asset-profiles/asset-profiles.module'; @@ -73,8 +72,7 @@ import { UserModule } from './user/user.module'; imports: [ AdminModule, AccessesModule, - AccountBalanceModule, - AccountModule, + AccountsModule, ActivitiesModule, AiModule, ApiKeysModule, diff --git a/apps/api/src/app/account-balance/account-balance.controller.ts b/apps/api/src/app/endpoints/accounts/account-balance.controller.ts similarity index 94% rename from apps/api/src/app/account-balance/account-balance.controller.ts rename to apps/api/src/app/endpoints/accounts/account-balance.controller.ts index a588de7621..9bcb25a8a3 100644 --- a/apps/api/src/app/account-balance/account-balance.controller.ts +++ b/apps/api/src/app/endpoints/accounts/account-balance.controller.ts @@ -4,7 +4,7 @@ import { RequiresScope } from '@ghostfolio/api/decorators/requires-scope.decorat import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor'; import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service'; import { AccountService } from '@ghostfolio/api/services/account/account.service'; -import { CreateAccountBalanceDto } from '@ghostfolio/common/dtos'; +import { CreateAccountBalanceWithAccountIdDto } from '@ghostfolio/common/dtos'; import { permissions } from '@ghostfolio/common/permissions'; import { scopes } from '@ghostfolio/common/scopes'; import type { ImpersonationContext } from '@ghostfolio/common/types'; @@ -21,6 +21,7 @@ import { import { AccountBalance } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; +/** @deprecated */ @Controller('account-balance') export class AccountBalanceController { public constructor( @@ -33,7 +34,7 @@ export class AccountBalanceController { @RequiresScope(scopes.accountUpdate) @UseInterceptors(RedactValuesInResponseInterceptor) public async createAccountBalance( - @Body() data: CreateAccountBalanceDto, + @Body() data: CreateAccountBalanceWithAccountIdDto, @Impersonation() { userId }: ImpersonationContext ): Promise { const account = await this.accountService.account({ diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/endpoints/accounts/accounts.controller.ts similarity index 82% rename from apps/api/src/app/account/account.controller.ts rename to apps/api/src/app/endpoints/accounts/accounts.controller.ts index f8ee15503b..3832becbf7 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/endpoints/accounts/accounts.controller.ts @@ -8,11 +8,13 @@ import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/ import { AccountService } from '@ghostfolio/api/services/account/account.service'; import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { + CreateAccountBalanceDto, CreateAccountDto, TransferBalanceDto, UpdateAccountDto } from '@ghostfolio/common/dtos'; import { + AccountBalanceResponse, AccountBalancesResponse, AccountResponse, AccountsResponse @@ -35,11 +37,16 @@ import { } from '@nestjs/common'; import { Account as AccountModel } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; +import { pick } from 'lodash-es'; import { GetAllAccountsDto } from './get-all-accounts.dto'; -@Controller('account') -export class AccountController { +@Controller([ + 'accounts', + /** @deprecated */ + 'account' +]) +export class AccountsController { public constructor( private readonly accountBalanceService: AccountBalanceService, private readonly accountService: AccountService, @@ -80,6 +87,33 @@ export class AccountController { }); } + @Delete(':id/balances/:balanceId') + @HasPermission(permissions.deleteAccountBalance) + @RequiresScope(scopes.accountUpdate) + public async deleteAccountBalance( + @Impersonation() { userId }: ImpersonationContext, + @Param('balanceId') balanceId: string, + @Param('id') id: string + ): Promise { + const accountBalance = await this.accountBalanceService.accountBalance({ + userId, + accountId: id, + id: balanceId + }); + + if (!accountBalance) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + await this.accountBalanceService.deleteAccountBalance({ + id: accountBalance.id, + userId: accountBalance.userId + }); + } + @Get() @RequiresScope(scopes.accountRead) @UseInterceptors(RedactValuesInResponseInterceptor) @@ -182,6 +216,40 @@ export class AccountController { } } + @HasPermission(permissions.createAccountBalance) + @Post(':id/balances') + @RequiresScope(scopes.accountUpdate) + @UseInterceptors(RedactValuesInResponseInterceptor) + public async createAccountBalance( + @Body() { balance, date }: CreateAccountBalanceDto, + @Impersonation() { userId }: ImpersonationContext, + @Param('id') id: string + ): Promise { + const account = await this.accountService.account({ + id_userId: { + id, + userId + } + }); + + if (!account) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + const accountBalance = + await this.accountBalanceService.createOrUpdateAccountBalance({ + balance, + date, + accountId: account.id, + userId: account.userId + }); + + return pick(accountBalance, ['accountId', 'date', 'id', 'value']); + } + @HasPermission(permissions.updateAccount) @Post('transfer-balance') @RequiresScope(scopes.accountUpdate) diff --git a/apps/api/src/app/account/account.module.ts b/apps/api/src/app/endpoints/accounts/accounts.module.ts similarity index 73% rename from apps/api/src/app/account/account.module.ts rename to apps/api/src/app/endpoints/accounts/accounts.module.ts index c2ba8b625a..947e26582a 100644 --- a/apps/api/src/app/account/account.module.ts +++ b/apps/api/src/app/endpoints/accounts/accounts.module.ts @@ -2,20 +2,21 @@ import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module' import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module'; import { AccountBalanceModule } from '@ghostfolio/api/services/account-balance/account-balance.module'; -import { AccountModule as AccountServiceModule } from '@ghostfolio/api/services/account/account.module'; +import { AccountModule } from '@ghostfolio/api/services/account/account.module'; import { ApiModule } from '@ghostfolio/api/services/api/api.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; import { Module } from '@nestjs/common'; -import { AccountController } from './account.controller'; +import { AccountBalanceController } from './account-balance.controller'; +import { AccountsController } from './accounts.controller'; @Module({ - controllers: [AccountController], + controllers: [AccountBalanceController, AccountsController], imports: [ AccountBalanceModule, - AccountServiceModule, + AccountModule, ApiModule, ConfigurationModule, ImpersonationModule, @@ -24,4 +25,4 @@ import { AccountController } from './account.controller'; UserModule ] }) -export class AccountModule {} +export class AccountsModule {} diff --git a/apps/api/src/app/account/get-all-accounts.dto.ts b/apps/api/src/app/endpoints/accounts/get-all-accounts.dto.ts similarity index 100% rename from apps/api/src/app/account/get-all-accounts.dto.ts rename to apps/api/src/app/endpoints/accounts/get-all-accounts.dto.ts diff --git a/apps/api/src/services/account-balance/account-balance.service.ts b/apps/api/src/services/account-balance/account-balance.service.ts index 15358a8486..65eb6b81c5 100644 --- a/apps/api/src/services/account-balance/account-balance.service.ts +++ b/apps/api/src/services/account-balance/account-balance.service.ts @@ -49,6 +49,7 @@ export class AccountBalanceService { date, userId }: CreateAccountBalanceDto & { + accountId: string; userId: string; }): Promise { const dateOfBalance = resetHours(parseISO(date)); diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts index ce31645986..539e43a67a 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts @@ -176,7 +176,10 @@ export class GfAccountDetailDialogComponent implements OnInit { this.isSubmitting.set(true); this.dataService - .postAccountBalance(accountBalance) + .postAccountBalance({ + accountBalance, + accountId: this.data.accountId + }) .pipe( finalize(() => { this.isSubmitting.set(false); @@ -202,7 +205,7 @@ export class GfAccountDetailDialogComponent implements OnInit { protected onDeleteAccountBalance(aId: string) { this.dataService - .deleteAccountBalance(aId) + .deleteAccountBalance({ accountId: this.data.accountId, id: aId }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.initialize(); diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html index c5f82d294a..2d3007f5bb 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html @@ -211,7 +211,6 @@ {} diff --git a/libs/common/src/lib/interfaces/responses/account-balances-response.interface.ts b/libs/common/src/lib/interfaces/responses/account-balances-response.interface.ts index f56cd6ce89..fe84544b42 100644 --- a/libs/common/src/lib/interfaces/responses/account-balances-response.interface.ts +++ b/libs/common/src/lib/interfaces/responses/account-balances-response.interface.ts @@ -1,7 +1,7 @@ -import type { AccountBalance } from '@ghostfolio/prisma/browser'; +import type { AccountBalanceResponse } from './account-balance-response.interface'; export interface AccountBalancesResponse { - balances: (Pick & { + balances: (AccountBalanceResponse & { valueInBaseCurrency: number; })[]; } diff --git a/libs/ui/src/lib/account-balances/account-balances.component.ts b/libs/ui/src/lib/account-balances/account-balances.component.ts index 4e72647b13..7554dcb849 100644 --- a/libs/ui/src/lib/account-balances/account-balances.component.ts +++ b/libs/ui/src/lib/account-balances/account-balances.component.ts @@ -69,7 +69,6 @@ export class GfAccountBalancesComponent implements OnChanges { input.required(); public readonly accountCurrency = input.required(); - public readonly accountId = input.required(); public readonly currentBalance = input(); public readonly displayedColumns: string[] = ['date', 'value', 'actions']; public readonly isSubmitting = input(false); @@ -138,7 +137,6 @@ export class GfAccountBalancesComponent implements OnChanges { const accountBalance: CreateAccountBalanceDto = { balance, - accountId: this.accountId(), date: format(date, DATE_FORMAT) }; diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts index 972523b750..c7bbf270b1 100644 --- a/libs/ui/src/lib/services/data.service.ts +++ b/libs/ui/src/lib/services/data.service.ts @@ -24,6 +24,7 @@ import { AccessResponse, AccessTokenResponse, AccessesResponse, + AccountBalanceResponse, AccountBalancesResponse, AccountResponse, AccountsResponse, @@ -73,7 +74,6 @@ import type { } from '@ghostfolio/common/types'; import type { Account, - AccountBalance, MarketData, Order, SymbolProfile, @@ -199,19 +199,19 @@ export class DataService { } public fetchAccount(aAccountId: string) { - return this.http.get(`/api/v1/account/${aAccountId}`); + return this.http.get(`/api/v1/accounts/${aAccountId}`); } public fetchAccountBalances(aAccountId: string) { return this.http.get( - `/api/v1/account/${aAccountId}/balances` + `/api/v1/accounts/${aAccountId}/balances` ); } public fetchAccounts({ filters }: { filters?: Filter[] } = {}) { const params = this.buildFiltersAsQueryParams({ filters }); - return this.http.get('/api/v1/account', { params }); + return this.http.get('/api/v1/accounts', { params }); } public fetchActivities({ @@ -315,11 +315,19 @@ export class DataService { } public deleteAccount(aId: string) { - return this.http.delete(`/api/v1/account/${aId}`); + return this.http.delete(`/api/v1/accounts/${aId}`); } - public deleteAccountBalance(aId: string) { - return this.http.delete(`/api/v1/account-balance/${aId}`); + public deleteAccountBalance({ + accountId, + id + }: { + accountId: string; + id: string; + }) { + return this.http.delete( + `/api/v1/accounts/${accountId}/balances/${id}` + ); } public deleteActivities({ @@ -867,13 +875,19 @@ export class DataService { } public postAccount(aAccount: CreateAccountDto) { - return this.http.post('/api/v1/account', aAccount); + return this.http.post('/api/v1/accounts', aAccount); } - public postAccountBalance(aAccountBalance: CreateAccountBalanceDto) { - return this.http.post( - '/api/v1/account-balance', - aAccountBalance + public postAccountBalance({ + accountBalance, + accountId + }: { + accountBalance: CreateAccountBalanceDto; + accountId: string; + }) { + return this.http.post( + `/api/v1/accounts/${accountId}/balances`, + accountBalance ); } @@ -934,7 +948,7 @@ export class DataService { } public putAccount(aAccount: UpdateAccountDto) { - return this.http.put(`/api/v1/account/${aAccount.id}`, aAccount); + return this.http.put(`/api/v1/accounts/${aAccount.id}`, aAccount); } public putActivity(aOrder: UpdateOrderDto) { @@ -985,7 +999,7 @@ export class DataService { accountIdTo, balance }: TransferBalanceDto) { - return this.http.post('/api/v1/account/transfer-balance', { + return this.http.post('/api/v1/accounts/transfer-balance', { accountIdFrom, accountIdTo, balance