diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ec8de136c..4bf0e5ae27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,18 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Fixed + +- Fixed the alignment of the average price in the chart of the holding detail dialog for custom asset profiles + +## 3.75.0 - 2026-09-28 + ### Changed - Extended the net performance on the analysis page to include the dividends (experimental) - Extended the net performance in the portfolio summary to include the dividends (experimental) - Improved the get quotes functionality of the _Manual_ service +- Migrated the historical market data editor dialog from `ngModel` to form control - Migrated the _ESLint_ configuration to the flat config format without `FlatCompat` - Upgraded `chartjs-chart-treemap` from version `4.2.0` to `4.2.2` ### Fixed - Fixed an issue where holdings without a quote have been valued at the unit price of the latest activity instead of the latest market price -- Fixed the alignment of the average price in the chart of the holding detail dialog for custom asset profiles +- Fixed the discovery of the _OpenID Connect_ (`OIDC`) configuration for issuer URLs with a trailing slash (experimental) ## 3.74.0 - 2026-09-27 diff --git a/apps/api/src/app/auth/auth.module.ts b/apps/api/src/app/auth/auth.module.ts index e2a71ca843..879463cc13 100644 --- a/apps/api/src/app/auth/auth.module.ts +++ b/apps/api/src/app/auth/auth.module.ts @@ -22,6 +22,7 @@ import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { GoogleStrategy } from './google.strategy'; import { JwtStrategy } from './jwt.strategy'; +import { getOidcDiscoveryUrl } from './oidc.helper'; import { OidcStrategy } from './oidc.strategy'; @Module({ @@ -94,9 +95,15 @@ import { OidcStrategy } from './oidc.strategy'; // Fetch OIDC configuration from discovery endpoint try { const response = await fetchService.fetch( - `${issuer}/.well-known/openid-configuration` + getOidcDiscoveryUrl(issuer) ); + if (!response.ok) { + throw new Error( + `OIDC discovery request failed with status ${response.status}` + ); + } + const config = (await response.json()) as { authorization_endpoint: string; token_endpoint: string; diff --git a/apps/api/src/app/auth/oidc.helper.spec.ts b/apps/api/src/app/auth/oidc.helper.spec.ts new file mode 100644 index 0000000000..9234f12c95 --- /dev/null +++ b/apps/api/src/app/auth/oidc.helper.spec.ts @@ -0,0 +1,20 @@ +import { getOidcDiscoveryUrl } from './oidc.helper'; + +describe('getOidcDiscoveryUrl', () => { + it.each([ + [ + 'https://auth.example.com', + 'https://auth.example.com/.well-known/openid-configuration' + ], + [ + 'https://auth.example.com/', + 'https://auth.example.com/.well-known/openid-configuration' + ], + [ + 'https://auth.example.com/application/o/ghostfolio/', + 'https://auth.example.com/application/o/ghostfolio/.well-known/openid-configuration' + ] + ])('creates the discovery URL for %s', (issuer, expected) => { + expect(getOidcDiscoveryUrl(issuer)).toBe(expected); + }); +}); diff --git a/apps/api/src/app/auth/oidc.helper.ts b/apps/api/src/app/auth/oidc.helper.ts new file mode 100644 index 0000000000..4e8de829fc --- /dev/null +++ b/apps/api/src/app/auth/oidc.helper.ts @@ -0,0 +1,4 @@ +export function getOidcDiscoveryUrl(issuer: string) { + // Remove trailing slashes + return `${issuer.replace(/\/+$/, '')}/.well-known/openid-configuration`; +} diff --git a/eslint.config.cjs b/eslint.config.cjs index 9c1b2024b6..1b4aa6a0c0 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -1,6 +1,7 @@ const nxEslintPlugin = require('@nx/eslint-plugin'); const importPlugin = require('eslint-plugin-import'); const storybook = require('eslint-plugin-storybook'); +const { defineConfig } = require('eslint/config'); const typescriptEslint = require('typescript-eslint'); module.exports = [ @@ -25,14 +26,12 @@ module.exports = [ ], enforceBuildableLibDependency: true } - ], - '@typescript-eslint/no-extra-semi': 'error', - 'no-extra-semi': 'off' + ] } }, ...nxEslintPlugin.configs['flat/typescript'], ...nxEslintPlugin.configs['flat/javascript'], - ...typescriptEslint.config({ + ...defineConfig({ files: ['**/*.ts'], plugins: { import: importPlugin }, extends: [ @@ -91,8 +90,6 @@ module.exports = [ } ], '@typescript-eslint/unified-signatures': 'error', - '@typescript-eslint/no-loss-of-precision': 'warn', - '@typescript-eslint/no-var-requires': 'warn', 'arrow-body-style': 'off', 'constructor-super': 'error', eqeqeq: ['error', 'smart'], diff --git a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts index 1b7b99fa21..17a5a5e612 100644 --- a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts +++ b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts @@ -7,11 +7,15 @@ import { CUSTOM_ELEMENTS_SCHEMA, DestroyRef, OnInit, - inject, - signal + inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { + FormControl, + FormGroup, + ReactiveFormsModule, + Validators +} from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core'; import { MatDatepickerModule } from '@angular/material/datepicker'; @@ -33,7 +37,6 @@ import { HistoricalMarketDataEditorDialogParams } from './interfaces/interfaces' changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'h-100' }, imports: [ - FormsModule, IonIcon, MatButtonModule, MatDatepickerModule, @@ -51,7 +54,10 @@ export class GfHistoricalMarketDataEditorDialogComponent implements OnInit { public readonly data = inject(MAT_DIALOG_DATA); - protected readonly marketPrice = signal(this.data.marketPrice); + protected readonly historicalMarketDataForm = new FormGroup({ + date: new FormControl({ disabled: true, value: this.data.dateString }), + marketPrice: new FormControl(this.data.marketPrice, Validators.required) + }); private readonly destroyRef = inject(DestroyRef); private readonly locale = @@ -84,14 +90,19 @@ export class GfHistoricalMarketDataEditorDialogComponent implements OnInit { }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ marketPrice }) => { - this.marketPrice.set(marketPrice); + this.historicalMarketDataForm.controls.marketPrice.setValue( + marketPrice + ); + + this.historicalMarketDataForm.controls.marketPrice.markAsDirty(); this.changeDetectorRef.markForCheck(); }); } public onUpdate() { - const marketPrice = this.marketPrice(); + const marketPrice = + this.historicalMarketDataForm.controls.marketPrice.value; if (isNil(marketPrice)) { return; diff --git a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html index 7e81836646..6e52114058 100644 --- a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html +++ b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html @@ -1,16 +1,14 @@ -
+

