Browse Source

Merge remote-tracking branch 'origin/main' into feature/enable-strict-null-checks-in-ui

pull/6264/head
KenTandrian 3 weeks ago
parent
commit
694d8ae69f
  1. 1
      CHANGELOG.md
  2. 7
      apps/api/src/app/endpoints/assets/assets.controller.ts
  3. 6
      apps/api/src/services/i18n/i18n.service.ts
  4. 7
      apps/client/src/app/app.component.ts
  5. 6
      apps/client/src/app/components/admin-jobs/admin-jobs.component.ts
  6. 5
      apps/client/src/app/components/admin-users/admin-users.component.ts
  7. 27
      apps/client/src/app/components/home-holdings/home-holdings.component.ts
  8. 21
      apps/client/src/app/components/home-market/home-market.component.ts
  9. 23
      apps/client/src/app/components/home-overview/home-overview.component.ts
  10. 25
      apps/client/src/app/components/home-summary/home-summary.component.ts
  11. 5
      apps/client/src/app/components/user-account-access/user-account-access.component.ts
  12. 5
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  13. 2
      apps/client/src/app/core/auth.guard.ts
  14. 6
      apps/client/src/app/core/http-response.interceptor.ts
  15. 17
      apps/client/src/app/pages/about/about-page.component.ts
  16. 52
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  17. 37
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  18. 48
      apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
  19. 22
      apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts
  20. 6
      apps/client/src/app/pages/register/register-page.component.ts
  21. 26
      apps/client/src/app/services/token-storage.service.ts
  22. 36
      apps/client/src/app/services/user/user.service.ts
  23. 7
      libs/ui/src/lib/toggle/toggle.component.html
  24. 32
      libs/ui/src/lib/toggle/toggle.component.ts

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Consolidated the sign-out logic within the user service to unify cookie, state and token clearance
- Upgraded `svgmap` from version `2.14.0` to `2.19.2` - Upgraded `svgmap` from version `2.14.0` to `2.19.2`
## 2.249.0 - 2026-03-10 ## 2.249.0 - 2026-03-10

7
apps/api/src/app/endpoints/assets/assets.controller.ts

