Browse Source

Merge branch 'main' into task/improve-style-of-close-button-in-dialogs

pull/8268/head
Thomas Kaul 1 day ago
committed by GitHub
parent
commit
2d509d01c1
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      .github/workflows/build-code.yml
  2. 11
      CHANGELOG.md
  3. 2
      DEVELOPMENT.md
  4. 16
      apps/api/src/app/__snapshots__/app.module.spec.ts.snap
  5. 2
      apps/api/src/app/activities/activities.module.ts
  6. 16
      apps/api/src/app/app.module.ts
  7. 17
      apps/api/src/app/asset/asset.module.ts
  8. 19
      apps/api/src/app/auth-device/auth-device.module.ts
  9. 4
      apps/api/src/app/auth/auth.module.ts
  10. 2
      apps/api/src/app/auth/web-auth.service.ts
  11. 3
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts
  12. 3
      apps/api/src/app/endpoints/asset-profiles/asset.controller.ts
  13. 10
      apps/api/src/app/endpoints/auth-devices/auth-devices.controller.ts
  14. 11
      apps/api/src/app/endpoints/auth-devices/auth-devices.module.ts
  15. 0
      apps/api/src/app/endpoints/cache/cache.controller.ts
  16. 0
      apps/api/src/app/endpoints/cache/cache.module.ts
  17. 18
      apps/api/src/app/endpoints/exchange-rates/exchange-rates.controller.ts
  18. 13
      apps/api/src/app/endpoints/exchange-rates/exchange-rates.module.ts
  19. 2
      apps/api/src/app/endpoints/exchange-rates/exchange-rates.service.ts
  20. 0
      apps/api/src/app/endpoints/info/info.controller.ts
  21. 0
      apps/api/src/app/endpoints/info/info.module.ts
  22. 0
      apps/api/src/app/endpoints/info/info.service.ts
  23. 0
      apps/api/src/app/endpoints/logo/get-logo.dto.ts
  24. 0
      apps/api/src/app/endpoints/logo/logo.controller.ts
  25. 0
      apps/api/src/app/endpoints/logo/logo.module.ts
  26. 0
      apps/api/src/app/endpoints/logo/logo.service.ts
  27. 14
      apps/api/src/app/exchange-rate/exchange-rate.module.ts
  28. 2
      apps/api/src/app/import/import.module.ts
  29. 12
      apps/api/src/services/auth-device/auth-device.module.ts
  30. 0
      apps/api/src/services/auth-device/auth-device.service.ts
  31. 18
      apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts
  32. 42
      apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html
  33. 10
      apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
  34. 5
      apps/client/src/app/components/user-account-membership/user-account-membership.html
  35. 2
      apps/client/src/app/services/web-authn.service.ts
  36. 14
      libs/common/src/lib/config.ts
  37. 2
      libs/common/src/lib/interfaces/index.ts
  38. 3
      libs/common/src/lib/interfaces/responses/exchange-rate-response.interface.ts
  39. 13
      libs/ui/src/lib/services/data.service.ts
  40. 30
      package-lock.json
  41. 10
      package.json

2
.github/workflows/build-code.yml

@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node_version: node_version:
- 22.23.2 - 22.23.4
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4

11
CHANGELOG.md

@ -11,8 +11,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the style of the close button in the dialog footer component - Improved the style of the close button in the dialog footer component
- Improved the style of the close button in the dialog header component - Improved the style of the close button in the dialog header component
## 3.90.0 - 2026-10-21
### Changed
- Extended the variations of the interstitials for the subscription by a trial
- Deprecated the `api/v1/auth-device` endpoint in favor of the `api/v1/auth-devices` endpoint
- Deprecated the `api/v1/exchange-rate` endpoint in favor of the `api/v1/exchange-rates` endpoint
- Upgraded the `Node.js` engine from version `>=22.22.3` to `>=22.23.4` (`package.json`)
- Upgraded `@modelcontextprotocol/core` and `@modelcontextprotocol/server` from version `2.0.0` to `2.3.1` - Upgraded `@modelcontextprotocol/core` and `@modelcontextprotocol/server` from version `2.0.0` to `2.3.1`
- Upgraded `@modelcontextprotocol/node` from version `2.0.0` to `2.1.1` - Upgraded `@modelcontextprotocol/node` from version `2.0.0` to `2.1.1`
- Upgraded `@rekog/mcp-nest` from version `2.0.7` to `2.0.8`
- Upgraded `undici` from version `8.11.2` to `8.12.2`
## 3.89.0 - 2026-10-20 ## 3.89.0 - 2026-10-20

2
DEVELOPMENT.md

