diff --git a/CHANGELOG.md b/CHANGELOG.md index dd6948aa0..89365d3b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 2.227.0 - 2026-01-02 ### Added @@ -13,12 +13,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Initialized the input properties in the _FIRE_ calculator +- Removed the deprecated public _Stripe_ key +- Upgraded `stripe` from version `18.5.0` to `20.1.0` + +### Fixed + +- Fixed the import of `jsonpath` to support REST APIs (`JSON`) via the scraper configuration + +## 2.226.0 - 2026-01-01 + +### Added + +- Extended the content of the _Self-Hosting_ section by information about additional data providers on the Frequently Asked Questions (FAQ) page + +### Changed + +- Upgraded `class-validator` from version `0.14.2` to `0.14.3` +- Upgraded `yahoo-finance2` from version `3.10.2` to `3.11.2` + +## 2.225.0 - 2025-12-31 + +### Added + +- Added a new endpoint to get all platforms (`GET api/v1/platforms`) +- Added the session url to the endpoint response of the _Stripe_ checkout + +### Changed + +- Improved the routing of the user detail dialog in the users section of the admin control panel - Lifted the asset profile identifier editing restriction for `MANUAL` data sources in the asset profile details dialog of the admin control panel +- Deprecated the public _Stripe_ key +- Improved the language localization for German (`de`) +- Eliminated `ngx-stripe` +- Upgraded `angular` from version `20.2.4` to `21.0.6` +- Upgraded `marked` from version `15.0.4` to `17.0.1` +- Upgraded `ngx-device-detector` from version `10.1.0` to `11.0.0` +- Upgraded `ng-extract-i18n-merge` from `3.1.0` to `3.2.1` +- Upgraded `ngx-markdown` from version `20.0.0` to `21.0.1` +- Upgraded `Nx` from version `21.5.1` to `22.3.3` - Upgraded `shx` from version `0.3.4` to `0.4.0` +- Upgraded `storybook` from version `9.1.5` to `10.1.10` +- Upgraded `zone.js` from version `0.15.1` to `0.16.0` ### Fixed - Added the missing currency suffix to the cash balance field in the create or update account dialog +- Fixed the time in market display of the portfolio summary tab on the home page for the impersonation mode - Fixed the delete button in the asset profile details dialog of the admin control panel by providing the missing `watchedByCount` parameter ## 2.224.2 - 2025-12-20 diff --git a/README.md b/README.md index 822825b57..35276b232 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,7 @@ Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/ { + const platforms = await this.platformService.getPlatforms(); + + return { platforms }; + } +} diff --git a/apps/api/src/app/endpoints/platforms/platforms.module.ts b/apps/api/src/app/endpoints/platforms/platforms.module.ts new file mode 100644 index 000000000..21d0edf69 --- /dev/null +++ b/apps/api/src/app/endpoints/platforms/platforms.module.ts @@ -0,0 +1,11 @@ +import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module'; + +import { Module } from '@nestjs/common'; + +import { PlatformsController } from './platforms.controller'; + +@Module({ + controllers: [PlatformsController], + imports: [PlatformModule] +}) +export class PlatformsModule {} diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index 3802e3ef4..c5152c1a2 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -93,7 +93,6 @@ export class InfoService { (await this.propertyService.getByKey( PROPERTY_COUNTRIES_OF_SUBSCRIBERS )) ?? []; - info.stripePublicKey = this.configurationService.get('STRIPE_PUBLIC_KEY'); } if (this.configurationService.get('ENABLE_FEATURE_SYSTEM_MESSAGE')) { diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 084c8f4ed..faabee79b 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1954,6 +1954,7 @@ export class PortfolioService { }).length, committedFunds: committedFunds.toNumber(), currentValueInBaseCurrency: currentValueInBaseCurrency.toNumber(), + dateOfFirstActivity: firstOrderDate, dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), emergencyFund: { assets: emergencyFundHoldingsValueInBaseCurrency, diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts index 0fad8c8ac..b38b07bb4 100644 --- a/apps/api/src/app/subscription/subscription.service.ts +++ b/apps/api/src/app/subscription/subscription.service.ts @@ -35,7 +35,7 @@ export class SubscriptionService { this.stripe = new Stripe( this.configurationService.get('STRIPE_SECRET_KEY'), { - apiVersion: '2025-08-27.basil' + apiVersion: '2025-12-15.clover' } ); } @@ -100,7 +100,8 @@ export class SubscriptionService { ); return { - sessionId: session.id + sessionId: session.id, + sessionUrl: session.url }; } diff --git a/apps/api/src/helper/object.helper.spec.ts b/apps/api/src/helper/object.helper.spec.ts index 433490325..e1ec81b8f 100644 --- a/apps/api/src/helper/object.helper.spec.ts +++ b/apps/api/src/helper/object.helper.spec.ts @@ -1,4 +1,24 @@ -import { redactAttributes } from './object.helper'; +import { query, redactAttributes } from './object.helper'; + +describe('query', () => { + it('should get market price from stock API response', () => { + const object = { + currency: 'USD', + market: { + previousClose: 273.04, + price: 271.86 + }, + symbol: 'AAPL' + }; + + const result = query({ + object, + pathExpression: '$.market.price' + })[0]; + + expect(result).toBe(271.86); + }); +}); describe('redactAttributes', () => { it('should redact provided attributes', () => { diff --git a/apps/api/src/helper/object.helper.ts b/apps/api/src/helper/object.helper.ts index a5854e9d9..6bb6579d2 100644 --- a/apps/api/src/helper/object.helper.ts +++ b/apps/api/src/helper/object.helper.ts @@ -1,4 +1,5 @@ import { Big } from 'big.js'; +import jsonpath from 'jsonpath'; import { cloneDeep, isArray, isObject } from 'lodash'; export function hasNotDefinedValuesInObject(aObject: Object): boolean { @@ -31,6 +32,16 @@ export function nullifyValuesInObjects(aObjects: T[], keys: string[]): T[] { }); } +export function query({ + object, + pathExpression +}: { + object: object; + pathExpression: string; +}) { + return jsonpath.query(object, pathExpression); +} + export function redactAttributes({ isFirstRun = true, object, diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index a91aa6e69..5f9d1055d 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -102,7 +102,6 @@ export class ConfigurationService { ROOT_URL: url({ default: environment.rootUrl }), - STRIPE_PUBLIC_KEY: str({ default: '' }), STRIPE_SECRET_KEY: str({ default: '' }), TWITTER_ACCESS_TOKEN: str({ default: 'dummyAccessToken' }), TWITTER_ACCESS_TOKEN_SECRET: str({ default: 'dummyAccessTokenSecret' }), diff --git a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts index 3cf935b1e..6030e62d4 100644 --- a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts +++ b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts @@ -18,7 +18,7 @@ import { import { Injectable } from '@nestjs/common'; import { DataSource, SymbolProfile } from '@prisma/client'; -import * as Alphavantage from 'alphavantage'; +import Alphavantage from 'alphavantage'; import { format, isAfter, isBefore, parse } from 'date-fns'; import { AlphaVantageHistoricalResponse } from './interfaces/interfaces'; diff --git a/apps/api/src/services/data-provider/manual/manual.service.ts b/apps/api/src/services/data-provider/manual/manual.service.ts index f18da49ab..7392f0914 100644 --- a/apps/api/src/services/data-provider/manual/manual.service.ts +++ b/apps/api/src/services/data-provider/manual/manual.service.ts @@ -1,3 +1,4 @@ +import { query } from '@ghostfolio/api/helper/object.helper'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { DataProviderInterface, @@ -26,7 +27,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { DataSource, SymbolProfile } from '@prisma/client'; import * as cheerio from 'cheerio'; import { addDays, format, isBefore } from 'date-fns'; -import * as jsonpath from 'jsonpath'; @Injectable() export class ManualService implements DataProviderInterface { @@ -286,9 +286,14 @@ export class ManualService implements DataProviderInterface { let value: string; if (response.headers.get('content-type')?.includes('application/json')) { - const data = await response.json(); + const object = await response.json(); - value = String(jsonpath.query(data, scraperConfiguration.selector)[0]); + value = String( + query({ + object, + pathExpression: scraperConfiguration.selector + })[0] + ); } else { const $ = cheerio.load(await response.text()); diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index 3c03744f1..57c58898e 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -52,7 +52,6 @@ export interface Environment extends CleanedEnvAccessors { REDIS_PORT: number; REQUEST_TIMEOUT: number; ROOT_URL: string; - STRIPE_PUBLIC_KEY: string; STRIPE_SECRET_KEY: string; TWITTER_ACCESS_TOKEN: string; TWITTER_ACCESS_TOKEN_SECRET: string; diff --git a/apps/api/tsconfig.app.json b/apps/api/tsconfig.app.json index 655120714..d38ef826f 100644 --- a/apps/api/tsconfig.app.json +++ b/apps/api/tsconfig.app.json @@ -4,6 +4,7 @@ "outDir": "../../dist/out-tsc", "types": ["node"], "emitDecoratorMetadata": true, + "moduleResolution": "node10", "target": "es2021", "module": "commonjs" }, diff --git a/apps/api/tsconfig.spec.json b/apps/api/tsconfig.spec.json index 148da8555..934e28503 100644 --- a/apps/api/tsconfig.spec.json +++ b/apps/api/tsconfig.spec.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "../../dist/out-tsc", "module": "commonjs", + "moduleResolution": "node10", "types": ["jest", "node"] }, "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts", "jest.config.ts"] diff --git a/apps/client/project.json b/apps/client/project.json index 0d3571cdf..09968d23f 100644 --- a/apps/client/project.json +++ b/apps/client/project.json @@ -75,7 +75,6 @@ "ngswConfigPath": "apps/client/ngsw-config.json", "optimization": false, "polyfills": "apps/client/src/polyfills.ts", - "scripts": ["node_modules/marked/marked.min.js"], "serviceWorker": true, "sourceMap": true, "styles": [ 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 fe2c81199..76548c45d 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 @@ -4,7 +4,6 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; import { NotificationService } from '@ghostfolio/ui/notifications'; import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard'; -import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -36,7 +35,6 @@ import ms from 'ms'; changeDetection: ChangeDetectionStrategy.OnPush, imports: [ ClipboardModule, - CommonModule, IonIcon, MatButtonModule, MatMenuModule, diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts index 57ee57f19..abea236b8 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts @@ -29,7 +29,6 @@ import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplet import { GfValueComponent } from '@ghostfolio/ui/value'; import { TextFieldModule } from '@angular/cdk/text-field'; -import { CommonModule } from '@angular/common'; import { HttpErrorResponse } from '@angular/common/http'; import { ChangeDetectionStrategy, @@ -95,7 +94,6 @@ import { AssetProfileDialogParams } from './interfaces/interfaces'; changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'd-flex flex-column h-100' }, imports: [ - CommonModule, FormsModule, GfCurrencySelectorComponent, GfEntityLogoComponent, diff --git a/apps/client/src/app/components/admin-users/admin-users.component.ts b/apps/client/src/app/components/admin-users/admin-users.component.ts index 99fbe7901..1722b498f 100644 --- a/apps/client/src/app/components/admin-users/admin-users.component.ts +++ b/apps/client/src/app/components/admin-users/admin-users.component.ts @@ -18,6 +18,7 @@ import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { NotificationService } from '@ghostfolio/ui/notifications'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfValueComponent } from '@ghostfolio/ui/value'; @@ -39,7 +40,7 @@ import { PageEvent } from '@angular/material/paginator'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { IonIcon } from '@ionic/angular/standalone'; import { differenceInSeconds, @@ -69,7 +70,8 @@ import { takeUntil } from 'rxjs/operators'; MatMenuModule, MatPaginatorModule, MatTableModule, - NgxSkeletonLoaderModule + NgxSkeletonLoaderModule, + RouterModule ], selector: 'gf-admin-users', styleUrls: ['./admin-users.scss'], @@ -88,6 +90,8 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit { public info: InfoItem; public isLoading = false; public pageSize = DEFAULT_PAGE_SIZE; + public routerLinkAdminControlUsers = + internalRoutes.adminControl.subRoutes.users.routerLink; public totalItems = 0; public user: User; @@ -136,11 +140,13 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit { ]; } - this.route.queryParams + this.route.paramMap .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((params) => { - if (params['userDetailDialog'] && params['userId']) { - this.openUserDetailDialog(params['userId']); + const userId = params.get('userId'); + + if (userId) { + this.openUserDetailDialog(userId); } }); @@ -248,9 +254,9 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit { } public onOpenUserDetailDialog(userId: string) { - this.router.navigate([], { - queryParams: { userId, userDetailDialog: true } - }); + this.router.navigate( + internalRoutes.adminControl.subRoutes.users.routerLink.concat(userId) + ); } public ngOnDestroy() { @@ -301,7 +307,9 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit { .afterClosed() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { - this.router.navigate(['.'], { relativeTo: this.route }); + this.router.navigate( + internalRoutes.adminControl.subRoutes.users.routerLink + ); }); } } diff --git a/apps/client/src/app/components/admin-users/admin-users.html b/apps/client/src/app/components/admin-users/admin-users.html index ebcdc6f5f..0f9789feb 100644 --- a/apps/client/src/app/components/admin-users/admin-users.html +++ b/apps/client/src/app/components/admin-users/admin-users.html @@ -215,9 +215,9 @@ - + @if (hasPermissionToImpersonateAllUsers) {