diff --git a/CHANGELOG.md b/CHANGELOG.md index dfef71271b..0ca9c898f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the server of the Model Context Protocol (MCP) (experimental) +- Hardened the validation of the query parameters (`accounts`, `assetClasses`, `dataSource` and `tags`) in the endpoints with filters + +### Fixed + +- Fixed the missing country mapping of _Congo (Dem. Rep. of the)_ and _Congo (Rep. of)_ in the _Financial Modeling Prep_ service + +## 3.66.0 - 2026-09-03 + +### Changed + +- Moved the details of the granted access from the table to the dialog on the access page (experimental) +- Restricted the _Restricted view and manage_ permission of the access to share the portfolio to the Model Context Protocol (MCP) (experimental) +- Migrated the transfer cash balance dialog to a dedicated route +- Improved the language localization for Italian (`it`) +- Upgraded `@rekog/mcp-nest` from version `2.0.0` to `2.0.2` +- Upgraded `prisma` from version `7.9.1` to `7.10.0` ### Fixed - Fixed the loading state of the accounts table on the accounts page +- Fixed the loading state of the holdings table on the portfolio holdings page ## 3.65.0 - 2026-08-31 diff --git a/apps/api/src/app/endpoints/ai/ai.controller.ts b/apps/api/src/app/endpoints/ai/ai.controller.ts index 1a3fd3ba2a..9691e211de 100644 --- a/apps/api/src/app/endpoints/ai/ai.controller.ts +++ b/apps/api/src/app/endpoints/ai/ai.controller.ts @@ -1,6 +1,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { FilterDto } from '@ghostfolio/api/dtos/filter.dto'; 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 { ApiService } from '@ghostfolio/api/services/api/api.service'; import { AiPromptResponse } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; @@ -12,7 +13,8 @@ import { Inject, Param, Query, - UseGuards + UseGuards, + UseInterceptors } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; @@ -30,6 +32,7 @@ export class AiController { @Get('prompt/:mode') @HasPermission(permissions.readAiPrompt) @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + @UseInterceptors(TransformDataSourceInRequestInterceptor) public async getPrompt( @Param('mode') mode: AiPromptMode, @Query() diff --git a/apps/api/src/app/endpoints/ai/ai.module.ts b/apps/api/src/app/endpoints/ai/ai.module.ts index d5cf0e3e95..844737cc60 100644 --- a/apps/api/src/app/endpoints/ai/ai.module.ts +++ b/apps/api/src/app/endpoints/ai/ai.module.ts @@ -7,6 +7,7 @@ import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.servic import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module'; +import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { ApiModule } from '@ghostfolio/api/services/api/api.module'; import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; @@ -46,6 +47,7 @@ import { AiService } from './ai.service'; RedisCacheModule, SymbolProfileModule, TagModule, + TransformDataSourceInRequestModule, UserModule ], providers: [ diff --git a/apps/api/src/app/endpoints/mcp/mcp.controller.ts b/apps/api/src/app/endpoints/mcp/mcp.controller.ts index 484ecb12ba..420acab3e1 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.controller.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.controller.ts @@ -64,8 +64,8 @@ export class GhostfolioMcpController { }: z.infer ) { const filters = this.apiService.buildFiltersFromQueryParams({ - filterByAccounts: accountIds?.join(','), - filterByAssetClasses: assetClasses?.join(','), + filterByAccounts: accountIds, + filterByAssetClasses: assetClasses, filterByDataSource: holding?.dataSource, filterBySymbol: holding?.symbol }); @@ -111,7 +111,7 @@ export class GhostfolioMcpController { } const filters = this.apiService.buildFiltersFromQueryParams({ - filterByAssetClasses: assetClasses?.join(','), + filterByAssetClasses: assetClasses, filterByDataSource: holding?.dataSource, filterBySymbol: holding?.symbol }); diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 082aeb7791..d53b3891ac 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -275,7 +275,7 @@ export class UserService { } try { - const { isBlocked } = await this.throttlerStorage.increment( + const { totalHits } = await this.throttlerStorage.increment( `${THROTTLE_DAILY_KEY}-${user.id}`, THROTTLE_DAILY_TTL, maxDailyRequests, @@ -283,7 +283,7 @@ export class UserService { THROTTLE_DAILY_KEY ); - return isBlocked; + return totalHits > maxDailyRequests; } catch (error) { this.logger.error(error); diff --git a/apps/api/src/dtos/filter.dto.ts b/apps/api/src/dtos/filter.dto.ts index cb26d582bc..6c50ec0629 100644 --- a/apps/api/src/dtos/filter.dto.ts +++ b/apps/api/src/dtos/filter.dto.ts @@ -1,23 +1,35 @@ -import { IsOptional, IsString } from 'class-validator'; +import { AssetClass, DataSource } from '@prisma/client'; +import { Transform, TransformFnParams } from 'class-transformer'; +import { IsEnum, IsOptional, IsString, IsUUID } from 'class-validator'; +import { isString } from 'lodash'; export class FilterDto { @IsOptional() - @IsString() - accounts?: string; + @IsUUID(undefined, { each: true }) + @Transform(({ value }: TransformFnParams) => { + return isString(value) ? value.split(',') : value; + }) + accounts?: string[]; + @IsEnum(AssetClass, { each: true }) @IsOptional() - @IsString() - assetClasses?: string; + @Transform(({ value }: TransformFnParams) => { + return isString(value) ? value.split(',') : value; + }) + assetClasses?: AssetClass[]; + @IsEnum(DataSource) @IsOptional() - @IsString() - dataSource?: string; + dataSource?: DataSource; @IsOptional() @IsString() symbol?: string; @IsOptional() - @IsString() - tags?: string; + @IsUUID(undefined, { each: true }) + @Transform(({ value }: TransformFnParams) => { + return isString(value) ? value.split(',') : value; + }) + tags?: string[]; } diff --git a/apps/api/src/services/api/api.service.ts b/apps/api/src/services/api/api.service.ts index 11074870ef..9dddff85ee 100644 --- a/apps/api/src/services/api/api.service.ts +++ b/apps/api/src/services/api/api.service.ts @@ -14,23 +14,23 @@ export class ApiService { filterBySymbol, filterByTags }: { - filterByAccounts?: string; - filterByAssetClasses?: string; + filterByAccounts?: string[]; + filterByAssetClasses?: string[]; filterByAssetSubClasses?: string; filterByDataSource?: string; filterByHoldingType?: string; filterBySearchQuery?: string; filterBySymbol?: string; - filterByTags?: string; + filterByTags?: string[]; }): Filter[] { - const accountIds = filterByAccounts?.split(',') ?? []; - const assetClasses = filterByAssetClasses?.split(',') ?? []; + const accountIds = filterByAccounts ?? []; + const assetClasses = filterByAssetClasses ?? []; const assetSubClasses = filterByAssetSubClasses?.split(',') ?? []; const dataSource = filterByDataSource; const holdingType = filterByHoldingType; const searchQuery = filterBySearchQuery?.toLowerCase(); const symbol = filterBySymbol; - const tagIds = filterByTags?.split(',') ?? []; + const tagIds = filterByTags ?? []; const filters = [ ...accountIds.map((accountId) => { @@ -96,11 +96,11 @@ export class ApiService { userSettings: UserSettings; }): Filter[] { return this.buildFiltersFromQueryParams({ - filterByAccounts: userSettings?.['filters.accounts']?.[0], - filterByAssetClasses: userSettings?.['filters.assetClasses']?.[0], + filterByAccounts: userSettings?.['filters.accounts'], + filterByAssetClasses: userSettings?.['filters.assetClasses'], filterByDataSource: userSettings?.['filters.dataSource'], filterBySymbol: userSettings?.['filters.symbol'], - filterByTags: userSettings?.['filters.tags']?.[0] + filterByTags: userSettings?.['filters.tags'] }); } } diff --git a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts index ced4db38a9..7c840b5fc6 100644 --- a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts +++ b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts @@ -54,6 +54,8 @@ export class FinancialModelingPrepService implements DataProviderInterface, OnModuleInit { private static countriesMapping = { + 'Congo (Dem. Rep. of the)': 'CD', + 'Congo (Rep. of)': 'CG', 'Czech Republic': 'CZ', 'Korea (the Republic of)': 'KR', Macau: 'MO', diff --git a/apps/client/src/app/components/access-table/access-table.component.html b/apps/client/src/app/components/access-table/access-table.component.html index fc324fb872..b8c2f8422d 100644 --- a/apps/client/src/app/components/access-table/access-table.component.html +++ b/apps/client/src/app/components/access-table/access-table.component.html @@ -45,36 +45,6 @@ - - Details - - @if (element.type === 'PUBLIC') { - - @if (user()?.settings?.isExperimentalFeatures) { -
- GET {{ baseUrl }}/api/v1/public/{{ - element.id - }}/portfolio -
- } - } @else if (element.type === 'MCP' && hasPermissionToEnableMcp) { -
- {{ baseUrl }}{{ mcpEndpoint }} -
-
- Authorization: Bearer {{ element.id }} -
- } - -
- @@ -113,14 +83,6 @@ } - @if (element.type === 'MCP' && hasPermissionToEnableMcp) { - - } @if ( (!isReceivedAccess() && user()?.settings?.isExperimentalFeatures) || element.type === 'PUBLIC' || diff --git a/apps/client/src/app/components/access-table/access-table.component.ts b/apps/client/src/app/components/access-table/access-table.component.ts index baf4247e3c..9b666b3b62 100644 --- a/apps/client/src/app/components/access-table/access-table.component.ts +++ b/apps/client/src/app/components/access-table/access-table.component.ts @@ -1,4 +1,3 @@ -import { MCP_ENDPOINT } from '@ghostfolio/common/config'; import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { getDateFormatString } from '@ghostfolio/common/helper'; import { Access, User } from '@ghostfolio/common/interfaces'; @@ -32,7 +31,6 @@ import { copyOutline, createOutline, ellipsisHorizontal, - linkOutline, removeCircleOutline, trashOutline } from 'ionicons/icons'; @@ -81,14 +79,7 @@ export class GfAccessTableComponent { protected readonly dataSource = new MatTableDataSource(); protected readonly displayedColumns = computed(() => { - const columns = [ - 'alias', - 'grantee', - 'type', - 'lastUsedAt', - 'expiresAt', - 'details' - ]; + const columns = ['alias', 'grantee', 'type', 'lastUsedAt', 'expiresAt']; if (this.showActions()) { columns.push('actions'); @@ -109,8 +100,6 @@ export class GfAccessTableComponent { return !this.accesses(); }); - protected readonly mcpEndpoint = MCP_ENDPOINT; - private readonly clipboard = inject(Clipboard); private readonly dataService = inject(DataService); private readonly notificationService = inject(NotificationService); @@ -121,7 +110,6 @@ export class GfAccessTableComponent { copyOutline, createOutline, ellipsisHorizontal, - linkOutline, removeCircleOutline, trashOutline }); @@ -142,18 +130,6 @@ export class GfAccessTableComponent { return `${this.baseUrl}/${languageCode}/${publicRoutes.public.path}/${aId}`; } - protected onCopyIdToClipboard(aId: string) { - this.clipboard.copy(aId); - - this.snackBar.open( - '✅ ' + $localize`Identifier has been copied to the clipboard`, - undefined, - { - duration: ms('3 seconds') - } - ); - } - protected onCopyUrlToClipboard(aId: string) { this.clipboard.copy(this.getPublicUrl(aId)); diff --git a/apps/client/src/app/components/home-holdings/home-holdings.html b/apps/client/src/app/components/home-holdings/home-holdings.html index df6d57c0e9..b55b1c54ef 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.html +++ b/apps/client/src/app/components/home-holdings/home-holdings.html @@ -37,7 +37,7 @@ }
diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts index b0be5c0c52..98275e5e78 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts @@ -1,11 +1,12 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { DEFAULT_LOCALE } from '@ghostfolio/common/config'; +import { DEFAULT_LOCALE, MCP_ENDPOINT } from '@ghostfolio/common/config'; import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos'; import { canApplyFiltersToAccess } from '@ghostfolio/common/helper'; import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { Scope, + canGrantRestrictedWriteAccess, getAccessLevel, getScopesOfAccess, getScopesOfAccessLevel, @@ -26,6 +27,7 @@ import { } from '@ghostfolio/ui/portfolio-filter-form'; import { DataService } from '@ghostfolio/ui/services'; +import { JsonPipe } from '@angular/common'; import type { HttpErrorResponse } from '@angular/common/http'; import { ChangeDetectionStrategy, @@ -68,6 +70,7 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces'; FormsModule, GfAccessLevelIconComponent, GfPortfolioFilterFormComponent, + JsonPipe, MatButtonModule, MatDatepickerModule, MatDialogModule, @@ -87,6 +90,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { public tags: Filter[] = []; protected accessForm: FormGroup; + protected readonly baseUrl = window.location.origin; protected minExpiresAt: Date; protected readonly mode: 'create' | 'update'; protected readonly today = startOfDay(new Date()); @@ -125,6 +129,10 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { return this.hasExperimentalFeatures && this.hasPermissionToEnableMcp; } + public get canGrantRestrictedWriteAccess() { + return canGrantRestrictedWriteAccess({ type: this.accessType }); + } + public get canGrantWriteAccess() { return this.hasExperimentalFeatures; } @@ -217,6 +225,10 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { this.loadHoldings(); } + protected get accessId() { + return this.data.access?.id; + } + protected get accessLevel(): AccessLevel { return this.accessForm?.get('accessLevel')?.value as AccessLevel; } @@ -229,10 +241,32 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { return this.accessType === 'PUBLIC'; } + protected get mcpConfiguration() { + return { + headers: { + Authorization: `Bearer ${this.accessId}` + }, + type: 'http', + url: `${this.baseUrl}${MCP_ENDPOINT}` + }; + } + protected get showExpiresAtErrorMessage() { return this.accessForm?.get('expiresAt')?.invalid === true; } + protected get showMcpDetails() { + return this.canGrantMcpAccess && this.isMcpAccess && this.mode === 'update'; + } + + protected get showPublicDetails() { + return ( + this.hasExperimentalFeatures && + this.isPublicAccess && + this.mode === 'update' + ); + } + protected onCancel() { this.dialogRef.close(); } @@ -245,6 +279,10 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { } } + private get isMcpAccess() { + return this.accessType === 'MCP'; + } + private async createAccess() { const filters = this.getFilters(); @@ -338,7 +376,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { } private async updateAccess() { - const accessId = this.data.access?.id; + const accessId = this.accessId; if (!accessId) { return; diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html index 89893e5708..31391beb0f 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -82,7 +82,7 @@ - @if (!isPublicAccess) { + @if (canGrantRestrictedWriteAccess) { } + @if (showMcpDetails || showPublicDetails) { +

Details

+ @if (showMcpDetails) { +
{{ mcpConfiguration | json }}
+ } @else if (showPublicDetails) { +
+ GET {{ baseUrl }}/api/v1/public/{{ accessId }}/portfolio +
+ } + }
diff --git a/apps/client/src/app/pages/accounts/accounts-page.routes.ts b/apps/client/src/app/pages/accounts/accounts-page.routes.ts index fd58d660ca..74ebb3a775 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.routes.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.routes.ts @@ -6,7 +6,8 @@ import { Routes } from '@angular/router'; import { GfAccountDialogHostComponent } from './account-dialog-host/account-dialog-host.component'; import { GfAccountsPageComponent } from './accounts-page.component'; -const { create, detail, update } = internalRoutes.accounts.subRoutes; +const { create, detail, transferCashBalance, update } = + internalRoutes.accounts.subRoutes; export const routes: Routes = [ { @@ -18,6 +19,12 @@ export const routes: Routes = [ path: create.path, title: create.title }, + { + component: GfAccountDialogHostComponent, + data: { mode: 'transferCashBalance' }, + path: transferCashBalance.path, + title: transferCashBalance.title + }, { children: [ { diff --git a/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts b/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts index 75f6f076a6..cdfa7181c4 100644 --- a/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts +++ b/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts @@ -99,13 +99,13 @@ export class GfTransferBalanceDialogComponent { } protected onSubmit() { - const account: TransferBalanceDto = { + const transferBalance: TransferBalanceDto = { accountIdFrom: this.transferBalanceForm.controls.fromAccount.value ?? '', accountIdTo: this.transferBalanceForm.controls.toAccount.value ?? '', balance: Number(this.transferBalanceForm.controls.balance.value) }; - this.dialogRef.close({ account }); + this.dialogRef.close(transferBalance); } private getAccountById(aId: string | null) { diff --git a/apps/client/src/locales/messages.it.xlf b/apps/client/src/locales/messages.it.xlf index c082eeee93..695acf32ea 100644 --- a/apps/client/src/locales/messages.it.xlf +++ b/apps/client/src/locales/messages.it.xlf @@ -40,7 +40,7 @@ please - please + per favore apps/client/src/app/pages/pricing/pricing-page.html 333 @@ -92,7 +92,7 @@ with - with + con apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html 87 @@ -100,7 +100,7 @@ Any person who has the link can view the data. - Any person who has the link can view the data. + Chiunque abbia il link può visualizzare i dati. apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 42 @@ -328,7 +328,7 @@ Paid - Paid + Pagamento effettuato apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts 136 @@ -368,7 +368,7 @@ Unknown - Unknown + Sconosciuto libs/ui/src/lib/i18n.ts 89 @@ -384,7 +384,7 @@ View and manage - View and manage + Visualizza e gestisci libs/ui/src/lib/access-level-icon/access-level-icon.component.html 9 @@ -420,7 +420,7 @@ and is driven by the efforts of its contributors - and is driven by the efforts of its contributors + ed è sostenuto dall’impegno dei suoi contributori apps/client/src/app/pages/about/overview/about-overview-page.html 50 @@ -428,7 +428,7 @@ Asset Profiles - Profilo dell’asset + Profili degli asset apps/client/src/app/components/admin-settings/admin-settings.component.html 131 @@ -492,7 +492,7 @@ Find an account... - Find an account... + Trova un account... libs/ui/src/lib/assistant/assistant.component.ts 448 @@ -528,7 +528,7 @@ Watch the Ghostfol.io Trailer on YouTube - Watch the Ghostfol.io Trailer on YouTube + Guarda il trailer di Ghostfol.io su YouTube apps/client/src/app/pages/landing/landing-page.html 19 @@ -644,7 +644,7 @@ Explore our guides to help you get started with investing and managing your finances. - Explore our guides to help you get started with investing and managing your finances. + Esplora le nostre guide per iniziare a investire e a gestire le tue finanze. apps/client/src/app/pages/resources/overview/resources-overview.component.ts 21 @@ -668,7 +668,7 @@ Update Account - Update Account + Aggiorna account libs/common/src/lib/routes/routes.ts 101 @@ -740,7 +740,7 @@ No auto-renewal on membership. - No auto-renewal on membership. + Nessun rinnovo automatico dell’abbonamento. apps/client/src/app/components/user-account-membership/user-account-membership.html 77 @@ -820,7 +820,7 @@ Creation - Creation + Creazione apps/client/src/app/components/admin-overview/admin-overview.html 191 @@ -876,7 +876,7 @@ Split Ratio - Split Ratio + Rapporto di frazionamento apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 532 @@ -884,7 +884,7 @@ Last Used - Last Used + Ultimo utilizzo apps/client/src/app/components/access-table/access-table.component.html 31 @@ -892,7 +892,7 @@ An error occurred while merging this asset profile into (). - An error occurred while merging this asset profile into (). + Si è verificato un errore durante l’unione di questo profilo di asset in (). apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts 942 @@ -936,7 +936,7 @@ Find quick answers to commonly asked questions about Ghostfolio in our Frequently Asked Questions (FAQ) section. - Find quick answers to commonly asked questions about Ghostfolio in our Frequently Asked Questions (FAQ) section. + Trova risposte rapide alle domande più frequenti su Ghostfolio nella nostra sezione FAQ. apps/client/src/app/pages/resources/overview/resources-overview.component.ts 16 @@ -992,7 +992,7 @@ Energy - Energy + Energia libs/ui/src/lib/i18n.ts 96 @@ -1220,7 +1220,7 @@ Ghostfolio in Numbers: Monthly Active Users (MAU) - Ghostfolio in Numbers: Monthly Active Users (MAU) + Ghostfolio in cifre: utenti attivi mensili (MAU) apps/client/src/app/pages/landing/landing-page.html 63 @@ -1240,7 +1240,7 @@ Performance with currency effect - Performance with currency effect + Performance con effetto valuta apps/client/src/app/pages/portfolio/analysis/analysis-page.html 125 @@ -1260,7 +1260,7 @@ Identifier has been copied to the clipboard - Identifier has been copied to the clipboard + Identificativo copiato negli appunti apps/client/src/app/components/access-table/access-table.component.ts 149 @@ -1340,7 +1340,7 @@ Add Account - Add Account + Aggiungi account libs/common/src/lib/routes/routes.ts 87 @@ -1356,7 +1356,7 @@ Consumer Defensive - Consumer Defensive + Beni di consumo difensivi libs/ui/src/lib/i18n.ts 95 @@ -1408,7 +1408,7 @@ Utilities - Utilities + Servizi di pubblica utilità libs/ui/src/lib/i18n.ts 103 @@ -9414,7 +9414,7 @@ Support Ghostfolio - Support Ghostfolio + Supporta Ghostfolio apps/client/src/app/pages/about/overview/about-overview-page.html 170 diff --git a/libs/common/src/lib/personal-finance-tools.ts b/libs/common/src/lib/personal-finance-tools.ts index 1339ea907d..d0d7a92ba2 100644 --- a/libs/common/src/lib/personal-finance-tools.ts +++ b/libs/common/src/lib/personal-finance-tools.ts @@ -1590,6 +1590,26 @@ export const personalFinanceTools: Product[] = [ slogan: 'The Intelligent Portfolio Tracker', url: 'https://www.navexa.com' }, + { + categories: [ + 'BUDGETING', + 'CRYPTOCURRENCY', + 'ETF_TRACKING', + 'FINANCIAL_PLANNING', + 'NET_WORTH_TRACKING', + 'STOCK_TRACKING' + ], + hasFreePlan: true, + hasSelfHostingAbility: false, + key: 'neontra', + languages: ['English'], + name: 'Neontra', + origin: 'CA', + platforms: ['ANDROID', 'IOS', 'WEB'], + pricingPerYear: '$75', + slogan: 'Everything you own. Everything you owe. Everything in one place.', + url: 'https://neontra.com' + }, { categories: ['INVESTMENT_RESEARCH', 'STOCK_TRACKING'], hasFreePlan: true, diff --git a/libs/common/src/lib/routes/routes.ts b/libs/common/src/lib/routes/routes.ts index 6985ba04f2..3d74297609 100644 --- a/libs/common/src/lib/routes/routes.ts +++ b/libs/common/src/lib/routes/routes.ts @@ -93,6 +93,11 @@ export const internalRoutes = { }, title: $localize`Account` }, + transferCashBalance: { + path: 'transfer-cash-balance', + routerLink: ['/accounts', 'transfer-cash-balance'], + title: $localize`Transfer Cash Balance` + }, update: { path: 'update', routerLink: (aAccountId: string) => { diff --git a/libs/common/src/lib/scopes.ts b/libs/common/src/lib/scopes.ts index 18270c7886..1c8efab431 100644 --- a/libs/common/src/lib/scopes.ts +++ b/libs/common/src/lib/scopes.ts @@ -64,7 +64,9 @@ export const SCOPES_OF_READ_RESTRICTED_ACCESS: readonly Scope[] = /** * Maximum scopes per access type. The scopes stored on an access are * intersected with it, hence a scope which the type does not permit stays - * ineffective even if it is stored. + * ineffective even if it is stored. A type which cannot grant the restricted + * write access drops the write scopes in addition, unless the access reads the + * monetary values. */ const SCOPES_OF_TYPE: Record = { MCP: [...SCOPES_OF_READ_RESTRICTED_ACCESS, scopes.activityCreate], @@ -72,6 +74,14 @@ const SCOPES_OF_TYPE: Record = { PUBLIC: SCOPES_OF_PUBLIC_ACCESS }; +/** + * Access types which combine a write scope with the restricted read access, + * because their tools change data without exposing the monetary values + */ +export function canGrantRestrictedWriteAccess({ type }: { type: AccessType }) { + return type === 'MCP'; +} + /** * Access level which the scopes of an access grant */ @@ -96,9 +106,16 @@ export function getScopesOfAccess({ }): string[] { const scopesToEvaluate = scopesOfAccess ?? []; + const permitsWriteAccess = + canGrantRestrictedWriteAccess({ type }) || + hasScope(scopesToEvaluate, scopes.portfolioReadValues); + // An unknown scope is dropped return SCOPES_OF_TYPE[type].filter((scope) => { - return scopesToEvaluate.includes(scope); + return ( + scopesToEvaluate.includes(scope) && + (permitsWriteAccess || !SCOPES_OF_WRITE_ACCESS.includes(scope)) + ); }); } diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.html b/libs/ui/src/lib/accounts-table/accounts-table.component.html index 2719f05a0d..a01abff1a8 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.html +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -4,7 +4,7 @@ class="align-items-center d-flex" mat-stroked-button [disabled]="dataSource?.data.length < 2" - (click)="onTransferBalance()" + [routerLink]="transferCashBalanceRouterLink" > Transfer Cash Balance... diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.ts b/libs/ui/src/lib/accounts-table/accounts-table.component.ts index 48eb99d51c..e986932906 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.ts +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.ts @@ -75,7 +75,6 @@ export class GfAccountsTableComponent { public readonly totalValueInBaseCurrency = input(); public readonly accountDeleted = output(); - public readonly transferBalance = output(); public readonly sort = viewChild.required(MatSort); @@ -141,6 +140,9 @@ export class GfAccountsTableComponent { protected readonly isLoading = computed(() => !this.accounts()); + protected readonly transferCashBalanceRouterLink = + internalRoutes.accounts.subRoutes.transferCashBalance.routerLink; + private readonly notificationService = inject(NotificationService); private readonly router = inject(Router); @@ -197,8 +199,4 @@ export class GfAccountsTableComponent { title: aComment }); } - - protected onTransferBalance() { - this.transferBalance.emit(); - } } diff --git a/libs/ui/src/lib/holdings-table/holdings-table.component.ts b/libs/ui/src/lib/holdings-table/holdings-table.component.ts index 7fe21720ef..1bbfa515b1 100644 --- a/libs/ui/src/lib/holdings-table/holdings-table.component.ts +++ b/libs/ui/src/lib/holdings-table/holdings-table.component.ts @@ -50,7 +50,7 @@ export class GfHoldingsTableComponent { public readonly hasPermissionToOpenDetails = input(true); public readonly hasPermissionToShowQuantities = input(true); public readonly hasPermissionToShowValues = input(true); - public readonly holdings = input.required(); + public readonly holdings = input.required(); public readonly locale = input(getLocale()); public readonly pageSize = model(Number.MAX_SAFE_INTEGER); @@ -89,7 +89,7 @@ export class GfHoldingsTableComponent { // Reactive data update effect(() => { - this.dataSource.data = this.holdings(); + this.dataSource.data = this.holdings() ?? []; }); // Reactive view connection diff --git a/package-lock.json b/package-lock.json index 16df6c6b8b..945f01ef54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "3.65.0", + "version": "3.66.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "3.65.0", + "version": "3.66.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -47,9 +47,9 @@ "@nestjs/serve-static": "5.0.5", "@nestjs/throttler": "6.5.0", "@openrouter/ai-sdk-provider": "3.0.0", - "@prisma/adapter-pg": "7.9.1", - "@prisma/client": "7.9.1", - "@rekog/mcp-nest": "2.0.0", + "@prisma/adapter-pg": "7.10.0", + "@prisma/client": "7.10.0", + "@rekog/mcp-nest": "2.0.2", "@simplewebauthn/browser": "13.3.0", "@simplewebauthn/server": "13.3.1", "ai": "7.0.37", @@ -134,7 +134,7 @@ "@nx/storybook": "23.1.1", "@nx/web": "23.1.1", "@nx/workspace": "23.1.1", - "@prisma/config": "7.9.1", + "@prisma/config": "7.10.0", "@schematics/angular": "22.1.4", "@storybook/addon-docs": "10.5.7", "@storybook/addon-themes": "10.5.7", @@ -165,7 +165,7 @@ "nx": "23.1.1", "prettier": "3.9.6", "prettier-plugin-organize-attributes": "1.0.0", - "prisma": "7.9.1", + "prisma": "7.10.0", "react": "18.2.0", "react-dom": "18.2.0", "replace-in-file": "9.0.0", @@ -13186,24 +13186,24 @@ } }, "node_modules/@prisma/adapter-pg": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.9.1.tgz", - "integrity": "sha512-Ho2RK1KanQxLNSC0sR5bpiiVep10sWPLXCcxK+KXfI/Q69TMRbiafSvLPv3V9snimX72rMCqGlyJ4sBO4lKTAw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.10.0.tgz", + "integrity": "sha512-N7nwSor0HO1Kz6xBv0TPAjAPysKK0fac6p4fVN3ensLOuzc/83Fgmln5k92eK/cvzqdkSR/2kkAqlbcdwVrwpw==", "license": "Apache-2.0", "dependencies": { - "@prisma/driver-adapter-utils": "7.9.1", + "@prisma/driver-adapter-utils": "7.10.0", "@types/pg": "^8.16.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "node_modules/@prisma/client": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.9.1.tgz", - "integrity": "sha512-+xgrh2EhJVF79wC0yX5G4PI1Rdcm7Qn/nekNQ+t/O153wtNggruHal+fXHSa0QE+Tp/Cw5wvxeCEhZZ59xGm8Q==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.10.0.tgz", + "integrity": "sha512-Ubw/QS9JGIBSBUsyxAUQuK/Jcu0Tsva7le7QbLd91Kix9yJvYDdj5QkwgEbbZniH80dd+sziQcALPc+HnvQC8Q==", "license": "Apache-2.0", "dependencies": { - "@prisma/client-runtime-utils": "7.9.1" + "@prisma/client-runtime-utils": "7.10.0" }, "engines": { "node": "^20.19 || ^22.12 || >=24.0" @@ -13222,15 +13222,15 @@ } }, "node_modules/@prisma/client-runtime-utils": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.9.1.tgz", - "integrity": "sha512-mVIBGYdO5CFmK0HvjxrtfIyQQcPdb88pSCeVQriVQPVZyDovIWblpHfOgcS8QO187j3QF0ePArH8qPhp0AU2vg==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.10.0.tgz", + "integrity": "sha512-cnCy7lUV8/CctgKVEmqAbSLAmwqJdE/qAlqTBk/0NDk59zEb2cZ0M0M0E4vVPnqbSEYudRroQDvOWfUZH6RIfw==", "license": "Apache-2.0" }, "node_modules/@prisma/config": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.9.1.tgz", - "integrity": "sha512-4znKhxTmXmuPye9Z6pbIyYb5VZlkZ05qG1L6Dr4g+7oTwc6V50Bs9XirFBDdjWt+H/AabMn9aUnxBcvj8z05aA==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.10.0.tgz", + "integrity": "sha512-Rcg828gIRE3HOQ3pOATFjV5d/P0U9OIobxhd/IMxlfWjA4vru0eGwb0AIwFw0rmcLMVShohZYWPixVxkBHsxUA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -13241,9 +13241,9 @@ } }, "node_modules/@prisma/debug": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.9.1.tgz", - "integrity": "sha512-/cpVZ4itxtcgB8GHBvZtcmuEjq+lWsLrRJxFMbwZrT1RIdtuKmUm7PPGo/wzfbYpBrk+9WmmBE8CHJw2rybKDQ==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.10.0.tgz", + "integrity": "sha512-caygJKtltmRIgdJ3jRpkOr7yM4DW6zxo5uOmojKWFb3asnxWoRkQOwZmXBgD8FZp4htrX+nMpcWqDwzlQ1+Y4g==", "license": "Apache-2.0" }, "node_modules/@prisma/dev": { @@ -13271,65 +13271,65 @@ } }, "node_modules/@prisma/driver-adapter-utils": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.9.1.tgz", - "integrity": "sha512-vmHehG7nn/heW32DXXpp13DxxAxVVe6n250oEt3dOL2E/4bt3olktKZN0mzSuxMMronyMSkbeW2uCOn3F4g8RQ==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.10.0.tgz", + "integrity": "sha512-u8zkcRLlaryO652T4qavBg0HmzNW5tSKdsCn6hc1PhWAp/J6k0vrxLuUs+b9o+HcjsK7Dfa01o4OFSn0frauJA==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.9.1" + "@prisma/debug": "7.10.0" } }, "node_modules/@prisma/engines": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.9.1.tgz", - "integrity": "sha512-UprXSMNXx2NF5ow4pqaQtE8OuBz6K78B0wc0tn2L28G5r933iWp1DR9Do2qWrsNvvFIP3x6mpEWnQtckMO0Uhg==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.10.0.tgz", + "integrity": "sha512-KNumN6NHFwybvfdYzTee9pqwx5PvknpWAaHn6L5NsbrKdl+SQrsVZs9opKs6U6SAsvB26HDt3WybRjOhgoWOYQ==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.9.1", - "@prisma/engines-version": "7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad", - "@prisma/fetch-engine": "7.9.1", - "@prisma/get-platform": "7.9.1" + "@prisma/debug": "7.10.0", + "@prisma/engines-version": "7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3", + "@prisma/fetch-engine": "7.10.0", + "@prisma/get-platform": "7.10.0" } }, "node_modules/@prisma/engines-version": { - "version": "7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad.tgz", - "integrity": "sha512-2BsPPFksz3CQUXG6af3rVCtJKg6+JJGJTtfgu2fU8DdXhOfkBjulCq8mwybCd6ge0/jhZq2kOtLAbmUDMyI1nA==", + "version": "7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3.tgz", + "integrity": "sha512-8OJ6RuZTZ06eFUOtBwxVmv8XMmOW6HWN5F+uxUbZkGxR0Bfab1dfAdXaHPmR5mb59E+fmUo8IOzXlbLY1SClbw==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.9.1.tgz", - "integrity": "sha512-PK8R60YZRQvYxBrGG9i7l2/rFyzy+2MuI1dKtmtrCqPH8YpiJx/MfiC7LRzX5786rZDEv7BngcjfIJW4/9ADuw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.0.tgz", + "integrity": "sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.9.1" + "@prisma/debug": "7.10.0" } }, "node_modules/@prisma/fetch-engine": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.9.1.tgz", - "integrity": "sha512-9DwxrNTeT25Orbu9CWh0CZvVlyY1lmscpbaeLZcOnuR7zcuFrt91YSmmOfIm7zJ08YOZ6mVzURKwLoMwEBcK8w==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.10.0.tgz", + "integrity": "sha512-Zqyu8DY14t6W/xwmAxUYWCXtHrvQnSvT644EAZSsdM8NSmCS74vJJbBKdVsK3ucFpnUWkEpbO1a0CxJXrg130g==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.9.1", - "@prisma/engines-version": "7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad", - "@prisma/get-platform": "7.9.1" + "@prisma/debug": "7.10.0", + "@prisma/engines-version": "7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3", + "@prisma/get-platform": "7.10.0" } }, "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.9.1.tgz", - "integrity": "sha512-PK8R60YZRQvYxBrGG9i7l2/rFyzy+2MuI1dKtmtrCqPH8YpiJx/MfiC7LRzX5786rZDEv7BngcjfIJW4/9ADuw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.0.tgz", + "integrity": "sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.9.1" + "@prisma/debug": "7.10.0" } }, "node_modules/@prisma/get-platform": { @@ -13587,9 +13587,9 @@ } }, "node_modules/@rekog/mcp-nest": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@rekog/mcp-nest/-/mcp-nest-2.0.0.tgz", - "integrity": "sha512-lT5V26V4fvPay50zMiw2KrEqlDSuzdAeyoQFd0+vfAwb40G6H8p8psTzgq5atTgNHktRPI7u4x8lrm8X844Asg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@rekog/mcp-nest/-/mcp-nest-2.0.2.tgz", + "integrity": "sha512-kuye6wmRk5Gw4KZSCSXd83cWznRw4eFbvNUXsd8+1dYcS4uWKVVbRvxQIKNCyxc2UcljpsmLpnksr1NCRn3YAQ==", "license": "MIT", "dependencies": { "multer": "^2.2.0", @@ -30547,16 +30547,16 @@ } }, "node_modules/prisma": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.9.1.tgz", - "integrity": "sha512-aPqePoZIqwlAchbgbFDO/wHqGB+7H1nj9gaM+OsL9h77S5S3TnLd9BgD3LnoeDikULo7cl2HSUrEyQ55Z7DYbg==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.10.0.tgz", + "integrity": "sha512-o0ornyJOWgygVAzGCpr8PdXV8EJLHyVGDDUr/voBQt8Azzw8cYTByzzPGcA/m4tCkPcnJA8raEOv2CslsKhPEw==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/config": "7.9.1", + "@prisma/config": "7.10.0", "@prisma/dev": "0.24.17", - "@prisma/engines": "7.9.1", + "@prisma/engines": "7.10.0", "@prisma/studio-core": "0.33.0", "mysql2": "3.15.3", "postgres": "3.4.7" diff --git a/package.json b/package.json index 62b73476cc..96a86e8faa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "3.65.0", + "version": "3.66.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", @@ -91,9 +91,9 @@ "@nestjs/serve-static": "5.0.5", "@nestjs/throttler": "6.5.0", "@openrouter/ai-sdk-provider": "3.0.0", - "@prisma/adapter-pg": "7.9.1", - "@prisma/client": "7.9.1", - "@rekog/mcp-nest": "2.0.0", + "@prisma/adapter-pg": "7.10.0", + "@prisma/client": "7.10.0", + "@rekog/mcp-nest": "2.0.2", "@simplewebauthn/browser": "13.3.0", "@simplewebauthn/server": "13.3.1", "ai": "7.0.37", @@ -178,7 +178,7 @@ "@nx/storybook": "23.1.1", "@nx/web": "23.1.1", "@nx/workspace": "23.1.1", - "@prisma/config": "7.9.1", + "@prisma/config": "7.10.0", "@schematics/angular": "22.1.4", "@storybook/addon-docs": "10.5.7", "@storybook/addon-themes": "10.5.7", @@ -209,7 +209,7 @@ "nx": "23.1.1", "prettier": "3.9.6", "prettier-plugin-organize-attributes": "1.0.0", - "prisma": "7.9.1", + "prisma": "7.10.0", "react": "18.2.0", "react-dom": "18.2.0", "replace-in-file": "9.0.0",