Feature/add restricted view (#295)

* Add restricted view

* Update changelog
pull/296/head
Thomas Kaul 3 years ago committed by GitHub
parent 7c91727eb1
commit 05b0efef82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added an option to hide absolute values like performances and quantities (_Restricted View_)
### Changed ### Changed
- Restructured the allocations page - Restructured the allocations page
@ -21,6 +25,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Removed the current net performance - Removed the current net performance
- Removed the read foreign portfolio permission - Removed the read foreign portfolio permission
### Todo
- Apply data migration (`yarn database:push`)
## 1.38.0 - 14.08.2021 ## 1.38.0 - 14.08.2021
### Added ### Added

@ -1,3 +1,4 @@
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper'; import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { import {
@ -33,7 +34,8 @@ export class AccountController {
public constructor( public constructor(
private readonly accountService: AccountService, private readonly accountService: AccountService,
private readonly impersonationService: ImpersonationService, private readonly impersonationService: ImpersonationService,
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService
) {} ) {}
@Delete(':id') @Delete(':id')
@ -94,7 +96,10 @@ export class AccountController {
impersonationUserId || this.request.user.id impersonationUserId || this.request.user.id
); );
if (impersonationUserId) { if (
impersonationUserId ||
this.userService.isRestrictedView(this.request.user)
) {
accounts = nullifyValuesInObjects(accounts, [ accounts = nullifyValuesInObjects(accounts, [
'balance', 'balance',
'fee', 'fee',

@ -1,4 +1,5 @@
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
@ -16,7 +17,8 @@ import { AccountService } from './account.service';
ExchangeRateDataModule, ExchangeRateDataModule,
ImpersonationModule, ImpersonationModule,
RedisCacheModule, RedisCacheModule,
PrismaModule PrismaModule,
UserModule
], ],
controllers: [AccountController], controllers: [AccountController],
providers: [AccountService] providers: [AccountService]

@ -1,3 +1,4 @@
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper'; import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { import {
@ -34,7 +35,8 @@ export class OrderController {
public constructor( public constructor(
private readonly impersonationService: ImpersonationService, private readonly impersonationService: ImpersonationService,
private readonly orderService: OrderService, private readonly orderService: OrderService,
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService
) {} ) {}
@Delete(':id') @Delete(':id')
@ -88,7 +90,10 @@ export class OrderController {
where: { userId: impersonationUserId || this.request.user.id } where: { userId: impersonationUserId || this.request.user.id }
}); });
if (impersonationUserId) { if (
impersonationUserId ||
this.userService.isRestrictedView(this.request.user)
) {
orders = nullifyValuesInObjects(orders, ['fee', 'quantity', 'unitPrice']); orders = nullifyValuesInObjects(orders, ['fee', 'quantity', 'unitPrice']);
} }

@ -1,5 +1,6 @@
import { CacheService } from '@ghostfolio/api/app/cache/cache.service'; import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
@ -17,7 +18,8 @@ import { OrderService } from './order.service';
DataProviderModule, DataProviderModule,
ImpersonationModule, ImpersonationModule,
PrismaModule, PrismaModule,
RedisCacheModule RedisCacheModule,
UserModule
], ],
controllers: [OrderController], controllers: [OrderController],
providers: [CacheService, OrderService], providers: [CacheService, OrderService],

@ -1,9 +1,9 @@
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { import {
hasNotDefinedValuesInObject, hasNotDefinedValuesInObject,
nullifyValuesInObject nullifyValuesInObject
} from '@ghostfolio/api/helper/object.helper'; } from '@ghostfolio/api/helper/object.helper';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { import {
PortfolioPerformance, PortfolioPerformance,
PortfolioPosition, PortfolioPosition,
@ -39,9 +39,9 @@ import { PortfolioService } from './portfolio.service';
export class PortfolioController { export class PortfolioController {
public constructor( public constructor(
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly impersonationService: ImpersonationService,
private readonly portfolioService: PortfolioService, private readonly portfolioService: PortfolioService,
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService
) {} ) {}
@Get('investments') @Get('investments')
@ -53,7 +53,10 @@ export class PortfolioController {
impersonationId impersonationId
); );
if (impersonationId) { if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
const maxInvestment = investments.reduce( const maxInvestment = investments.reduce(
(investment, item) => Math.max(investment, item.investment), (investment, item) => Math.max(investment, item.investment),
1 1
@ -92,7 +95,10 @@ export class PortfolioController {
res.status(StatusCodes.ACCEPTED); res.status(StatusCodes.ACCEPTED);
} }
if (impersonationId) { if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
let maxValue = 0; let maxValue = 0;
chartData.forEach((portfolioItem) => { chartData.forEach((portfolioItem) => {
@ -133,7 +139,10 @@ export class PortfolioController {
res.status(StatusCodes.ACCEPTED); res.status(StatusCodes.ACCEPTED);
} }
if (impersonationId) { if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
const totalInvestment = Object.values(details) const totalInvestment = Object.values(details)
.map((portfolioPosition) => { .map((portfolioPosition) => {
return portfolioPosition.investment; return portfolioPosition.investment;
@ -187,7 +196,10 @@ export class PortfolioController {
} }
let performance = performanceInformation.performance; let performance = performanceInformation.performance;
if (impersonationId) { if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
performance = nullifyValuesInObject(performance, [ performance = nullifyValuesInObject(performance, [
'currentGrossPerformance', 'currentGrossPerformance',
'currentValue' 'currentValue'
@ -213,7 +225,10 @@ export class PortfolioController {
res.status(StatusCodes.ACCEPTED); res.status(StatusCodes.ACCEPTED);
} }
if (impersonationId) { if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
result.positions = result.positions.map((position) => { result.positions = result.positions.map((position) => {
return nullifyValuesInObject(position, [ return nullifyValuesInObject(position, [
'grossPerformance', 'grossPerformance',
@ -233,7 +248,10 @@ export class PortfolioController {
): Promise<PortfolioSummary> { ): Promise<PortfolioSummary> {
let summary = await this.portfolioService.getSummary(impersonationId); let summary = await this.portfolioService.getSummary(impersonationId);
if (impersonationId) { if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
summary = nullifyValuesInObject(summary, [ summary = nullifyValuesInObject(summary, [
'cash', 'cash',
'committedFunds', 'committedFunds',
@ -261,7 +279,10 @@ export class PortfolioController {
); );
if (position) { if (position) {
if (impersonationId) { if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
position = nullifyValuesInObject(position, [ position = nullifyValuesInObject(position, [
'grossPerformance', 'grossPerformance',
'investment', 'investment',

@ -1,6 +1,6 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { OrderModule } from '@ghostfolio/api/app/order/order.module'; import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
@ -24,7 +24,8 @@ import { RulesService } from './rules.service';
ExchangeRateDataModule, ExchangeRateDataModule,
ImpersonationModule, ImpersonationModule,
OrderModule, OrderModule,
PrismaModule PrismaModule,
UserModule
], ],
controllers: [PortfolioController], controllers: [PortfolioController],
providers: [ providers: [
@ -33,8 +34,7 @@ import { RulesService } from './rules.service';
MarketDataService, MarketDataService,
PortfolioService, PortfolioService,
RulesService, RulesService,
SymbolProfileService, SymbolProfileService
UserService
] ]
}) })
export class PortfolioModule {} export class PortfolioModule {}

@ -0,0 +1,3 @@
export interface UserSettings {
isRestrictedView?: boolean;
}

@ -0,0 +1,6 @@
import { IsBoolean } from 'class-validator';
export class UpdateUserSettingDto {
@IsBoolean()
isRestrictedView?: boolean;
}

@ -26,6 +26,8 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { UserItem } from './interfaces/user-item.interface'; import { UserItem } from './interfaces/user-item.interface';
import { UserSettingsParams } from './interfaces/user-settings-params.interface'; import { UserSettingsParams } from './interfaces/user-settings-params.interface';
import { UserSettings } from './interfaces/user-settings.interface';
import { UpdateUserSettingDto } from './update-user-setting.dto';
import { UpdateUserSettingsDto } from './update-user-settings.dto'; import { UpdateUserSettingsDto } from './update-user-settings.dto';
import { UserService } from './user.service'; import { UserService } from './user.service';
@ -78,6 +80,32 @@ export class UserController {
}; };
} }
@Put('setting')
@UseGuards(AuthGuard('jwt'))
public async updateUserSetting(@Body() data: UpdateUserSettingDto) {
if (
!hasPermission(
getPermissions(this.request.user.role),
permissions.updateUserSettings
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const userSettings: UserSettings = {
...(<UserSettings>this.request.user.Settings.settings),
...data
};
return await this.userService.updateUserSetting({
userSettings,
userId: this.request.user.id
});
}
@Put('settings') @Put('settings')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async updateUserSettings(@Body() data: UpdateUserSettingsDto) { public async updateUserSettings(@Body() data: UpdateUserSettingsDto) {

@ -14,6 +14,7 @@ import { UserService } from './user.service';
}) })
], ],
controllers: [UserController], controllers: [UserController],
providers: [ConfigurationService, PrismaService, UserService] providers: [ConfigurationService, PrismaService, UserService],
exports: [UserService]
}) })
export class UserModule {} export class UserModule {}

@ -9,6 +9,7 @@ import { Currency, Prisma, Provider, User, ViewMode } from '@prisma/client';
import { isBefore } from 'date-fns'; import { isBefore } from 'date-fns';
import { UserSettingsParams } from './interfaces/user-settings-params.interface'; import { UserSettingsParams } from './interfaces/user-settings-params.interface';
import { UserSettings } from './interfaces/user-settings.interface';
const crypto = require('crypto'); const crypto = require('crypto');
@ -50,6 +51,7 @@ export class UserService {
}), }),
accounts: Account, accounts: Account,
settings: { settings: {
...(<UserSettings>Settings.settings),
locale, locale,
baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY, baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY,
viewMode: Settings?.viewMode ?? ViewMode.DEFAULT viewMode: Settings?.viewMode ?? ViewMode.DEFAULT
@ -57,6 +59,10 @@ export class UserService {
}; };
} }
public isRestrictedView(aUser: UserWithSettings) {
return (aUser.Settings.settings as UserSettings)?.isRestrictedView ?? false;
}
public async user( public async user(
userWhereUniqueInput: Prisma.UserWhereUniqueInput userWhereUniqueInput: Prisma.UserWhereUniqueInput
): Promise<UserWithSettings | null> { ): Promise<UserWithSettings | null> {
@ -84,6 +90,7 @@ export class UserService {
// Set default settings if needed // Set default settings if needed
userFromDatabase.Settings = { userFromDatabase.Settings = {
currency: UserService.DEFAULT_CURRENCY, currency: UserService.DEFAULT_CURRENCY,
settings: null,
updatedAt: new Date(), updatedAt: new Date(),
userId: userFromDatabase?.id, userId: userFromDatabase?.id,
viewMode: ViewMode.DEFAULT viewMode: ViewMode.DEFAULT
@ -219,6 +226,35 @@ export class UserService {
}); });
} }
public async updateUserSetting({
userId,
userSettings
}: {
userId: string;
userSettings: UserSettings;
}) {
const settings = userSettings as Prisma.JsonObject;
await this.prismaService.settings.upsert({
create: {
settings,
User: {
connect: {
id: userId
}
}
},
update: {
settings
},
where: {
userId: userId
}
});
return;
}
public async updateUserSettings({ public async updateUserSettings({
currency, currency,
userId, userId,

@ -10,7 +10,7 @@
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Type</th> <th *matHeaderCellDef class="px-1" i18n mat-header-cell>Type</th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<ion-icon class="mr-1" name="lock-closed-outline"></ion-icon> <ion-icon class="mr-1" name="lock-closed-outline"></ion-icon>
Restricted Access Restricted View
</td></ng-container </td></ng-container
> >

@ -136,6 +136,24 @@ export class AccountPageComponent implements OnDestroy, OnInit {
}); });
} }
public onRestrictedViewChange(aEvent: MatSlideToggleChange) {
this.dataService
.putUserSetting({ isRestrictedView: aEvent.checked })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
}
public onSignInWithFingerprintChange(aEvent: MatSlideToggleChange) { public onSignInWithFingerprintChange(aEvent: MatSlideToggleChange) {
if (aEvent.checked) { if (aEvent.checked) {
this.registerDevice(); this.registerDevice();

@ -50,6 +50,22 @@
</div> </div>
</div> </div>
</div> </div>
<div class="align-items-center d-flex mt-4 py-1">
<div class="w-50">
<div i18n>Restricted View</div>
<div class="hint-text text-muted" i18n>
Hides absolute values like performances and quantities.
</div>
</div>
<div class="w-50">
<mat-slide-toggle
color="primary"
[checked]="user.settings.isRestrictedView"
[disabled]="!hasPermissionToUpdateUserSettings"
(change)="onRestrictedViewChange($event)"
></mat-slide-toggle>
</div>
</div>
<div class="d-flex mt-4 py-1"> <div class="d-flex mt-4 py-1">
<form #changeUserSettingsForm="ngForm" class="w-100"> <form #changeUserSettingsForm="ngForm" class="w-100">
<div class="d-flex mb-2"> <div class="d-flex mb-2">

@ -2,6 +2,11 @@
color: rgb(var(--dark-primary-text)); color: rgb(var(--dark-primary-text));
display: block; display: block;
.hint-text {
font-size: 90%;
line-height: 1.2;
}
.mat-form-field { .mat-form-field {
::ng-deep { ::ng-deep {
.mat-form-field-wrapper { .mat-form-field-wrapper {

@ -61,7 +61,7 @@
[isLoading]="isLoadingPerformance" [isLoading]="isLoadingPerformance"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[performance]="performance" [performance]="performance"
[showDetails]="!hasImpersonationId" [showDetails]="!hasImpersonationId && !user.settings.isRestrictedView"
></gf-portfolio-performance> ></gf-portfolio-performance>
<div class="text-center"> <div class="text-center">
<gf-toggle <gf-toggle

@ -49,7 +49,7 @@
[isLoading]="isLoadingPerformance" [isLoading]="isLoadingPerformance"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[performance]="performance" [performance]="performance"
[showDetails]="!hasImpersonationId" [showDetails]="!hasImpersonationId && !user.settings.isRestrictedView"
></gf-portfolio-performance> ></gf-portfolio-performance>
</div> </div>
</div> </div>

@ -13,6 +13,7 @@ import { PortfolioPositions } from '@ghostfolio/api/app/portfolio/interfaces/por
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface'; import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface'; import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
import { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-settings.dto'; import { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-settings.dto';
import { import {
Access, Access,
@ -210,6 +211,10 @@ export class DataService {
return this.http.put<UserItem>(`/api/order/${aOrder.id}`, aOrder); return this.http.put<UserItem>(`/api/order/${aOrder.id}`, aOrder);
} }
public putUserSetting(aData: UpdateUserSettingDto) {
return this.http.put<User>(`/api/user/setting`, aData);
}
public putUserSettings(aData: UpdateUserSettingsDto) { public putUserSettings(aData: UpdateUserSettingsDto) {
return this.http.put<User>(`/api/user/settings`, aData); return this.http.put<User>(`/api/user/settings`, aData);
} }

@ -2,6 +2,7 @@ import { Currency, ViewMode } from '@prisma/client';
export interface UserSettings { export interface UserSettings {
baseCurrency?: Currency; baseCurrency?: Currency;
isRestrictedView?: boolean;
locale: string; locale: string;
viewMode?: ViewMode; viewMode?: ViewMode;
} }

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "settings" JSONB;

@ -109,8 +109,9 @@ model Property {
model Settings { model Settings {
currency Currency? currency Currency?
viewMode ViewMode? settings Json?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
viewMode ViewMode?
User User @relation(fields: [userId], references: [id]) User User @relation(fields: [userId], references: [id])
userId String @id userId String @id
} }

Loading…
Cancel
Save