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
### Added
- Added an option to hide absolute values like performances and quantities (_Restricted View_)
### Changed
- 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 read foreign portfolio permission
### Todo
- Apply data migration (`yarn database:push`)
## 1.38.0 - 14.08.2021
### Added

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

@ -1,4 +1,5 @@
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 { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
@ -16,7 +17,8 @@ import { AccountService } from './account.service';
ExchangeRateDataModule,
ImpersonationModule,
RedisCacheModule,
PrismaModule
PrismaModule,
UserModule
],
controllers: [AccountController],
providers: [AccountService]

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

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

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

@ -1,6 +1,6 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service';
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 { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
@ -24,7 +24,8 @@ import { RulesService } from './rules.service';
ExchangeRateDataModule,
ImpersonationModule,
OrderModule,
PrismaModule
PrismaModule,
UserModule
],
controllers: [PortfolioController],
providers: [
@ -33,8 +34,7 @@ import { RulesService } from './rules.service';
MarketDataService,
PortfolioService,
RulesService,
SymbolProfileService,
UserService
SymbolProfileService
]
})
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 { 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 { 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')
@UseGuards(AuthGuard('jwt'))
public async updateUserSettings(@Body() data: UpdateUserSettingsDto) {

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

@ -9,6 +9,7 @@ import { Currency, Prisma, Provider, User, ViewMode } from '@prisma/client';
import { isBefore } from 'date-fns';
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
import { UserSettings } from './interfaces/user-settings.interface';
const crypto = require('crypto');
@ -50,6 +51,7 @@ export class UserService {
}),
accounts: Account,
settings: {
...(<UserSettings>Settings.settings),
locale,
baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY,
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(
userWhereUniqueInput: Prisma.UserWhereUniqueInput
): Promise<UserWithSettings | null> {
@ -84,6 +90,7 @@ export class UserService {
// Set default settings if needed
userFromDatabase.Settings = {
currency: UserService.DEFAULT_CURRENCY,
settings: null,
updatedAt: new Date(),
userId: userFromDatabase?.id,
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({
currency,
userId,

@ -10,7 +10,7 @@
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Type</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<ion-icon class="mr-1" name="lock-closed-outline"></ion-icon>
Restricted Access
Restricted View
</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) {
if (aEvent.checked) {
this.registerDevice();

@ -50,6 +50,22 @@
</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">
<form #changeUserSettingsForm="ngForm" class="w-100">
<div class="d-flex mb-2">

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

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

@ -49,7 +49,7 @@
[isLoading]="isLoadingPerformance"
[locale]="user?.settings?.locale"
[performance]="performance"
[showDetails]="!hasImpersonationId"
[showDetails]="!hasImpersonationId && !user.settings.isRestrictedView"
></gf-portfolio-performance>
</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 { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-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 {
Access,
@ -210,6 +211,10 @@ export class DataService {
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) {
return this.http.put<User>(`/api/user/settings`, aData);
}

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

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

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

Loading…
Cancel
Save