@ -5,7 +5,7 @@
### Prerequisites ### Prerequisites
- [Docker](https://www.docker.com/products/docker-desktop) - [Docker](https://www.docker.com/products/docker-desktop)
- [Node.js](https://nodejs.org/en/download) (version `>=22.18.0`) - [Node.js](https://nodejs.org/en/download) (version `>=22.23.4`)
- Create a local copy of this Git repository (clone) - Create a local copy of this Git repository (clone)
- Copy the file `.env.dev` to `.env` and populate it with your data (`cp .env.dev .env`) - Copy the file `.env.dev` to `.env` and populate it with your data (`cp .env.dev .env`)

16
apps/api/src/app/__snapshots__/app.module.spec.ts.snap

@ -116,6 +116,15 @@ exports[`AppModule should keep the security of each route 1`] = `
"interceptors": [], "interceptors": [],
"permission": "deleteAuthDevice", "permission": "deleteAuthDevice",
}, },
"DELETE /auth-devices/:id": {
"allowDuringImpersonation": true,
"guards": [
"AuthGuard('jwt')",
"HasPermissionGuard",
],
"interceptors": [],
"permission": "deleteAuthDevice",
},
"DELETE /benchmarks/:dataSource/:symbol": { "DELETE /benchmarks/:dataSource/:symbol": {
"allowDuringImpersonation": true, "allowDuringImpersonation": true,
"guards": [ "guards": [
@ -489,6 +498,13 @@ exports[`AppModule should keep the security of each route 1`] = `
], ],
"interceptors": [], "interceptors": [],
}, },
"GET /exchange-rates/:symbol/:dateString": {
"guards": [
"AuthGuard('jwt')",
"HasPermissionGuard",
],
"interceptors": [],
},
"GET /export": { "GET /export": {
"guards": [ "guards": [
"AuthGuard('jwt')", "AuthGuard('jwt')",

2
apps/api/src/app/activities/activities.module.ts

@ -1,6 +1,5 @@
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module'; import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
@ -29,7 +28,6 @@ import { ActivitiesService } from './activities.service';
ApiModule, ApiModule,
AssetProfileSplitModule, AssetProfileSplitModule,
BenchmarkModule, BenchmarkModule,
CacheModule,
DataGatheringQueueModule, DataGatheringQueueModule,
DataProviderModule, DataProviderModule,
ExchangeRateDataModule, ExchangeRateDataModule,

16
apps/api/src/app/app.module.ts

@ -40,16 +40,18 @@ import { AccountModule } from './account/account.module';
import { ActivitiesModule } from './activities/activities.module'; import { ActivitiesModule } from './activities/activities.module';
import { AdminModule } from './admin/admin.module'; import { AdminModule } from './admin/admin.module';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AssetModule } from './asset/asset.module';
import { AuthDeviceModule } from './auth-device/auth-device.module';
import { AuthModule } from './auth/auth.module'; import { AuthModule } from './auth/auth.module';
import { CacheModule } from './cache/cache.module';
import { AiModule } from './endpoints/ai/ai.module'; import { AiModule } from './endpoints/ai/ai.module';
import { ApiKeysModule } from './endpoints/api-keys/api-keys.module'; import { ApiKeysModule } from './endpoints/api-keys/api-keys.module';
import { AssetProfilesModule } from './endpoints/asset-profiles/asset-profiles.module'; import { AssetProfilesModule } from './endpoints/asset-profiles/asset-profiles.module';
import { AssetsModule } from './endpoints/assets/assets.module'; import { AssetsModule } from './endpoints/assets/assets.module';
import { AuthDevicesModule } from './endpoints/auth-devices/auth-devices.module';
import { BenchmarksModule } from './endpoints/benchmarks/benchmarks.module'; import { BenchmarksModule } from './endpoints/benchmarks/benchmarks.module';
import { CacheModule } from './endpoints/cache/cache.module';
import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module'; import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module';
import { ExchangeRatesModule } from './endpoints/exchange-rates/exchange-rates.module';
import { InfoModule } from './endpoints/info/info.module';
import { LogoModule } from './endpoints/logo/logo.module';
import { MarketDataModule } from './endpoints/market-data/market-data.module'; import { MarketDataModule } from './endpoints/market-data/market-data.module';
import { McpModule } from './endpoints/mcp/mcp.module'; import { McpModule } from './endpoints/mcp/mcp.module';
import { PlatformsModule } from './endpoints/platforms/platforms.module'; import { PlatformsModule } from './endpoints/platforms/platforms.module';
@ -57,12 +59,9 @@ import { PublicModule } from './endpoints/public/public.module';
import { SitemapModule } from './endpoints/sitemap/sitemap.module'; import { SitemapModule } from './endpoints/sitemap/sitemap.module';
import { TagsModule } from './endpoints/tags/tags.module'; import { TagsModule } from './endpoints/tags/tags.module';
import { WatchlistModule } from './endpoints/watchlist/watchlist.module'; import { WatchlistModule } from './endpoints/watchlist/watchlist.module';
import { ExchangeRateModule } from './exchange-rate/exchange-rate.module';
import { ExportModule } from './export/export.module'; import { ExportModule } from './export/export.module';
import { HealthModule } from './health/health.module'; import { HealthModule } from './health/health.module';
import { ImportModule } from './import/import.module'; import { ImportModule } from './import/import.module';
import { InfoModule } from './info/info.module';
import { LogoModule } from './logo/logo.module';
import { PortfolioModule } from './portfolio/portfolio.module'; import { PortfolioModule } from './portfolio/portfolio.module';
import { SubscriptionModule } from './subscription/subscription.module'; import { SubscriptionModule } from './subscription/subscription.module';
import { SymbolModule } from './symbol/symbol.module'; import { SymbolModule } from './symbol/symbol.module';
@ -78,9 +77,8 @@ import { UserModule } from './user/user.module';
AiModule, AiModule,
ApiKeysModule, ApiKeysModule,
AssetProfilesModule, AssetProfilesModule,
AssetModule,
AssetsModule, AssetsModule,
AuthDeviceModule, AuthDevicesModule,
AuthModule, AuthModule,
BenchmarksModule, BenchmarksModule,
BullBoardModule.forRoot({ BullBoardModule.forRoot({
@ -119,8 +117,8 @@ import { UserModule } from './user/user.module';
DataProviderModule, DataProviderModule,
EventEmitterModule.forRoot(), EventEmitterModule.forRoot(),
EventsModule, EventsModule,
ExchangeRateModule,
ExchangeRateDataModule, ExchangeRateDataModule,
ExchangeRatesModule,
ExportModule, ExportModule,
GhostfolioModule, GhostfolioModule,
HealthModule, HealthModule,

17
apps/api/src/app/asset/asset.module.ts

@ -1,17 +0,0 @@
import { AssetProfilesModule } from '@ghostfolio/api/app/endpoints/asset-profiles/asset-profiles.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { Module } from '@nestjs/common';
import { AssetController } from './asset.controller';
@Module({
controllers: [AssetController],
imports: [
AssetProfilesModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
]
})
export class AssetModule {}

19
apps/api/src/app/auth-device/auth-device.module.ts

@ -1,19 +0,0 @@
import { AuthDeviceController } from '@ghostfolio/api/app/auth-device/auth-device.controller';
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
@Module({
controllers: [AuthDeviceController],
imports: [
JwtModule.register({
secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: '180 days' }
}),
PrismaModule
],
providers: [AuthDeviceService]
})
export class AuthDeviceModule {}

4
apps/api/src/app/auth/auth.module.ts

@ -1,9 +1,9 @@
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service'; import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ApiKeyService } from '@ghostfolio/api/services/api-key/api-key.service'; import { ApiKeyService } from '@ghostfolio/api/services/api-key/api-key.service';
import { ApiModule } from '@ghostfolio/api/services/api/api.module'; import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { AuthDeviceModule } from '@ghostfolio/api/services/auth-device/auth-device.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { FetchModule } from '@ghostfolio/api/services/fetch/fetch.module'; import { FetchModule } from '@ghostfolio/api/services/fetch/fetch.module';
@ -29,6 +29,7 @@ import { OidcStrategy } from './oidc.strategy';
controllers: [AuthController], controllers: [AuthController],
imports: [ imports: [
ApiModule, ApiModule,
AuthDeviceModule,
ConfigurationModule, ConfigurationModule,
FetchModule, FetchModule,
JwtModule.register({ JwtModule.register({
@ -45,7 +46,6 @@ import { OidcStrategy } from './oidc.strategy';
providers: [ providers: [
ApiKeyService, ApiKeyService,
ApiKeyStrategy, ApiKeyStrategy,
AuthDeviceService,
AuthService, AuthService,
GoogleStrategy, GoogleStrategy,
JwtStrategy, JwtStrategy,

2
apps/api/src/app/auth/web-auth.service.ts

@ -1,7 +1,7 @@
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { PortfolioSnapshotValue } from '@ghostfolio/api/app/portfolio/interfaces/snapshot-value.interface'; import { PortfolioSnapshotValue } from '@ghostfolio/api/app/portfolio/interfaces/snapshot-value.interface';
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { AuthDeviceService } from '@ghostfolio/api/services/auth-device/auth-device.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { RedisCacheService } from '@ghostfolio/api/services/redis-cache/redis-cache.service'; import { RedisCacheService } from '@ghostfolio/api/services/redis-cache/redis-cache.service';

3
apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts

@ -15,9 +15,10 @@ import { Module } from '@nestjs/common';
import { AssetProfilesController } from './asset-profiles.controller'; import { AssetProfilesController } from './asset-profiles.controller';
import { AssetProfilesService } from './asset-profiles.service'; import { AssetProfilesService } from './asset-profiles.service';
import { AssetController } from './asset.controller';
@Module({ @Module({
controllers: [AssetProfilesController], controllers: [AssetController, AssetProfilesController],
exports: [AssetProfilesService], exports: [AssetProfilesService],
imports: [ imports: [
ActivitiesModule, ActivitiesModule,

3
apps/api/src/app/asset/asset.controller.ts → apps/api/src/app/endpoints/asset-profiles/asset.controller.ts

@ -1,4 +1,3 @@
import { AssetProfilesService } from '@ghostfolio/api/app/endpoints/asset-profiles/asset-profiles.service';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import type { AssetResponse } from '@ghostfolio/common/interfaces'; import type { AssetResponse } from '@ghostfolio/common/interfaces';
@ -7,6 +6,8 @@ import { Controller, Get, Param, UseInterceptors } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { pick } from 'lodash-es'; import { pick } from 'lodash-es';
import { AssetProfilesService } from './asset-profiles.service';
@Controller('asset') @Controller('asset')
export class AssetController { export class AssetController {
public constructor( public constructor(

10
apps/api/src/app/auth-device/auth-device.controller.ts → apps/api/src/app/endpoints/auth-devices/auth-devices.controller.ts

@ -1,7 +1,7 @@
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { AllowDuringImpersonation } from '@ghostfolio/api/decorators/allow-during-impersonation.decorator'; import { AllowDuringImpersonation } from '@ghostfolio/api/decorators/allow-during-impersonation.decorator';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { AuthDeviceService } from '@ghostfolio/api/services/auth-device/auth-device.service';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
@ -18,8 +18,12 @@ import { AuthGuard } from '@nestjs/passport';
import { getReasonPhrase, StatusCodes } from 'http-status-codes'; import { getReasonPhrase, StatusCodes } from 'http-status-codes';
@AllowDuringImpersonation() @AllowDuringImpersonation()
@Controller('auth-device') @Controller([
export class AuthDeviceController { 'auth-devices',
/** @deprecated */
'auth-device'
])
export class AuthDevicesController {
public constructor( public constructor(
private readonly authDeviceService: AuthDeviceService, private readonly authDeviceService: AuthDeviceService,
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser

11
apps/api/src/app/endpoints/auth-devices/auth-devices.module.ts

@ -0,0 +1,11 @@
import { AuthDeviceModule } from '@ghostfolio/api/services/auth-device/auth-device.module';
import { Module } from '@nestjs/common';
import { AuthDevicesController } from './auth-devices.controller';
@Module({
controllers: [AuthDevicesController],
imports: [AuthDeviceModule]
})
export class AuthDevicesModule {}

0
apps/api/src/app/cache/cache.controller.ts → apps/api/src/app/endpoints/cache/cache.controller.ts

0
apps/api/src/app/cache/cache.module.ts → apps/api/src/app/endpoints/cache/cache.module.ts

18
apps/api/src/app/exchange-rate/exchange-rate.controller.ts → apps/api/src/app/endpoints/exchange-rates/exchange-rates.controller.ts

@ -1,5 +1,5 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { DataProviderHistoricalResponse } from '@ghostfolio/common/interfaces'; import { ExchangeRateResponse } from '@ghostfolio/common/interfaces';
import { import {
Controller, Controller,
@ -12,12 +12,16 @@ import { AuthGuard } from '@nestjs/passport';
import { parseISO } from 'date-fns'; import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { ExchangeRateService } from './exchange-rate.service'; import { ExchangeRatesService } from './exchange-rates.service';
@Controller('exchange-rate') @Controller([
export class ExchangeRateController { 'exchange-rates',
/** @deprecated */
'exchange-rate'
])
export class ExchangeRatesController {
public constructor( public constructor(
private readonly exchangeRateService: ExchangeRateService private readonly exchangeRatesService: ExchangeRatesService
) {} ) {}
@Get(':symbol/:dateString') @Get(':symbol/:dateString')
@ -25,10 +29,10 @@ export class ExchangeRateController {
public async getExchangeRate( public async getExchangeRate(
@Param('dateString') dateString: string, @Param('dateString') dateString: string,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<DataProviderHistoricalResponse> { ): Promise<ExchangeRateResponse> {
const date = parseISO(dateString); const date = parseISO(dateString);
const exchangeRate = await this.exchangeRateService.getExchangeRate({ const exchangeRate = await this.exchangeRatesService.getExchangeRate({
date, date,
symbol symbol
}); });

13
apps/api/src/app/endpoints/exchange-rates/exchange-rates.module.ts

@ -0,0 +1,13 @@
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { Module } from '@nestjs/common';
import { ExchangeRatesController } from './exchange-rates.controller';
import { ExchangeRatesService } from './exchange-rates.service';
@Module({
controllers: [ExchangeRatesController],
imports: [ExchangeRateDataModule],
providers: [ExchangeRatesService]
})
export class ExchangeRatesModule {}

2
apps/api/src/app/exchange-rate/exchange-rate.service.ts → apps/api/src/app/endpoints/exchange-rates/exchange-rates.service.ts

@ -3,7 +3,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
export class ExchangeRateService { export class ExchangeRatesService {
public constructor( public constructor(
private readonly exchangeRateDataService: ExchangeRateDataService private readonly exchangeRateDataService: ExchangeRateDataService
) {} ) {}

0
apps/api/src/app/info/info.controller.ts → apps/api/src/app/endpoints/info/info.controller.ts

0
apps/api/src/app/info/info.module.ts → apps/api/src/app/endpoints/info/info.module.ts

0
apps/api/src/app/info/info.service.ts → apps/api/src/app/endpoints/info/info.service.ts

0
apps/api/src/app/logo/get-logo.dto.ts → apps/api/src/app/endpoints/logo/get-logo.dto.ts

0
apps/api/src/app/logo/logo.controller.ts → apps/api/src/app/endpoints/logo/logo.controller.ts

0
apps/api/src/app/logo/logo.module.ts → apps/api/src/app/endpoints/logo/logo.module.ts

0
apps/api/src/app/logo/logo.service.ts → apps/api/src/app/endpoints/logo/logo.service.ts

14
apps/api/src/app/exchange-rate/exchange-rate.module.ts

@ -1,14 +0,0 @@
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { Module } from '@nestjs/common';
import { ExchangeRateController } from './exchange-rate.controller';
import { ExchangeRateService } from './exchange-rate.service';
@Module({
controllers: [ExchangeRateController],
exports: [ExchangeRateService],
imports: [ExchangeRateDataModule],
providers: [ExchangeRateService]
})
export class ExchangeRateModule {}

2
apps/api/src/app/import/import.module.ts

@ -1,6 +1,5 @@
import { AccountModule } from '@ghostfolio/api/app/account/account.module'; import { AccountModule } from '@ghostfolio/api/app/account/account.module';
import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module'; import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module';
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module'; import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
@ -28,7 +27,6 @@ import { ImportService } from './import.service';
AccountModule, AccountModule,
ActivitiesModule, ActivitiesModule,
ApiModule, ApiModule,
CacheModule,
ConfigurationModule, ConfigurationModule,
DataGatheringQueueModule, DataGatheringQueueModule,
DataProviderModule, DataProviderModule,

12
apps/api/src/services/auth-device/auth-device.module.ts

@ -0,0 +1,12 @@
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common';
import { AuthDeviceService } from './auth-device.service';
@Module({
exports: [AuthDeviceService],
imports: [PrismaModule],
providers: [AuthDeviceService]
})
export class AuthDeviceModule {}

0
apps/api/src/app/auth-device/auth-device.service.ts → apps/api/src/services/auth-device/auth-device.service.ts

18
apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts

@ -1,3 +1,5 @@
import { TRY_SUBSCRIPTION_MAIL_HREF } from '@ghostfolio/common/config';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { publicRoutes } from '@ghostfolio/common/routes/routes'; import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { GfMembershipCardComponent } from '@ghostfolio/ui/membership-card'; import { GfMembershipCardComponent } from '@ghostfolio/ui/membership-card';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
@ -51,6 +53,7 @@ export class GfSubscriptionInterstitialDialogComponent implements OnInit {
public remainingSkipButtonDelay = public remainingSkipButtonDelay =
GfSubscriptionInterstitialDialogComponent.SKIP_BUTTON_DELAY_IN_SECONDS; GfSubscriptionInterstitialDialogComponent.SKIP_BUTTON_DELAY_IN_SECONDS;
public routerLinkPricing = publicRoutes.pricing.routerLink; public routerLinkPricing = publicRoutes.pricing.routerLink;
public trySubscriptionMailHref = TRY_SUBSCRIPTION_MAIL_HREF;
public variantIndex: number; public variantIndex: number;
public constructor( public constructor(
@ -59,9 +62,18 @@ export class GfSubscriptionInterstitialDialogComponent implements OnInit {
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
public dialogRef: MatDialogRef<GfSubscriptionInterstitialDialogComponent> public dialogRef: MatDialogRef<GfSubscriptionInterstitialDialogComponent>
) { ) {
this.variantIndex = Math.floor( const isEligibleForTrial =
Math.random() * GfSubscriptionInterstitialDialogComponent.VARIANTS_COUNT !this.data.user?.subscription?.subscribedAt &&
); hasPermission(
this.data.user?.permissions,
permissions.updateUserSettings
);
const variantsCount = isEligibleForTrial
? GfSubscriptionInterstitialDialogComponent.VARIANTS_COUNT + 1
: GfSubscriptionInterstitialDialogComponent.VARIANTS_COUNT;
this.variantIndex = Math.floor(Math.random() * variantsCount);
addIcons({ arrowForwardOutline, checkmarkCircleOutline }); addIcons({ arrowForwardOutline, checkmarkCircleOutline });
} }

42
apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html

@ -195,4 +195,46 @@
</a> </a>
</div> </div>
} }
@case (4) {
<div class="flex-grow-1 pt-0" mat-dialog-content>
<div class="intro-container">
<div class="intro-inner-container mx-auto">
<div class="h-100 intro w-100"></div>
</div>
</div>
<div>
<h5 class="align-items-center d-flex justify-content-center mb-3">
<span>Ghostfolio Premium</span>
<gf-premium-indicator class="ms-1" [enableLink]="false" />
</h5>
<div class="fw-normal h5 mb-3 text-center" i18n>
Request a free trial
</div>
<p class="m-0 text-center" i18n>
No credit card required. The trial ends automatically.
</p>
</div>
</div>
<div class="justify-content-end" mat-dialog-actions>
<button
mat-button
[disabled]="remainingSkipButtonDelay > 0"
(click)="closeDialog()"
>
<ng-container i18n>Skip</ng-container>
@if (remainingSkipButtonDelay > 0) {
({{ remainingSkipButtonDelay }})
}
</button>
<a
color="primary"
mat-flat-button
[href]="trySubscriptionMailHref"
(click)="closeDialog()"
>
<span i18n>Try Premium</span>
<ion-icon class="ms-1" name="arrow-forward-outline" />
</a>
</div>
}
} }

10
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts

@ -1,5 +1,5 @@
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { E_MAIL_LINE_BREAK } from '@ghostfolio/common/config'; import { TRY_SUBSCRIPTION_MAIL_HREF } from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { getDateFormatString } from '@ghostfolio/common/helper'; import { getDateFormatString } from '@ghostfolio/common/helper';
import { User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
@ -49,13 +49,7 @@ export class GfUserAccountMembershipComponent {
protected hasPermissionToCreateApiKey: boolean; protected hasPermissionToCreateApiKey: boolean;
protected hasPermissionToUpdateUserSettings: boolean; protected hasPermissionToUpdateUserSettings: boolean;
protected price: number; protected price: number;
protected readonly trySubscriptionMailHref = `mailto:hi@ghostfol.io?subject=Ghostfolio Premium Trial&body=${[ protected readonly trySubscriptionMailHref = TRY_SUBSCRIPTION_MAIL_HREF;
'Hello',
'',
'I am interested in Ghostfolio Premium. Can you please send me a coupon code to try it for some time?',
'',
'Kind regards'
].join(E_MAIL_LINE_BREAK)}`;
protected user: User; protected user: User;
private couponId: string | undefined; private couponId: string | undefined;

5
apps/client/src/app/components/user-account-membership/user-account-membership.html

@ -50,7 +50,10 @@
} }
} }
<div class="align-items-center d-flex justify-content-center mt-4"> <div class="align-items-center d-flex justify-content-center mt-4">
@if (!user?.subscription?.subscribedAt) { @if (
!user?.subscription?.subscribedAt &&
hasPermissionToUpdateUserSettings
) {
<a <a
class="mx-1" class="mx-1"
mat-stroked-button mat-stroked-button

2
apps/client/src/app/services/web-authn.service.ts

@ -66,7 +66,7 @@ export class WebAuthnService {
const deviceId = this.getDeviceId(); const deviceId = this.getDeviceId();
return this.http return this.http
.delete<AuthDeviceDto>(`/api/v1/auth-device/${deviceId}`) .delete<AuthDeviceDto>(`/api/v1/auth-devices/${deviceId}`)
.pipe( .pipe(
catchError((error) => { catchError((error) => {
console.warn(`Could not deregister device ${deviceId}`, error); console.warn(`Could not deregister device ${deviceId}`, error);

14
libs/common/src/lib/config.ts

@ -422,4 +422,18 @@ export const THROTTLE_DEFAULT_TTL = ms('1 minute');
export const THROTTLE_SIGNUP_LIMIT = 5; export const THROTTLE_SIGNUP_LIMIT = 5;
export const THROTTLE_SIGNUP_TTL = ms('1 hour'); export const THROTTLE_SIGNUP_TTL = ms('1 hour');
export const TRY_SUBSCRIPTION_MAIL_HREF = `mailto:hi@ghostfol.io?subject=${encodeURIComponent(
'Ghostfolio Premium Trial'
)}&body=${[
'Hello',
'',
'I am interested in Ghostfolio Premium. Can you please send me a coupon code to try it for some time?',
'',
'Kind regards'
]
.map((line) => {
return encodeURIComponent(line);
})
.join(E_MAIL_LINE_BREAK)}`;
export const UNKNOWN_KEY = 'UNKNOWN'; export const UNKNOWN_KEY = 'UNKNOWN';

2
libs/common/src/lib/interfaces/index.ts

@ -67,6 +67,7 @@ import type {
} from './responses/data-provider-response.interface'; } from './responses/data-provider-response.interface';
import type { DividendsResponse } from './responses/dividends-response.interface'; import type { DividendsResponse } from './responses/dividends-response.interface';
import type { ResponseError } from './responses/errors.interface'; import type { ResponseError } from './responses/errors.interface';
import type { ExchangeRateResponse } from './responses/exchange-rate-response.interface';
import type { ExportResponse } from './responses/export-response.interface'; import type { ExportResponse } from './responses/export-response.interface';
import type { HistoricalResponse } from './responses/historical-response.interface'; import type { HistoricalResponse } from './responses/historical-response.interface';
import type { ImportResponse } from './responses/import-response.interface'; import type { ImportResponse } from './responses/import-response.interface';
@ -150,6 +151,7 @@ export {
DataProviderResponse, DataProviderResponse,
DividendsResponse, DividendsResponse,
EnhancedAssetProfile, EnhancedAssetProfile,
ExchangeRateResponse,
ExportResponse, ExportResponse,
Filter, Filter,
FilterGroup, FilterGroup,

3
libs/common/src/lib/interfaces/responses/exchange-rate-response.interface.ts

@ -0,0 +1,3 @@
export interface ExchangeRateResponse {
marketPrice: number;
}

13
libs/ui/src/lib/services/data.service.ts

@ -40,7 +40,6 @@ import {
CreateActivityResponse, CreateActivityResponse,
CreateStripeCheckoutSessionResponse, CreateStripeCheckoutSessionResponse,
DataProviderHealthResponse, DataProviderHealthResponse,
DataProviderHistoricalResponse,
ExportResponse, ExportResponse,
Filter, Filter,
ImportResponse, ImportResponse,
@ -309,18 +308,6 @@ export class DataService {
); );
} }
public fetchExchangeRateForDate({
date,
symbol
}: {
date: Date;
symbol: string;
}) {
return this.http.get<DataProviderHistoricalResponse>(
`/api/v1/exchange-rate/${encodeURIComponent(symbol)}/${format(date, DATE_FORMAT, { in: utc })}`
);
}
public deleteAccess(aId: string) { public deleteAccess(aId: string) {
return this.http.delete<void>(`/api/v1/access/${aId}`); return this.http.delete<void>(`/api/v1/access/${aId}`);
} }

30
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.89.0", "version": "3.90.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.89.0", "version": "3.90.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
@ -49,7 +49,7 @@
"@openrouter/ai-sdk-provider": "3.1.0", "@openrouter/ai-sdk-provider": "3.1.0",
"@prisma/adapter-pg": "7.10.0", "@prisma/adapter-pg": "7.10.0",
"@prisma/client": "7.10.0", "@prisma/client": "7.10.0",
"@rekog/mcp-nest": "2.0.7", "@rekog/mcp-nest": "2.0.8",
"@simplewebauthn/browser": "14.0.0", "@simplewebauthn/browser": "14.0.0",
"@simplewebauthn/server": "14.0.3", "@simplewebauthn/server": "14.0.3",
"ai": "7.0.127", "ai": "7.0.127",
@ -104,7 +104,7 @@
"svgmap": "2.21.0", "svgmap": "2.21.0",
"tablemark": "4.1.0", "tablemark": "4.1.0",
"twitter-api-v2": "1.29.1", "twitter-api-v2": "1.29.1",
"undici": "8.11.2", "undici": "8.12.2",
"uuid": "14.0.2", "uuid": "14.0.2",
"yahoo-finance2": "4.0.2", "yahoo-finance2": "4.0.2",
"zod": "4.6.5", "zod": "4.6.5",
@ -149,7 +149,7 @@
"@types/jest": "30.0.0", "@types/jest": "30.0.0",
"@types/jsonpath": "0.2.4", "@types/jsonpath": "0.2.4",
"@types/lodash-es": "4.17.12", "@types/lodash-es": "4.17.12",
"@types/node": "22.20.1", "@types/node": "22.20.5",
"@types/papaparse": "5.5.2", "@types/papaparse": "5.5.2",
"@types/passport-google-oauth20": "2.0.17", "@types/passport-google-oauth20": "2.0.17",
"@types/passport-openidconnect": "0.1.3", "@types/passport-openidconnect": "0.1.3",
@ -180,7 +180,7 @@
"webpack-cli": "7.1.0" "webpack-cli": "7.1.0"
}, },
"engines": { "engines": {
"node": ">=22.22.3" "node": ">=22.23.4"
} }
}, },
"node_modules/@adobe/css-tools": { "node_modules/@adobe/css-tools": {
@ -12753,9 +12753,9 @@
} }
}, },
"node_modules/@rekog/mcp-nest": { "node_modules/@rekog/mcp-nest": {
"version": "2.0.7", "version": "2.0.8",
"resolved": "https://registry.npmjs.org/@rekog/mcp-nest/-/mcp-nest-2.0.7.tgz", "resolved": "https://registry.npmjs.org/@rekog/mcp-nest/-/mcp-nest-2.0.8.tgz",
"integrity": "sha512-8DWrSapEA9Z/YJL9dFmj7sSbemmeQmf0VkGkW6Sn1itwKFf0n9hzTiwWt8XBtZvV5JVDNCtvRZKYYKgNjDcYPg==", "integrity": "sha512-CDqzT1RBdvCNsRkDthcNPG3nxoy0GbRCrdrK+TjL+90ElL+j65lquBhUQcR37csGyeLa8uLnj/2unLqlKZRzoA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"multer": "^2.3.0", "multer": "^2.3.0",
@ -14306,9 +14306,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.20.1", "version": "22.20.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz",
"integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
@ -33913,9 +33913,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "8.11.2", "version": "8.12.2",
"resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.2.tgz",
"integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==", "integrity": "sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=22.19.0" "node": ">=22.19.0"

10
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.89.0", "version": "3.90.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio", "repository": "https://github.com/ghostfolio/ghostfolio",
@ -93,7 +93,7 @@
"@openrouter/ai-sdk-provider": "3.1.0", "@openrouter/ai-sdk-provider": "3.1.0",
"@prisma/adapter-pg": "7.10.0", "@prisma/adapter-pg": "7.10.0",
"@prisma/client": "7.10.0", "@prisma/client": "7.10.0",
"@rekog/mcp-nest": "2.0.7", "@rekog/mcp-nest": "2.0.8",
"@simplewebauthn/browser": "14.0.0", "@simplewebauthn/browser": "14.0.0",
"@simplewebauthn/server": "14.0.3", "@simplewebauthn/server": "14.0.3",
"ai": "7.0.127", "ai": "7.0.127",
@ -148,7 +148,7 @@
"svgmap": "2.21.0", "svgmap": "2.21.0",
"tablemark": "4.1.0", "tablemark": "4.1.0",
"twitter-api-v2": "1.29.1", "twitter-api-v2": "1.29.1",
"undici": "8.11.2", "undici": "8.12.2",
"uuid": "14.0.2", "uuid": "14.0.2",
"yahoo-finance2": "4.0.2", "yahoo-finance2": "4.0.2",
"zod": "4.6.5", "zod": "4.6.5",
@ -193,7 +193,7 @@
"@types/jest": "30.0.0", "@types/jest": "30.0.0",
"@types/jsonpath": "0.2.4", "@types/jsonpath": "0.2.4",
"@types/lodash-es": "4.17.12", "@types/lodash-es": "4.17.12",
"@types/node": "22.20.1", "@types/node": "22.20.5",
"@types/papaparse": "5.5.2", "@types/papaparse": "5.5.2",
"@types/passport-google-oauth20": "2.0.17", "@types/passport-google-oauth20": "2.0.17",
"@types/passport-openidconnect": "0.1.3", "@types/passport-openidconnect": "0.1.3",
@ -224,6 +224,6 @@
"webpack-cli": "7.1.0" "webpack-cli": "7.1.0"
}, },
"engines": { "engines": {
"node": ">=22.22.3" "node": ">=22.23.4"
} }
} }

Loading…
Cancel
Save