Date: Fri, 2 May 2025 16:46:43 +0200
Subject: [PATCH 4/9] Feature/improve watchlist for impersonation mode (#4632)
* Improve watchlist for impersonation mode
* Update changelog
---
CHANGELOG.md | 1 +
.../watchlist/watchlist.controller.ts | 13 ++++++--
.../endpoints/watchlist/watchlist.module.ts | 2 ++
.../home-watchlist.component.ts | 30 ++++++++++++++-----
.../home-watchlist/home-watchlist.html | 2 +-
5 files changed, 37 insertions(+), 11 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 85df415b..7bbdaf43 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Extended the watchlist by the date of the last all time high, the current change to the all time high and the current market condition (experimental)
+- Added support for the impersonation mode in the watchlist (experimental)
### Changed
diff --git a/apps/api/src/app/endpoints/watchlist/watchlist.controller.ts b/apps/api/src/app/endpoints/watchlist/watchlist.controller.ts
index c9e41d5d..8d9d322a 100644
--- a/apps/api/src/app/endpoints/watchlist/watchlist.controller.ts
+++ b/apps/api/src/app/endpoints/watchlist/watchlist.controller.ts
@@ -2,6 +2,8 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
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 { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
+import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { WatchlistResponse } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { RequestWithUser } from '@ghostfolio/common/types';
@@ -11,6 +13,7 @@ import {
Controller,
Delete,
Get,
+ Headers,
HttpException,
Inject,
Param,
@@ -29,6 +32,7 @@ import { WatchlistService } from './watchlist.service';
@Controller('watchlist')
export class WatchlistController {
public constructor(
+ private readonly impersonationService: ImpersonationService,
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly watchlistService: WatchlistService
) {}
@@ -79,9 +83,14 @@ export class WatchlistController {
@HasPermission(permissions.readWatchlist)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
- public async getWatchlistItems(): Promise {
+ public async getWatchlistItems(
+ @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string
+ ): Promise {
+ const impersonationUserId =
+ await this.impersonationService.validateImpersonationId(impersonationId);
+
const watchlist = await this.watchlistService.getWatchlistItems(
- this.request.user.id
+ impersonationUserId || this.request.user.id
);
return {
diff --git a/apps/api/src/app/endpoints/watchlist/watchlist.module.ts b/apps/api/src/app/endpoints/watchlist/watchlist.module.ts
index a2d32d1d..ce9ae12b 100644
--- a/apps/api/src/app/endpoints/watchlist/watchlist.module.ts
+++ b/apps/api/src/app/endpoints/watchlist/watchlist.module.ts
@@ -2,6 +2,7 @@ import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
+import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { DataGatheringModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
@@ -18,6 +19,7 @@ import { WatchlistService } from './watchlist.service';
BenchmarkModule,
DataGatheringModule,
DataProviderModule,
+ ImpersonationModule,
MarketDataModule,
PrismaModule,
SymbolProfileModule,
diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
index efad2fef..5c0b3fa5 100644
--- a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
+++ b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
@@ -1,4 +1,5 @@
import { DataService } from '@ghostfolio/client/services/data.service';
+import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import {
AssetProfileIdentifier,
@@ -45,6 +46,7 @@ import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/
})
export class HomeWatchlistComponent implements OnDestroy, OnInit {
public deviceType: string;
+ public hasImpersonationId: boolean;
public hasPermissionToCreateWatchlistItem: boolean;
public hasPermissionToDeleteWatchlistItem: boolean;
public user: User;
@@ -57,12 +59,20 @@ export class HomeWatchlistComponent implements OnDestroy, OnInit {
private dataService: DataService,
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
+ private impersonationStorageService: ImpersonationStorageService,
private route: ActivatedRoute,
private router: Router,
private userService: UserService
) {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
+ this.impersonationStorageService
+ .onChangeHasImpersonation()
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe((impersonationId) => {
+ this.hasImpersonationId = !!impersonationId;
+ });
+
this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
@@ -77,14 +87,18 @@ export class HomeWatchlistComponent implements OnDestroy, OnInit {
if (state?.user) {
this.user = state.user;
- this.hasPermissionToCreateWatchlistItem = hasPermission(
- this.user.permissions,
- permissions.createWatchlistItem
- );
- this.hasPermissionToDeleteWatchlistItem = hasPermission(
- this.user.permissions,
- permissions.deleteWatchlistItem
- );
+ this.hasPermissionToCreateWatchlistItem =
+ !this.hasImpersonationId &&
+ hasPermission(
+ this.user.permissions,
+ permissions.createWatchlistItem
+ );
+ this.hasPermissionToDeleteWatchlistItem =
+ !this.hasImpersonationId &&
+ hasPermission(
+ this.user.permissions,
+ permissions.deleteWatchlistItem
+ );
this.changeDetectorRef.markForCheck();
}
diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.html b/apps/client/src/app/components/home-watchlist/home-watchlist.html
index d290a4a2..9149eab9 100644
--- a/apps/client/src/app/components/home-watchlist/home-watchlist.html
+++ b/apps/client/src/app/components/home-watchlist/home-watchlist.html
@@ -20,7 +20,7 @@