@ -4,6 +4,7 @@ import { interpolate } from '@ghostfolio/common/helper';
import { import {
Controller, Controller,
Get, Get,
OnModuleInit,
Param, Param,
Res, Res,
Version, Version,
@ -14,12 +15,14 @@ import { readFileSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
@Controller('assets') @Controller('assets')
export class AssetsController { export class AssetsController implements OnModuleInit {
private webManifest = ''; private webManifest = '';
public constructor( public constructor(
public readonly configurationService: ConfigurationService public readonly configurationService: ConfigurationService
) { ) {}
public onModuleInit() {
try { try {
this.webManifest = readFileSync( this.webManifest = readFileSync(
join(__dirname, 'assets', 'site.webmanifest'), join(__dirname, 'assets', 'site.webmanifest'),

6
apps/api/src/services/i18n/i18n.service.ts

@ -1,16 +1,16 @@
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import * as cheerio from 'cheerio'; import * as cheerio from 'cheerio';
import { readFileSync, readdirSync } from 'node:fs'; import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
@Injectable() @Injectable()
export class I18nService { export class I18nService implements OnModuleInit {
private localesPath = join(__dirname, 'assets', 'locales'); private localesPath = join(__dirname, 'assets', 'locales');
private translations: { [locale: string]: cheerio.CheerioAPI } = {}; private translations: { [locale: string]: cheerio.CheerioAPI } = {};
public constructor() { public onModuleInit() {
this.loadFiles(); this.loadFiles();
} }

7
apps/client/src/app/app.component.ts

@ -38,7 +38,6 @@ import { GfHeaderComponent } from './components/header/header.component';
import { GfHoldingDetailDialogComponent } from './components/holding-detail-dialog/holding-detail-dialog.component'; import { GfHoldingDetailDialogComponent } from './components/holding-detail-dialog/holding-detail-dialog.component';
import { HoldingDetailDialogParams } from './components/holding-detail-dialog/interfaces/interfaces'; import { HoldingDetailDialogParams } from './components/holding-detail-dialog/interfaces/interfaces';
import { ImpersonationStorageService } from './services/impersonation-storage.service'; import { ImpersonationStorageService } from './services/impersonation-storage.service';
import { TokenStorageService } from './services/token-storage.service';
import { UserService } from './services/user/user.service'; import { UserService } from './services/user/user.service';
@Component({ @Component({
@ -82,7 +81,6 @@ export class GfAppComponent implements OnDestroy, OnInit {
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private title: Title, private title: Title,
private tokenStorageService: TokenStorageService,
private userService: UserService private userService: UserService
) { ) {
this.initializeTheme(); this.initializeTheme();
@ -236,12 +234,11 @@ export class GfAppComponent implements OnDestroy, OnInit {
} }
public onCreateAccount() { public onCreateAccount() {
this.tokenStorageService.signOut(); this.userService.signOut();
} }
public onSignOut() { public onSignOut() {
this.tokenStorageService.signOut(); this.userService.signOut();
this.userService.remove();
document.location.href = `/${document.documentElement.lang}`; document.location.href = `/${document.documentElement.lang}`;
} }

6
apps/client/src/app/components/admin-jobs/admin-jobs.component.ts

@ -194,7 +194,11 @@ export class GfAdminJobsComponent implements OnInit {
public onOpenBullBoard() { public onOpenBullBoard() {
const token = this.tokenStorageService.getToken(); const token = this.tokenStorageService.getToken();
document.cookie = `${BULL_BOARD_COOKIE_NAME}=${token}; path=${BULL_BOARD_ROUTE}; SameSite=Strict`; document.cookie = [
`${BULL_BOARD_COOKIE_NAME}=${encodeURIComponent(token)}`,
'path=/',
'SameSite=Strict'
].join('; ');
window.open(BULL_BOARD_ROUTE, '_blank'); window.open(BULL_BOARD_ROUTE, '_blank');
} }

5
apps/client/src/app/components/admin-users/admin-users.component.ts

@ -1,7 +1,6 @@
import { UserDetailDialogParams } from '@ghostfolio/client/components/user-detail-dialog/interfaces/interfaces'; import { UserDetailDialogParams } from '@ghostfolio/client/components/user-detail-dialog/interfaces/interfaces';
import { GfUserDetailDialogComponent } from '@ghostfolio/client/components/user-detail-dialog/user-detail-dialog.component'; import { GfUserDetailDialogComponent } from '@ghostfolio/client/components/user-detail-dialog/user-detail-dialog.component';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
@ -106,7 +105,6 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
private notificationService: NotificationService, private notificationService: NotificationService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private tokenStorageService: TokenStorageService,
private userService: UserService private userService: UserService
) { ) {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;
@ -229,8 +227,7 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
this.notificationService.alert({ this.notificationService.alert({
discardFn: () => { discardFn: () => {
if (aUserId === this.user.id) { if (aUserId === this.user.id) {
this.tokenStorageService.signOut(); this.userService.signOut();
this.userService.remove();
document.location.href = `/${document.documentElement.lang}`; document.location.href = `/${document.documentElement.lang}`;
} }

27
apps/client/src/app/components/home-holdings/home-holdings.component.ts

@ -19,9 +19,10 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
OnDestroy, DestroyRef,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatButtonToggleModule } from '@angular/material/button-toggle';
@ -30,8 +31,6 @@ import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { gridOutline, reorderFourOutline } from 'ionicons/icons'; import { gridOutline, reorderFourOutline } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
imports: [ imports: [
@ -51,7 +50,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./home-holdings.scss'], styleUrls: ['./home-holdings.scss'],
templateUrl: './home-holdings.html' templateUrl: './home-holdings.html'
}) })
export class GfHomeHoldingsComponent implements OnDestroy, OnInit { export class GfHomeHoldingsComponent implements OnInit {
public static DEFAULT_HOLDINGS_VIEW_MODE: HoldingsViewMode = 'TABLE'; public static DEFAULT_HOLDINGS_VIEW_MODE: HoldingsViewMode = 'TABLE';
public deviceType: string; public deviceType: string;
@ -71,11 +70,10 @@ export class GfHomeHoldingsComponent implements OnDestroy, OnInit {
GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE
); );
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private router: Router, private router: Router,
@ -89,13 +87,13 @@ export class GfHomeHoldingsComponent implements OnDestroy, OnInit {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
}); });
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -117,15 +115,15 @@ export class GfHomeHoldingsComponent implements OnDestroy, OnInit {
}); });
this.viewModeFormControl.valueChanges this.viewModeFormControl.valueChanges
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((holdingsViewMode) => { .subscribe((holdingsViewMode) => {
this.dataService this.dataService
.putUserSetting({ holdingsViewMode }) .putUserSetting({ holdingsViewMode })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -149,11 +147,6 @@ export class GfHomeHoldingsComponent implements OnDestroy, OnInit {
} }
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private fetchHoldings() { private fetchHoldings() {
const filters = this.userService.getFilters(); const filters = this.userService.getFilters();
@ -193,7 +186,7 @@ export class GfHomeHoldingsComponent implements OnDestroy, OnInit {
this.holdings = undefined; this.holdings = undefined;
this.fetchHoldings() this.fetchHoldings()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ holdings }) => { .subscribe(({ holdings }) => {
this.holdings = holdings; this.holdings = holdings;

21
apps/client/src/app/components/home-market/home-market.component.ts

@ -17,12 +17,11 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
OnDestroy, DestroyRef,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
imports: [ imports: [
@ -35,7 +34,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./home-market.scss'], styleUrls: ['./home-market.scss'],
templateUrl: './home-market.html' templateUrl: './home-market.html'
}) })
export class GfHomeMarketComponent implements OnDestroy, OnInit { export class GfHomeMarketComponent implements OnInit {
public benchmarks: Benchmark[]; public benchmarks: Benchmark[];
public deviceType: string; public deviceType: string;
public fearAndGreedIndex: number; public fearAndGreedIndex: number;
@ -47,11 +46,10 @@ export class GfHomeMarketComponent implements OnDestroy, OnInit {
public readonly numberOfDays = 365; public readonly numberOfDays = 365;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private userService: UserService private userService: UserService
) { ) {
@ -59,7 +57,7 @@ export class GfHomeMarketComponent implements OnDestroy, OnInit {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.fetchInfo();
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -82,7 +80,7 @@ export class GfHomeMarketComponent implements OnDestroy, OnInit {
includeHistoricalData: this.numberOfDays, includeHistoricalData: this.numberOfDays,
symbol: ghostfolioFearAndGreedIndexSymbol symbol: ghostfolioFearAndGreedIndexSymbol
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ historicalData, marketPrice }) => { .subscribe(({ historicalData, marketPrice }) => {
this.fearAndGreedIndex = marketPrice; this.fearAndGreedIndex = marketPrice;
this.historicalDataItems = [ this.historicalDataItems = [
@ -99,16 +97,11 @@ export class GfHomeMarketComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.fetchBenchmarks() .fetchBenchmarks()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ benchmarks }) => { .subscribe(({ benchmarks }) => {
this.benchmarks = benchmarks; this.benchmarks = benchmarks;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

23
apps/client/src/app/components/home-overview/home-overview.component.ts

@ -19,14 +19,13 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
OnDestroy, DestroyRef,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
imports: [ imports: [
@ -41,7 +40,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./home-overview.scss'], styleUrls: ['./home-overview.scss'],
templateUrl: './home-overview.html' templateUrl: './home-overview.html'
}) })
export class GfHomeOverviewComponent implements OnDestroy, OnInit { export class GfHomeOverviewComponent implements OnInit {
public deviceType: string; public deviceType: string;
public errors: AssetProfileIdentifier[]; public errors: AssetProfileIdentifier[];
public hasError: boolean; public hasError: boolean;
@ -62,18 +61,17 @@ export class GfHomeOverviewComponent implements OnDestroy, OnInit {
public unit: string; public unit: string;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private layoutService: LayoutService, private layoutService: LayoutService,
private userService: UserService private userService: UserService
) { ) {
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -99,7 +97,7 @@ export class GfHomeOverviewComponent implements OnDestroy, OnInit {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
@ -107,17 +105,12 @@ export class GfHomeOverviewComponent implements OnDestroy, OnInit {
}); });
this.layoutService.shouldReloadContent$ this.layoutService.shouldReloadContent$
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.update(); this.update();
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private update() { private update() {
this.historicalDataItems = null; this.historicalDataItems = null;
this.isLoadingPerformance = true; this.isLoadingPerformance = true;
@ -126,7 +119,7 @@ export class GfHomeOverviewComponent implements OnDestroy, OnInit {
.fetchPortfolioPerformance({ .fetchPortfolioPerformance({
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ chart, errors, performance }) => { .subscribe(({ chart, errors, performance }) => {
this.errors = errors; this.errors = errors;
this.performance = performance; this.performance = performance;

25
apps/client/src/app/components/home-summary/home-summary.component.ts

@ -13,14 +13,13 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
OnDestroy, DestroyRef,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatSnackBarRef, TextOnlySnackBar } from '@angular/material/snack-bar'; import { MatSnackBarRef, TextOnlySnackBar } from '@angular/material/snack-bar';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
imports: [GfPortfolioSummaryComponent, MatCardModule], imports: [GfPortfolioSummaryComponent, MatCardModule],
@ -29,7 +28,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./home-summary.scss'], styleUrls: ['./home-summary.scss'],
templateUrl: './home-summary.html' templateUrl: './home-summary.html'
}) })
export class GfHomeSummaryComponent implements OnDestroy, OnInit { export class GfHomeSummaryComponent implements OnInit {
public deviceType: string; public deviceType: string;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
@ -40,11 +39,10 @@ export class GfHomeSummaryComponent implements OnDestroy, OnInit {
public summary: PortfolioSummary; public summary: PortfolioSummary;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private userService: UserService private userService: UserService
@ -57,7 +55,7 @@ export class GfHomeSummaryComponent implements OnDestroy, OnInit {
); );
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -77,7 +75,7 @@ export class GfHomeSummaryComponent implements OnDestroy, OnInit {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
}); });
@ -86,11 +84,11 @@ export class GfHomeSummaryComponent implements OnDestroy, OnInit {
public onChangeEmergencyFund(emergencyFund: number) { public onChangeEmergencyFund(emergencyFund: number) {
this.dataService this.dataService
.putUserSetting({ emergencyFund }) .putUserSetting({ emergencyFund })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -99,17 +97,12 @@ export class GfHomeSummaryComponent implements OnDestroy, OnInit {
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private update() { private update() {
this.isLoading = true; this.isLoading = true;
this.dataService this.dataService
.fetchPortfolioDetails() .fetchPortfolioDetails()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ summary }) => { .subscribe(({ summary }) => {
this.summary = summary; this.summary = summary;
this.isLoading = false; this.isLoading = false;

5
apps/client/src/app/components/user-account-access/user-account-access.component.ts

@ -1,5 +1,4 @@
import { GfAccessTableComponent } from '@ghostfolio/client/components/access-table/access-table.component'; import { GfAccessTableComponent } from '@ghostfolio/client/components/access-table/access-table.component';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { CreateAccessDto } from '@ghostfolio/common/dtos'; import { CreateAccessDto } from '@ghostfolio/common/dtos';
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
@ -76,7 +75,6 @@ export class GfUserAccountAccessComponent implements OnDestroy, OnInit {
private notificationService: NotificationService, private notificationService: NotificationService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private tokenStorageService: TokenStorageService,
private userService: UserService private userService: UserService
) { ) {
const { globalPermissions } = this.dataService.fetchInfo(); const { globalPermissions } = this.dataService.fetchInfo();
@ -161,8 +159,7 @@ export class GfUserAccountAccessComponent implements OnDestroy, OnInit {
.subscribe(({ accessToken }) => { .subscribe(({ accessToken }) => {
this.notificationService.alert({ this.notificationService.alert({
discardFn: () => { discardFn: () => {
this.tokenStorageService.signOut(); this.userService.signOut();
this.userService.remove();
document.location.href = `/${document.documentElement.lang}`; document.location.href = `/${document.documentElement.lang}`;
}, },

5
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts

@ -3,7 +3,6 @@ import {
KEY_TOKEN, KEY_TOKEN,
SettingsStorageService SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service'; } from '@ghostfolio/client/services/settings-storage.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
@ -108,7 +107,6 @@ export class GfUserAccountSettingsComponent implements OnDestroy, OnInit {
private notificationService: NotificationService, private notificationService: NotificationService,
private settingsStorageService: SettingsStorageService, private settingsStorageService: SettingsStorageService,
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
private tokenStorageService: TokenStorageService,
private userService: UserService, private userService: UserService,
public webAuthnService: WebAuthnService public webAuthnService: WebAuthnService
) { ) {
@ -198,8 +196,7 @@ export class GfUserAccountSettingsComponent implements OnDestroy, OnInit {
takeUntil(this.unsubscribeSubject) takeUntil(this.unsubscribeSubject)
) )
.subscribe(() => { .subscribe(() => {
this.tokenStorageService.signOut(); this.userService.signOut();
this.userService.remove();
document.location.href = `/${document.documentElement.lang}`; document.location.href = `/${document.documentElement.lang}`;
}); });

2
apps/client/src/app/core/auth.guard.ts

@ -68,7 +68,7 @@ export class AuthGuard {
this.dataService this.dataService
.putUserSetting({ language: document.documentElement.lang }) .putUserSetting({ language: document.documentElement.lang })
.subscribe(() => { .subscribe(() => {
this.userService.remove(); this.userService.reset();
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();

6
apps/client/src/app/core/http-response.interceptor.ts

@ -1,4 +1,4 @@
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { InfoItem } from '@ghostfolio/common/interfaces'; import { InfoItem } from '@ghostfolio/common/interfaces';
import { internalRoutes, publicRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes, publicRoutes } from '@ghostfolio/common/routes/routes';
@ -32,8 +32,8 @@ export class HttpResponseInterceptor implements HttpInterceptor {
public constructor( public constructor(
private dataService: DataService, private dataService: DataService,
private router: Router, private router: Router,
private tokenStorageService: TokenStorageService,
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
private userService: UserService,
private webAuthnService: WebAuthnService private webAuthnService: WebAuthnService
) { ) {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.fetchInfo();
@ -115,7 +115,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
if (this.webAuthnService.isEnabled()) { if (this.webAuthnService.isEnabled()) {
this.router.navigate(internalRoutes.webauthn.routerLink); this.router.navigate(internalRoutes.webauthn.routerLink);
} else { } else {
this.tokenStorageService.signOut(); this.userService.signOut();
} }
} }
} }

17
apps/client/src/app/pages/about/about-page.component.ts

@ -8,9 +8,10 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
OnDestroy, DestroyRef,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
@ -24,8 +25,6 @@ import {
sparklesOutline sparklesOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
host: { class: 'page has-tabs' }, host: { class: 'page has-tabs' },
@ -35,17 +34,16 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./about-page.scss'], styleUrls: ['./about-page.scss'],
templateUrl: './about-page.html' templateUrl: './about-page.html'
}) })
export class AboutPageComponent implements OnDestroy, OnInit { export class AboutPageComponent implements OnInit {
public deviceType: string; public deviceType: string;
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
public tabs: TabConfiguration[] = []; public tabs: TabConfiguration[] = [];
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private userService: UserService private userService: UserService
) { ) {
@ -57,7 +55,7 @@ export class AboutPageComponent implements OnDestroy, OnInit {
); );
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
this.tabs = [ this.tabs = [
{ {
@ -118,9 +116,4 @@ export class AboutPageComponent implements OnDestroy, OnInit {
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

52
apps/client/src/app/pages/accounts/accounts-page.component.ts

@ -13,7 +13,13 @@ import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table';
import { NotificationService } from '@ghostfolio/ui/notifications'; import { NotificationService } from '@ghostfolio/ui/notifications';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectorRef,
Component,
DestroyRef,
OnInit
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
@ -21,8 +27,8 @@ import { Account as AccountModel } from '@prisma/client';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { addOutline } from 'ionicons/icons'; import { addOutline } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { EMPTY, Subject, Subscription } from 'rxjs'; import { EMPTY, Subscription } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
import { GfCreateOrUpdateAccountDialogComponent } from './create-or-update-account-dialog/create-or-update-account-dialog.component'; import { GfCreateOrUpdateAccountDialogComponent } from './create-or-update-account-dialog/create-or-update-account-dialog.component';
import { CreateOrUpdateAccountDialogParams } from './create-or-update-account-dialog/interfaces/interfaces'; import { CreateOrUpdateAccountDialogParams } from './create-or-update-account-dialog/interfaces/interfaces';
@ -36,7 +42,7 @@ import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-ba
styleUrls: ['./accounts-page.scss'], styleUrls: ['./accounts-page.scss'],
templateUrl: './accounts-page.html' templateUrl: './accounts-page.html'
}) })
export class GfAccountsPageComponent implements OnDestroy, OnInit { export class GfAccountsPageComponent implements OnInit {
public accounts: AccountModel[]; public accounts: AccountModel[];
public activitiesCount = 0; public activitiesCount = 0;
public deviceType: string; public deviceType: string;
@ -48,11 +54,10 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
public totalValueInBaseCurrency = 0; public totalValueInBaseCurrency = 0;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
@ -62,7 +67,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
private userService: UserService private userService: UserService
) { ) {
this.route.queryParams this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((params) => { .subscribe((params) => {
if (params['accountId'] && params['accountDetailDialog']) { if (params['accountId'] && params['accountDetailDialog']) {
this.openAccountDetailDialog(params['accountId']); this.openAccountDetailDialog(params['accountId']);
@ -94,13 +99,13 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
}); });
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -124,7 +129,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
public fetchAccounts() { public fetchAccounts() {
this.dataService this.dataService
.fetchAccounts() .fetchAccounts()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe( .subscribe(
({ ({
accounts, accounts,
@ -151,11 +156,11 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.deleteAccount(aId) .deleteAccount(aId)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
this.fetchAccounts(); this.fetchAccounts();
@ -204,18 +209,18 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((account: UpdateAccountDto | null) => { .subscribe((account: UpdateAccountDto | null) => {
if (account) { if (account) {
this.reset(); this.reset();
this.dataService this.dataService
.putAccount(account) .putAccount(account)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
this.fetchAccounts(); this.fetchAccounts();
@ -228,11 +233,6 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private openAccountDetailDialog(aAccountId: string) { private openAccountDetailDialog(aAccountId: string) {
const dialogRef = this.dialog.open< const dialogRef = this.dialog.open<
GfAccountDetailDialogComponent, GfAccountDetailDialogComponent,
@ -254,7 +254,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.fetchAccounts(); this.fetchAccounts();
@ -284,18 +284,18 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((account: CreateAccountDto | null) => { .subscribe((account: CreateAccountDto | null) => {
if (account) { if (account) {
this.reset(); this.reset();
this.dataService this.dataService
.postAccount(account) .postAccount(account)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
this.fetchAccounts(); this.fetchAccounts();
@ -321,7 +321,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((data: any) => { .subscribe((data: any) => {
if (data) { if (data) {
this.reset(); this.reset();
@ -343,7 +343,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
return EMPTY; return EMPTY;
}), }),
takeUntil(this.unsubscribeSubject) takeUntilDestroyed(this.destroyRef)
) )
.subscribe(() => { .subscribe(() => {
this.fetchAccounts(); this.fetchAccounts();

37
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts

@ -24,10 +24,11 @@ import { Clipboard } from '@angular/cdk/clipboard';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
OnDestroy, DestroyRef,
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu'; import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
@ -42,8 +43,6 @@ import { isNumber, sortBy } from 'lodash';
import ms from 'ms'; import ms from 'ms';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
imports: [ imports: [
@ -64,7 +63,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./analysis-page.scss'], styleUrls: ['./analysis-page.scss'],
templateUrl: './analysis-page.html' templateUrl: './analysis-page.html'
}) })
export class GfAnalysisPageComponent implements OnDestroy, OnInit { export class GfAnalysisPageComponent implements OnInit {
@ViewChild(MatMenuTrigger) actionsMenuButton!: MatMenuTrigger; @ViewChild(MatMenuTrigger) actionsMenuButton!: MatMenuTrigger;
public benchmark: Partial<SymbolProfile>; public benchmark: Partial<SymbolProfile>;
@ -102,12 +101,11 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
public unitLongestStreak: string; public unitLongestStreak: string;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private clipboard: Clipboard, private clipboard: Clipboard,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
@ -135,13 +133,13 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
}); });
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -163,11 +161,11 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
public onChangeBenchmark(symbolProfileId: string) { public onChangeBenchmark(symbolProfileId: string) {
this.dataService this.dataService
.putUserSetting({ benchmark: symbolProfileId }) .putUserSetting({ benchmark: symbolProfileId })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -193,7 +191,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
mode, mode,
filters: this.userService.getFilters() filters: this.userService.getFilters()
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ prompt }) => { .subscribe(({ prompt }) => {
this.clipboard.copy(prompt); this.clipboard.copy(prompt);
@ -207,7 +205,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
snackBarRef snackBarRef
.onAction() .onAction()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
window.open('https://duck.ai', '_blank'); window.open('https://duck.ai', '_blank');
}); });
@ -222,11 +220,6 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private fetchDividendsAndInvestments() { private fetchDividendsAndInvestments() {
this.isLoadingDividendTimelineChart = true; this.isLoadingDividendTimelineChart = true;
this.isLoadingInvestmentTimelineChart = true; this.isLoadingInvestmentTimelineChart = true;
@ -237,7 +230,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
groupBy: this.mode, groupBy: this.mode,
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ dividends }) => { .subscribe(({ dividends }) => {
this.dividendsByGroup = dividends; this.dividendsByGroup = dividends;
@ -252,7 +245,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
groupBy: this.mode, groupBy: this.mode,
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ investments, streaks }) => { .subscribe(({ investments, streaks }) => {
this.investmentsByGroup = investments; this.investmentsByGroup = investments;
this.streaks = streaks; this.streaks = streaks;
@ -287,7 +280,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
filters: this.userService.getFilters(), filters: this.userService.getFilters(),
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ chart, firstOrderDate, performance }) => { .subscribe(({ chart, firstOrderDate, performance }) => {
this.firstOrderDate = firstOrderDate ?? new Date(); this.firstOrderDate = firstOrderDate ?? new Date();
@ -346,7 +339,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
filters: this.userService.getFilters(), filters: this.userService.getFilters(),
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ holdings }) => { .subscribe(({ holdings }) => {
const holdingsSorted = sortBy( const holdingsSorted = sortBy(
holdings.filter(({ netPerformancePercentWithCurrencyEffect }) => { holdings.filter(({ netPerformancePercentWithCurrencyEffect }) => {
@ -397,7 +390,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
range: this.user?.settings?.dateRange, range: this.user?.settings?.dateRange,
startDate: this.firstOrderDate startDate: this.firstOrderDate
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ marketData }) => { .subscribe(({ marketData }) => {
this.benchmarkDataItems = marketData.map(({ date, value }) => { this.benchmarkDataItems = marketData.map(({ date, value }) => {
return { return {

48
apps/client/src/app/pages/portfolio/fire/fire-page.component.ts

@ -12,14 +12,18 @@ import { DataService } from '@ghostfolio/ui/services';
import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule, NgStyle } from '@angular/common'; import { CommonModule, NgStyle } from '@angular/common';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectorRef,
Component,
DestroyRef,
OnInit
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
imports: [ imports: [
@ -36,7 +40,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./fire-page.scss'], styleUrls: ['./fire-page.scss'],
templateUrl: './fire-page.html' templateUrl: './fire-page.html'
}) })
export class GfFirePageComponent implements OnDestroy, OnInit { export class GfFirePageComponent implements OnInit {
public deviceType: string; public deviceType: string;
public fireWealth: FireWealth; public fireWealth: FireWealth;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
@ -52,11 +56,10 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
public withdrawalRatePerYear: Big; public withdrawalRatePerYear: Big;
public withdrawalRatePerYearProjected: Big; public withdrawalRatePerYearProjected: Big;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private userService: UserService private userService: UserService
@ -68,7 +71,7 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.fetchPortfolioDetails() .fetchPortfolioDetails()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ summary }) => { .subscribe(({ summary }) => {
this.fireWealth = { this.fireWealth = {
today: { today: {
@ -92,19 +95,19 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
}); });
this.safeWithdrawalRateControl.valueChanges this.safeWithdrawalRateControl.valueChanges
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((value) => { .subscribe((value) => {
this.onSafeWithdrawalRateChange(Number(value)); this.onSafeWithdrawalRateChange(Number(value));
}); });
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -132,11 +135,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
public onAnnualInterestRateChange(annualInterestRate: number) { public onAnnualInterestRateChange(annualInterestRate: number) {
this.dataService this.dataService
.putUserSetting({ annualInterestRate }) .putUserSetting({ annualInterestRate })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -163,11 +166,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
retirementDate: retirementDate.toISOString(), retirementDate: retirementDate.toISOString(),
projectedTotalAmount: null projectedTotalAmount: null
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -179,11 +182,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
public onSafeWithdrawalRateChange(safeWithdrawalRate: number) { public onSafeWithdrawalRateChange(safeWithdrawalRate: number) {
this.dataService this.dataService
.putUserSetting({ safeWithdrawalRate }) .putUserSetting({ safeWithdrawalRate })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -198,11 +201,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
public onSavingsRateChange(savingsRate: number) { public onSavingsRateChange(savingsRate: number) {
this.dataService this.dataService
.putUserSetting({ savingsRate }) .putUserSetting({ savingsRate })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -217,11 +220,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
projectedTotalAmount, projectedTotalAmount,
retirementDate: null retirementDate: null
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -230,11 +233,6 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private calculateWithdrawalRates() { private calculateWithdrawalRates() {
if (this.fireWealth && this.user?.settings?.safeWithdrawalRate) { if (this.fireWealth && this.user?.settings?.safeWithdrawalRate) {
this.withdrawalRatePerYear = new Big( this.withdrawalRatePerYear = new Big(

22
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts

@ -12,7 +12,8 @@ import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
import { NgClass } from '@angular/common'; import { NgClass } from '@angular/common';
import { ChangeDetectorRef, Component } from '@angular/core'; import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { import {
@ -21,7 +22,6 @@ import {
warningOutline warningOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, takeUntil } from 'rxjs';
@Component({ @Component({
imports: [ imports: [
@ -48,11 +48,10 @@ export class GfXRayPageComponent {
public statistics: PortfolioReportResponse['xRay']['statistics']; public statistics: PortfolioReportResponse['xRay']['statistics'];
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private userService: UserService private userService: UserService
) { ) {
@ -62,13 +61,13 @@ export class GfXRayPageComponent {
public ngOnInit() { public ngOnInit() {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
}); });
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -91,28 +90,23 @@ export class GfXRayPageComponent {
public onRulesUpdated(event: UpdateUserSettingDto) { public onRulesUpdated(event: UpdateUserSettingDto) {
this.dataService this.dataService
.putUserSetting(event) .putUserSetting(event)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
this.initializePortfolioReport(); this.initializePortfolioReport();
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private initializePortfolioReport() { private initializePortfolioReport() {
this.isLoading = true; this.isLoading = true;
this.dataService this.dataService
.fetchPortfolioReport() .fetchPortfolioReport()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ xRay: { categories, statistics } }) => { .subscribe(({ xRay: { categories, statistics } }) => {
this.categories = categories; this.categories = categories;
this.inactiveRules = this.mergeInactiveRules(categories); this.inactiveRules = this.mergeInactiveRules(categories);

6
apps/client/src/app/pages/register/register-page.component.ts

@ -1,4 +1,5 @@
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { InfoItem, LineChartItem } from '@ghostfolio/common/interfaces'; import { InfoItem, LineChartItem } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfLogoComponent } from '@ghostfolio/ui/logo'; import { GfLogoComponent } from '@ghostfolio/ui/logo';
@ -42,11 +43,12 @@ export class GfRegisterPageComponent implements OnInit {
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private router: Router, private router: Router,
private tokenStorageService: TokenStorageService private tokenStorageService: TokenStorageService,
private userService: UserService
) { ) {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.fetchInfo();
this.tokenStorageService.signOut(); this.userService.signOut();
} }
public ngOnInit() { public ngOnInit() {

26
apps/client/src/app/services/token-storage.service.ts

@ -1,19 +1,11 @@
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { KEY_TOKEN } from './settings-storage.service'; import { KEY_TOKEN } from './settings-storage.service';
import { UserService } from './user/user.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class TokenStorageService { export class TokenStorageService {
public constructor(
private userService: UserService,
private webAuthnService: WebAuthnService
) {}
public getToken(): string { public getToken(): string {
return ( return (
window.sessionStorage.getItem(KEY_TOKEN) || window.sessionStorage.getItem(KEY_TOKEN) ||
@ -25,23 +17,7 @@ export class TokenStorageService {
if (staySignedIn) { if (staySignedIn) {
window.localStorage.setItem(KEY_TOKEN, token); window.localStorage.setItem(KEY_TOKEN, token);
} }
window.sessionStorage.setItem(KEY_TOKEN, token);
}
public signOut() {
const utmSource = window.localStorage.getItem('utm_source');
if (this.webAuthnService.isEnabled()) { window.sessionStorage.setItem(KEY_TOKEN, token);
this.webAuthnService.deregister().subscribe();
}
window.localStorage.clear();
window.sessionStorage.clear();
this.userService.remove();
if (utmSource) {
window.localStorage.setItem('utm_source', utmSource);
}
} }
} }

36
apps/client/src/app/services/user/user.service.ts

@ -1,3 +1,4 @@
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { Filter, User } from '@ghostfolio/common/interfaces'; import { Filter, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -26,7 +27,8 @@ export class UserService extends ObservableStore<UserStoreState> {
public constructor( public constructor(
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private http: HttpClient private http: HttpClient,
private webAuthnService: WebAuthnService
) { ) {
super({ trackStateHistory: true }); super({ trackStateHistory: true });
@ -93,10 +95,40 @@ export class UserService extends ObservableStore<UserStoreState> {
return this.getFilters().length > 0; return this.getFilters().length > 0;
} }
public remove() { public reset() {
this.setState({ user: null }, UserStoreActions.RemoveUser); this.setState({ user: null }, UserStoreActions.RemoveUser);
} }
public signOut() {
const utmSource = window.localStorage.getItem('utm_source');
if (this.webAuthnService.isEnabled()) {
this.webAuthnService.deregister().subscribe();
}
window.localStorage.clear();
window.sessionStorage.clear();
void this.clearAllCookies();
this.reset();
if (utmSource) {
window.localStorage.setItem('utm_source', utmSource);
}
}
private async clearAllCookies() {
if (!('cookieStore' in window)) {
console.warn('Cookie Store API not available in this browser');
return;
}
const cookies = await cookieStore.getAll();
await Promise.all(cookies.map(({ name }) => cookieStore.delete(name)));
}
private fetchUser(): Observable<User> { private fetchUser(): Observable<User> {
return this.http.get<any>('/api/v1/user').pipe( return this.http.get<any>('/api/v1/user').pipe(
map((user) => { map((user) => {

7
libs/ui/src/lib/toggle/toggle.component.html

@ -3,13 +3,14 @@
[formControl]="optionFormControl" [formControl]="optionFormControl"
(change)="onValueChange()" (change)="onValueChange()"
> >
@for (option of options; track option) { @for (option of options(); track option) {
<mat-radio-button <mat-radio-button
class="d-inline-flex" class="d-inline-flex"
[disabled]="isLoading" [disabled]="isLoading()"
[ngClass]="{ [ngClass]="{
'cursor-default': option.value === optionFormControl.value, 'cursor-default': option.value === optionFormControl.value,
'cursor-pointer': !isLoading && option.value !== optionFormControl.value 'cursor-pointer':
!isLoading() && option.value !== optionFormControl.value
}" }"
[value]="option.value" [value]="option.value"
>{{ option.label }}</mat-radio-button >{{ option.label }}</mat-radio-button

32
libs/ui/src/lib/toggle/toggle.component.ts

@ -4,10 +4,9 @@ import { CommonModule } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
EventEmitter, effect,
Input, input,
OnChanges, output
Output
} from '@angular/core'; } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatRadioModule } from '@angular/material/radio'; import { MatRadioModule } from '@angular/material/radio';
@ -19,22 +18,25 @@ import { MatRadioModule } from '@angular/material/radio';
styleUrls: ['./toggle.component.scss'], styleUrls: ['./toggle.component.scss'],
templateUrl: './toggle.component.html' templateUrl: './toggle.component.html'
}) })
export class GfToggleComponent implements OnChanges { export class GfToggleComponent {
@Input() defaultValue: string; public readonly defaultValue = input.required<string>();
@Input() isLoading: boolean; public readonly isLoading = input<boolean>(false);
@Input() options: ToggleOption[] = []; public readonly options = input<ToggleOption[]>([]);
@Output() valueChange = new EventEmitter<Pick<ToggleOption, 'value'>>(); protected readonly optionFormControl = new FormControl<string | null>(null);
protected readonly valueChange = output<Pick<ToggleOption, 'value'>>();
public optionFormControl = new FormControl<string | null>(null); public constructor() {
effect(() => {
public ngOnChanges() { this.optionFormControl.setValue(this.defaultValue());
this.optionFormControl.setValue(this.defaultValue); });
} }
public onValueChange() { public onValueChange() {
if (this.optionFormControl.value !== null) { const value = this.optionFormControl.value;
this.valueChange.emit({ value: this.optionFormControl.value });
if (value !== null) {
this.valueChange.emit({ value });
} }
} }
} }

Loading…
Cancel
Save