diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index d137ae97c..b9932fb4f 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -74,6 +74,11 @@ export class PortfolioController { ): Promise { let hasDetails = true; let hasError = false; + const hasReadRestrictedAccessPermission = + this.userService.hasReadRestrictedAccessPermission({ + impersonationId, + user: this.request.user + }); if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { hasDetails = this.request.user.subscription.type === 'Premium'; @@ -108,7 +113,7 @@ export class PortfolioController { let portfolioSummary = summary; if ( - impersonationId || + hasReadRestrictedAccessPermission || this.userService.isRestrictedView(this.request.user) ) { const totalInvestment = Object.values(holdings) @@ -148,7 +153,7 @@ export class PortfolioController { if ( hasDetails === false || - impersonationId || + hasReadRestrictedAccessPermission || this.userService.isRestrictedView(this.request.user) ) { portfolioSummary = nullifyValuesInObject(summary, [ @@ -164,6 +169,7 @@ export class PortfolioController { 'excludedAccountsAndActivities', 'fees', 'fireWealth', + 'interest', 'items', 'liabilities', 'netWorth', @@ -216,6 +222,12 @@ export class PortfolioController { @Query('range') dateRange: DateRange = 'max', @Query('tags') filterByTags?: string ): Promise { + const hasReadRestrictedAccessPermission = + this.userService.hasReadRestrictedAccessPermission({ + impersonationId, + user: this.request.user + }); + const filters = this.apiService.buildFiltersFromQueryParams({ filterByAccounts, filterByAssetClasses, @@ -230,7 +242,7 @@ export class PortfolioController { }); if ( - impersonationId || + hasReadRestrictedAccessPermission || this.userService.isRestrictedView(this.request.user) ) { const maxDividend = dividends.reduce( @@ -266,6 +278,12 @@ export class PortfolioController { @Query('range') dateRange: DateRange = 'max', @Query('tags') filterByTags?: string ): Promise { + const hasReadRestrictedAccessPermission = + this.userService.hasReadRestrictedAccessPermission({ + impersonationId, + user: this.request.user + }); + const filters = this.apiService.buildFiltersFromQueryParams({ filterByAccounts, filterByAssetClasses, @@ -281,7 +299,7 @@ export class PortfolioController { }); if ( - impersonationId || + hasReadRestrictedAccessPermission || this.userService.isRestrictedView(this.request.user) ) { const maxInvestment = investments.reduce( @@ -329,6 +347,12 @@ export class PortfolioController { @Query('tags') filterByTags?: string, @Query('withExcludedAccounts') withExcludedAccounts = false ): Promise { + const hasReadRestrictedAccessPermission = + this.userService.hasReadRestrictedAccessPermission({ + impersonationId, + user: this.request.user + }); + const filters = this.apiService.buildFiltersFromQueryParams({ filterByAccounts, filterByAssetClasses, @@ -344,7 +368,7 @@ export class PortfolioController { }); if ( - impersonationId || + hasReadRestrictedAccessPermission || this.request.user.Settings.settings.viewMode === 'ZEN' || this.userService.isRestrictedView(this.request.user) ) { diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index d923e7c12..a4812c136 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -105,18 +105,33 @@ export class UserService { return usersWithAdminRole.length > 0; } - public isRestrictedView(aUser: UserWithSettings) { - return aUser.Settings.settings.isRestrictedView ?? false; + public hasReadRestrictedAccessPermission({ + impersonationId, + user + }: { + impersonationId: string; + user: UserWithSettings; + }) { + if (!impersonationId) { + return false; + } + + const access = user.Access?.find(({ id }) => { + return id === impersonationId; + }); + + return access?.permissions?.includes('READ_RESTRICTED') ?? true; } - public hasReadRestrictedPermission(user: UserWithSettings): boolean { - return user.permissions.includes('READ_RESTRICTED'); + public isRestrictedView(aUser: UserWithSettings) { + return aUser.Settings.settings.isRestrictedView ?? false; } public async user( userWhereUniqueInput: Prisma.UserWhereUniqueInput ): Promise { const { + Access, accessToken, Account, Analytics, @@ -131,6 +146,7 @@ export class UserService { updatedAt } = await this.prismaService.user.findUnique({ include: { + Access: true, Account: { include: { Platform: true } }, @@ -142,6 +158,7 @@ export class UserService { }); const user: UserWithSettings = { + Access, accessToken, Account, authChallenge, diff --git a/apps/api/src/interceptors/redact-values-in-response.interceptor.ts b/apps/api/src/interceptors/redact-values-in-response.interceptor.ts index 56111bf4b..aca3bd5e4 100644 --- a/apps/api/src/interceptors/redact-values-in-response.interceptor.ts +++ b/apps/api/src/interceptors/redact-values-in-response.interceptor.ts @@ -1,6 +1,7 @@ import { UserService } from '@ghostfolio/api/app/user/user.service'; import { redactAttributes } from '@ghostfolio/api/helper/object.helper'; import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; +import { UserWithSettings } from '@ghostfolio/common/types'; import { CallHandler, ExecutionContext, @@ -22,13 +23,20 @@ export class RedactValuesInResponseInterceptor ): Observable { return next.handle().pipe( map((data: any) => { - const request = context.switchToHttp().getRequest(); - const hasImpersonationId = - !!request.headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()]; + const { headers, user }: { headers: Headers; user: UserWithSettings } = + context.switchToHttp().getRequest(); + + const impersonationId = + headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()]; + const hasReadRestrictedPermission = + this.userService.hasReadRestrictedAccessPermission({ + impersonationId, + user + }); if ( - hasImpersonationId || - this.userService.hasReadRestrictedPermission(request.user) + hasReadRestrictedPermission || + this.userService.isRestrictedView(user) ) { data = redactAttributes({ object: data, diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html index 480652fb5..863ac5e16 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -33,7 +33,9 @@ Permission Restricted view + @if(data?.user?.settings?.isExperimentalFeatures) { View + } diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts index b7850fb38..f3b2f4964 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts @@ -1,5 +1,6 @@ -import { Access } from '@ghostfolio/common/interfaces'; +import { Access, User } from '@ghostfolio/common/interfaces'; export interface CreateOrUpdateAccessDialogParams { access: Access; + user: User; } diff --git a/apps/client/src/app/components/user-account-access/user-account-access.component.ts b/apps/client/src/app/components/user-account-access/user-account-access.component.ts index f6fc723bd..6242725b8 100644 --- a/apps/client/src/app/components/user-account-access/user-account-access.component.ts +++ b/apps/client/src/app/components/user-account-access/user-account-access.component.ts @@ -7,7 +7,6 @@ import { } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; -import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto'; import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { Access, User } from '@ghostfolio/common/interfaces'; @@ -107,7 +106,8 @@ export class UserAccountAccessComponent implements OnDestroy, OnInit { alias: '', permissions: ['READ_RESTRICTED'], type: 'PRIVATE' - } + }, + user: this.user }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', width: this.deviceType === 'mobile' ? '100vw' : '50rem' diff --git a/libs/common/src/lib/types/user-with-settings.type.ts b/libs/common/src/lib/types/user-with-settings.type.ts index c1ec630de..7e10f101a 100644 --- a/libs/common/src/lib/types/user-with-settings.type.ts +++ b/libs/common/src/lib/types/user-with-settings.type.ts @@ -1,10 +1,11 @@ import { UserSettings } from '@ghostfolio/common/interfaces'; import { SubscriptionOffer } from '@ghostfolio/common/types'; import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type'; -import { Account, Settings, User } from '@prisma/client'; +import { Access, Account, Settings, User } from '@prisma/client'; // TODO: Compare with User interface export type UserWithSettings = User & { + Access: Access[]; Account: Account[]; activityCount: number; permissions?: string[];