diff --git a/CHANGELOG.md b/CHANGELOG.md index 851dcd1e6..1f5406964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Extended the support of the impersonation mode for local development + ### Fixed - Improved the holdings table by showing the cash position also when the filter contains the accounts, so that we can see the total allocation for that account diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index 3c4d3db5b..bf15d998f 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -87,10 +87,7 @@ export class AccountController { @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId ): Promise { const impersonationUserId = - await this.impersonationService.validateImpersonationId( - impersonationId, - this.request.user.id - ); + await this.impersonationService.validateImpersonationId(impersonationId); return this.portfolioService.getAccountsWithAggregations({ userId: impersonationUserId || this.request.user.id, @@ -106,10 +103,7 @@ export class AccountController { @Param('id') id: string ): Promise { const impersonationUserId = - await this.impersonationService.validateImpersonationId( - impersonationId, - this.request.user.id - ); + await this.impersonationService.validateImpersonationId(impersonationId); const accountsWithAggregations = await this.portfolioService.getAccountsWithAggregations({ diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index 0f1e382d6..c478860d2 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -96,10 +96,7 @@ export class OrderController { }); const impersonationUserId = - await this.impersonationService.validateImpersonationId( - impersonationId, - this.request.user.id - ); + await this.impersonationService.validateImpersonationId(impersonationId); const userCurrency = this.request.user.Settings.settings.baseCurrency; const activities = await this.orderService.getOrders({ diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 8e368d667..1b839977b 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1827,10 +1827,7 @@ export class PortfolioService { private async getUserId(aImpersonationId: string, aUserId: string) { const impersonationUserId = - await this.impersonationService.validateImpersonationId( - aImpersonationId, - aUserId - ); + await this.impersonationService.validateImpersonationId(aImpersonationId); return impersonationUserId || aUserId; } diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index aaf4d9bad..82889369b 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -1,4 +1,5 @@ import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; +import { environment } from '@ghostfolio/api/environments/environment'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; @@ -196,6 +197,10 @@ export class UserService { } } + if (!environment.production && role === 'ADMIN') { + currentPermissions.push(permissions.impersonateAllUsers); + } + user.Account = sortBy(user.Account, (account) => { return account.name; }); diff --git a/apps/api/src/services/impersonation/impersonation.service.ts b/apps/api/src/services/impersonation/impersonation.service.ts index fe17127c8..12e76aab8 100644 --- a/apps/api/src/services/impersonation/impersonation.service.ts +++ b/apps/api/src/services/impersonation/impersonation.service.ts @@ -1,15 +1,35 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; -import { Injectable } from '@nestjs/common'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { RequestWithUser } from '@ghostfolio/common/types'; +import { Inject, Injectable } from '@nestjs/common'; +import { REQUEST } from '@nestjs/core'; @Injectable() export class ImpersonationService { - public constructor(private readonly prismaService: PrismaService) {} + public constructor( + private readonly prismaService: PrismaService, + @Inject(REQUEST) private readonly request: RequestWithUser + ) {} - public async validateImpersonationId(aId = '', aUserId: string) { + public async validateImpersonationId(aId = '') { const accessObject = await this.prismaService.access.findFirst({ - where: { GranteeUser: { id: aUserId }, id: aId } + where: { + GranteeUser: { id: this.request.user.id }, + id: aId + } }); - return accessObject?.userId; + if (accessObject?.userId) { + return accessObject?.userId; + } else if ( + hasPermission( + this.request.user.permissions, + permissions.impersonateAllUsers + ) + ) { + return aId; + } + + return null; } } diff --git a/apps/client/src/app/components/admin-users/admin-users.component.ts b/apps/client/src/app/components/admin-users/admin-users.component.ts index bae725e53..3b5ae9758 100644 --- a/apps/client/src/app/components/admin-users/admin-users.component.ts +++ b/apps/client/src/app/components/admin-users/admin-users.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; 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 { getDateFormatString, getEmojiFlag } from '@ghostfolio/common/helper'; import { AdminData, InfoItem, User } from '@ghostfolio/common/interfaces'; @@ -21,6 +22,7 @@ export class AdminUsersComponent implements OnDestroy, OnInit { public defaultDateFormat: string; public getEmojiFlag = getEmojiFlag; public hasPermissionForSubscription: boolean; + public hasPermissionToImpersonateAllUsers: boolean; public info: InfoItem; public user: User; public users: AdminData['users']; @@ -30,6 +32,7 @@ export class AdminUsersComponent implements OnDestroy, OnInit { public constructor( private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, + private impersonationStorageService: ImpersonationStorageService, private userService: UserService ) { this.info = this.dataService.fetchInfo(); @@ -48,6 +51,11 @@ export class AdminUsersComponent implements OnDestroy, OnInit { this.defaultDateFormat = getDateFormatString( this.user.settings.locale ); + + this.hasPermissionToImpersonateAllUsers = hasPermission( + this.user.permissions, + permissions.impersonateAllUsers + ); } }); } @@ -88,6 +96,16 @@ export class AdminUsersComponent implements OnDestroy, OnInit { } } + public onImpersonateUser(aId: string) { + if (aId) { + this.impersonationStorageService.setId(aId); + } else { + this.impersonationStorageService.removeId(); + } + + window.location.reload(); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/components/admin-users/admin-users.html b/apps/client/src/app/components/admin-users/admin-users.html index 0f5944129..73e57f156 100644 --- a/apps/client/src/app/components/admin-users/admin-users.html +++ b/apps/client/src/app/components/admin-users/admin-users.html @@ -106,12 +106,20 @@ - + +