From fd2df8de573b73bac96e1e4cd4f34b2468da8ffe Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 4 Oct 2026 13:27:08 +0200 Subject: [PATCH 1/3] Task/move dividend and dividend yield in holding detail dialog from experimental to general availability (#8047) * Move dividend and dividend yield from experimental to general availability * Update changelog --- CHANGELOG.md | 1 + .../holding-detail-dialog/holding-detail-dialog.html | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dff0d90ded..614104b8f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Localized the number formatting in the chart of the holdings tab on the home page +- Moved the dividend and the dividend yield in the holding detail dialog from experimental to general availability - Changed the installation of the dependencies from `npm install` to `npm ci` in the `Dockerfile` - Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `13.3` to `14.0` - Upgraded `nestjs` from version `11.2.3` to `11.2.6` diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html index 2b378a4d60..b54cc68f7d 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -183,9 +183,7 @@ >Invested Capital - @if ( - dividendInBaseCurrency && user?.settings?.isExperimentalFeatures - ) { + @if (dividendInBaseCurrency) {
Date: Sun, 4 Oct 2026 13:27:36 +0200 Subject: [PATCH 2/3] Task/setup test for route guards (#8044) Setup test for route guards --- apps/api/src/app/app.module.spec.ts | 172 ++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 apps/api/src/app/app.module.spec.ts diff --git a/apps/api/src/app/app.module.spec.ts b/apps/api/src/app/app.module.spec.ts new file mode 100644 index 0000000000..976e08393b --- /dev/null +++ b/apps/api/src/app/app.module.spec.ts @@ -0,0 +1,172 @@ +import { + DynamicModule, + ForwardReference, + RequestMethod, + Type +} from '@nestjs/common'; +import { + GUARDS_METADATA, + METHOD_METADATA, + MODULE_METADATA, + PATH_METADATA +} from '@nestjs/common/constants'; +import { MetadataScanner } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; + +import { AppModule } from './app.module'; + +// The packages are ECMAScript modules, which Jest cannot load +jest.mock('@openrouter/ai-sdk-provider', () => { + return {}; +}); +jest.mock('ai', () => { + return {}; +}); + +type ModuleDefinition = + DynamicModule | ForwardReference<() => Type> | Promise | Type; + +/** + * The routes of the controllers which answer a request without the + * authentication of a user or of an API key, sorted by the request method and + * the path. A new route has to apply AuthGuard('api-key') or AuthGuard('jwt') + * (e.g. via the decorator RequiresScope) or has to be added here. + * + * The MCP transport, Bull Board and the static files are not covered, as they + * are not served by a controller + */ +const PUBLIC_ROUTES = [ + 'GET /asset/:dataSource/:symbol', + 'GET /assets/:languageCode/site.webmanifest', + 'GET /auth/google', + 'GET /auth/google/callback', + 'GET /auth/oidc', + 'GET /auth/oidc/callback', + 'GET /benchmarks', + 'GET /health', + 'GET /health/ai', + 'GET /health/data-enhancer/:name', + 'GET /health/data-provider/:dataSource', + 'GET /health/liveness', + 'GET /info', + 'GET /logo', + 'GET /logo/:dataSource/:symbol', + 'GET /public/:accessId/portfolio', + 'GET /sitemap.xml', + 'GET /subscription/stripe/callback', + 'POST /auth/anonymous', + 'POST /auth/webauthn/generate-authentication-options', + 'POST /auth/webauthn/verify-authentication', + 'POST /user' +]; + +/** + * Gives the controllers of the module and of each module which it imports + */ +async function getControllers( + moduleDefinition: ModuleDefinition, + visitedModules = new Set() +): Promise { + const resolvedModule = await ('forwardRef' in moduleDefinition + ? moduleDefinition.forwardRef() + : moduleDefinition); + + if (visitedModules.has(resolvedModule)) { + return []; + } + + visitedModules.add(resolvedModule); + + const isDynamicModule = 'module' in resolvedModule; + + const controllers = [ + ...(isDynamicModule + ? (resolvedModule.controllers ?? []) + : ((Reflect.getMetadata(MODULE_METADATA.CONTROLLERS, resolvedModule) ?? + []) as Type[])) + ]; + + const importedModules = isDynamicModule + ? [resolvedModule.module, ...(resolvedModule.imports ?? [])] + : ((Reflect.getMetadata(MODULE_METADATA.IMPORTS, resolvedModule) ?? + []) as ModuleDefinition[]); + + for (const importedModule of importedModules) { + controllers.push(...(await getControllers(importedModule, visitedModules))); + } + + return [...new Set(controllers)]; +} + +/** + * Gives the paths which the decorator of a controller or of a route sets + */ +function getPaths(target: object) { + return [ + (Reflect.getMetadata(PATH_METADATA, target) ?? '') as string | string[] + ].flat(); +} + +/** + * Gives the routes of the controller which apply neither AuthGuard('api-key') + * nor AuthGuard('jwt'), each as the request method and the path + */ +function getRoutesWithoutAuthentication(controller: Type) { + const authenticationGuards: unknown[] = [ + AuthGuard('api-key'), + AuthGuard('jwt') + ]; + const routeHandlersByName = controller.prototype as Record; + + return new MetadataScanner() + .getAllMethodNames(routeHandlersByName) + .flatMap((methodName) => { + const routeHandler = routeHandlersByName[methodName]; + const requestMethod = Reflect.getMetadata( + METHOD_METADATA, + routeHandler + ) as RequestMethod | undefined; + + if (requestMethod === undefined) { + return []; + } + + const guards = [controller, routeHandler].flatMap((target) => { + return (Reflect.getMetadata(GUARDS_METADATA, target) ?? + []) as unknown[]; + }); + + const hasAuthentication = guards.some((guard) => { + return authenticationGuards.includes(guard); + }); + + if (hasAuthentication) { + return []; + } + + return getPaths(controller).flatMap((controllerPath) => { + return getPaths(routeHandler).map((routePath) => { + const path = `${controllerPath}/${routePath}` + .split('/') + .filter(Boolean) + .join('/'); + + return `${RequestMethod[requestMethod]} /${path}`; + }); + }); + }); +} + +describe('AppModule', () => { + it('should require the authentication for each route which is not public', async () => { + const controllers = await getControllers(AppModule); + + const routesWithoutAuthentication = controllers + .flatMap((controller) => { + return getRoutesWithoutAuthentication(controller); + }) + .sort(); + + expect(routesWithoutAuthentication).toEqual(PUBLIC_ROUTES); + }); +}); From 4200af750fa2ac200a7ac6f629866e59a23c13ab Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 4 Oct 2026 13:30:45 +0200 Subject: [PATCH 3/3] Release 3.79.0 (#8048) --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 614104b8f8..245fadf2de 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 +## 3.79.0 - 2026-10-04 ### Added diff --git a/package-lock.json b/package-lock.json index b25373e7b4..57eb3b25a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "3.78.0", + "version": "3.79.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "3.78.0", + "version": "3.79.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index d4d7169c0d..2e7b433487 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "3.78.0", + "version": "3.79.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio",