Feature/extend support for impersonation mode (#1898)
* Support impersonation of all users for local development * Update changelog
This commit is contained in:
parent
672d8dfab2
commit
f4c748f67a
@ -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
|
||||
|
@ -87,10 +87,7 @@ export class AccountController {
|
||||
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId
|
||||
): Promise<Accounts> {
|
||||
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<AccountWithValue> {
|
||||
const impersonationUserId =
|
||||
await this.impersonationService.validateImpersonationId(
|
||||
impersonationId,
|
||||
this.request.user.id
|
||||
);
|
||||
await this.impersonationService.validateImpersonationId(impersonationId);
|
||||
|
||||
const accountsWithAggregations =
|
||||
await this.portfolioService.getAccountsWithAggregations({
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -106,12 +106,20 @@
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="accountMenu"
|
||||
[matMenuTriggerFor]="userMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||
<mat-menu #userMenu="matMenu" xPosition="before">
|
||||
<button
|
||||
*ngIf="hasPermissionToImpersonateAllUsers"
|
||||
mat-menu-item
|
||||
(click)="onImpersonateUser(userItem.id)"
|
||||
>
|
||||
<ion-icon class="mr-2" name="contract-outline"></ion-icon>
|
||||
<span i18n>Impersonate</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="userItem.id === user?.id"
|
||||
|
@ -20,6 +20,7 @@ export const permissions = {
|
||||
enableSubscription: 'enableSubscription',
|
||||
enableSubscriptionInterstitial: 'enableSubscriptionInterstitial',
|
||||
enableSystemMessage: 'enableSystemMessage',
|
||||
impersonateAllUsers: 'impersonateAllUsers',
|
||||
reportDataGlitch: 'reportDataGlitch',
|
||||
toggleReadOnlyMode: 'toggleReadOnlyMode',
|
||||
updateAccount: 'updateAccount',
|
||||
|
Loading…
x
Reference in New Issue
Block a user