Details for {{ data.symbol }}

Date - + Market Price - + {{ data.currency }}
- - +
diff --git a/nx.json b/nx.json index 3b8c3846a1..e0a41e9835 100644 --- a/nx.json +++ b/nx.json @@ -98,6 +98,7 @@ "cache": true }, "lint": { + "inputs": ["default", "^default", "{workspaceRoot}/eslint.config.cjs"], "cache": true }, "@nx/jest:jest": { diff --git a/package-lock.json b/package-lock.json index c4896f0436..3f8ee0f880 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "3.74.0", + "version": "3.75.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "3.74.0", + "version": "3.75.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -152,8 +152,6 @@ "@types/papaparse": "5.5.2", "@types/passport-google-oauth20": "2.0.17", "@types/passport-openidconnect": "0.1.3", - "@typescript-eslint/eslint-plugin": "8.67.0", - "@typescript-eslint/parser": "8.67.0", "eslint": "9.35.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-import": "2.32.0", diff --git a/package.json b/package.json index 3361c354ba..a3f94b74b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "3.74.0", + "version": "3.75.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", @@ -196,8 +196,6 @@ "@types/papaparse": "5.5.2", "@types/passport-google-oauth20": "2.0.17", "@types/passport-openidconnect": "0.1.3", - "@typescript-eslint/eslint-plugin": "8.67.0", - "@typescript-eslint/parser": "8.67.0", "eslint": "9.35.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-import": "2.32.0",