diff --git a/CHANGELOG.md b/CHANGELOG.md index 6193b353..75152a11 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 + +- 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 diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index 3a3604c6..4c25dfa5 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -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', diff --git a/apps/api/src/app/account/account.module.ts b/apps/api/src/app/account/account.module.ts index 1a3bb32e..0b7c2872 100644 --- a/apps/api/src/app/account/account.module.ts +++ b/apps/api/src/app/account/account.module.ts @@ -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] diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index 535c7218..fd516553 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -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']); } diff --git a/apps/api/src/app/order/order.module.ts b/apps/api/src/app/order/order.module.ts index 83dec6f5..92a503d6 100644 --- a/apps/api/src/app/order/order.module.ts +++ b/apps/api/src/app/order/order.module.ts @@ -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], diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index b97b016c..0646f999 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -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 { 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', diff --git a/apps/api/src/app/portfolio/portfolio.module.ts b/apps/api/src/app/portfolio/portfolio.module.ts index e3fb146e..e8161e92 100644 --- a/apps/api/src/app/portfolio/portfolio.module.ts +++ b/apps/api/src/app/portfolio/portfolio.module.ts @@ -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 {} diff --git a/apps/api/src/app/user/interfaces/user-settings.interface.ts b/apps/api/src/app/user/interfaces/user-settings.interface.ts new file mode 100644 index 00000000..7870abee --- /dev/null +++ b/apps/api/src/app/user/interfaces/user-settings.interface.ts @@ -0,0 +1,3 @@ +export interface UserSettings { + isRestrictedView?: boolean; +} diff --git a/apps/api/src/app/user/update-user-setting.dto.ts b/apps/api/src/app/user/update-user-setting.dto.ts new file mode 100644 index 00000000..a6a58313 --- /dev/null +++ b/apps/api/src/app/user/update-user-setting.dto.ts @@ -0,0 +1,6 @@ +import { IsBoolean } from 'class-validator'; + +export class UpdateUserSettingDto { + @IsBoolean() + isRestrictedView?: boolean; +} diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index fd07beae..1aa5e7d1 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -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 = { + ...(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) { diff --git a/apps/api/src/app/user/user.module.ts b/apps/api/src/app/user/user.module.ts index df9439c8..81f083fc 100644 --- a/apps/api/src/app/user/user.module.ts +++ b/apps/api/src/app/user/user.module.ts @@ -14,6 +14,7 @@ import { UserService } from './user.service'; }) ], controllers: [UserController], - providers: [ConfigurationService, PrismaService, UserService] + providers: [ConfigurationService, PrismaService, UserService], + exports: [UserService] }) export class UserModule {} diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 4134d3d0..af40fe37 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -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: { + ...(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 { @@ -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, diff --git a/apps/client/src/app/components/access-table/access-table.component.html b/apps/client/src/app/components/access-table/access-table.component.html index 8897a440..387394b6 100644 --- a/apps/client/src/app/components/access-table/access-table.component.html +++ b/apps/client/src/app/components/access-table/access-table.component.html @@ -10,7 +10,7 @@ Type - Restricted Access + Restricted View diff --git a/apps/client/src/app/pages/account/account-page.component.ts b/apps/client/src/app/pages/account/account-page.component.ts index e8ba412d..3f87e37e 100644 --- a/apps/client/src/app/pages/account/account-page.component.ts +++ b/apps/client/src/app/pages/account/account-page.component.ts @@ -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(); diff --git a/apps/client/src/app/pages/account/account-page.html b/apps/client/src/app/pages/account/account-page.html index 6c9bace9..fb5b4ca0 100644 --- a/apps/client/src/app/pages/account/account-page.html +++ b/apps/client/src/app/pages/account/account-page.html @@ -50,6 +50,22 @@ +
+
+
Restricted View
+
+ Hides absolute values like performances and quantities. +
+
+
+ +
+
diff --git a/apps/client/src/app/pages/account/account-page.scss b/apps/client/src/app/pages/account/account-page.scss index 446a47da..37599724 100644 --- a/apps/client/src/app/pages/account/account-page.scss +++ b/apps/client/src/app/pages/account/account-page.scss @@ -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 { diff --git a/apps/client/src/app/pages/home/home-page.html b/apps/client/src/app/pages/home/home-page.html index 77dd2c94..dd6d73aa 100644 --- a/apps/client/src/app/pages/home/home-page.html +++ b/apps/client/src/app/pages/home/home-page.html @@ -61,7 +61,7 @@ [isLoading]="isLoadingPerformance" [locale]="user?.settings?.locale" [performance]="performance" - [showDetails]="!hasImpersonationId" + [showDetails]="!hasImpersonationId && !user.settings.isRestrictedView" >
diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 9fb6da56..b9d02911 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -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(`/api/order/${aOrder.id}`, aOrder); } + public putUserSetting(aData: UpdateUserSettingDto) { + return this.http.put(`/api/user/setting`, aData); + } + public putUserSettings(aData: UpdateUserSettingsDto) { return this.http.put(`/api/user/settings`, aData); } diff --git a/libs/common/src/lib/interfaces/user-settings.interface.ts b/libs/common/src/lib/interfaces/user-settings.interface.ts index a63394e0..dd13c0e1 100644 --- a/libs/common/src/lib/interfaces/user-settings.interface.ts +++ b/libs/common/src/lib/interfaces/user-settings.interface.ts @@ -2,6 +2,7 @@ import { Currency, ViewMode } from '@prisma/client'; export interface UserSettings { baseCurrency?: Currency; + isRestrictedView?: boolean; locale: string; viewMode?: ViewMode; } diff --git a/prisma/migrations/20210815180121_added_settings_to_settings/migration.sql b/prisma/migrations/20210815180121_added_settings_to_settings/migration.sql new file mode 100644 index 00000000..da863fb7 --- /dev/null +++ b/prisma/migrations/20210815180121_added_settings_to_settings/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Settings" ADD COLUMN "settings" JSONB; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0fe9e818..cc172d53 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 }