From dad694471756899ff85c5abcac74a704fdbda0ff Mon Sep 17 00:00:00 2001 From: Thomas <4159106+dtslvr@users.noreply.github.com> Date: Mon, 6 Dec 2021 21:57:26 +0100 Subject: [PATCH] Setup read only mode --- apps/api/src/app/admin/admin.controller.ts | 1 - apps/api/src/app/info/info.service.ts | 9 ++++ apps/api/src/app/user/user.service.ts | 15 ++++++- .../api/src/services/configuration.service.ts | 1 + .../interfaces/environment.interface.ts | 1 + .../src/services/property/property.service.ts | 2 +- .../admin-overview.component.ts | 45 ++++++++++++++----- .../admin-overview/admin-overview.html | 10 +++++ .../admin-overview/admin-overview.module.ts | 9 +++- libs/common/src/lib/config.ts | 1 + .../lib/interfaces/admin-data.interface.ts | 2 +- .../src/lib/interfaces/info-item.interface.ts | 3 +- libs/common/src/lib/permissions.ts | 6 +++ 13 files changed, 86 insertions(+), 19 deletions(-) diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index 054371458..4ec509f04 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -1,7 +1,6 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; import { PropertyDto } from '@ghostfolio/api/services/property/property.dto'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; -import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config'; import { AdminData, AdminMarketData, diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index 074313d5a..073a3dbd9 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -6,6 +6,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { + PROPERTY_IS_READ_ONLY_MODE, PROPERTY_STRIPE_CONFIG, PROPERTY_SYSTEM_MESSAGE } from '@ghostfolio/common/config'; @@ -36,6 +37,7 @@ export class InfoService { public async get(): Promise { const info: Partial = {}; + let isReadOnlyMode: boolean; const platforms = await this.prismaService.platform.findMany({ orderBy: { name: 'asc' }, select: { id: true, name: true } @@ -52,6 +54,12 @@ export class InfoService { globalPermissions.push(permissions.enableImport); } + if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) { + isReadOnlyMode = (await this.propertyService.getByKey( + PROPERTY_IS_READ_ONLY_MODE + )) as boolean; + } + if (this.configurationService.get('ENABLE_FEATURE_SOCIAL_LOGIN')) { globalPermissions.push(permissions.enableSocialLogin); } @@ -77,6 +85,7 @@ export class InfoService { return { ...info, globalPermissions, + isReadOnlyMode, platforms, systemMessage, currencies: this.exchangeRateDataService.getCurrencies(), diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index a513cbba6..f163b9656 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -3,10 +3,14 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.ser import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { baseCurrency, locale } from '@ghostfolio/common/config'; import { User as IUser, UserWithSettings } from '@ghostfolio/common/interfaces'; -import { getPermissions, permissions } from '@ghostfolio/common/permissions'; +import { + getPermissions, + hasRole, + permissions +} from '@ghostfolio/common/permissions'; import { SubscriptionType } from '@ghostfolio/common/types/subscription.type'; import { Injectable } from '@nestjs/common'; -import { Prisma, Provider, User, ViewMode } from '@prisma/client'; +import { Prisma, Provider, Role, User, ViewMode } from '@prisma/client'; import { UserSettingsParams } from './interfaces/user-settings-params.interface'; import { UserSettings } from './interfaces/user-settings.interface'; @@ -80,6 +84,13 @@ export class UserService { currentPermissions.push(permissions.accessFearAndGreedIndex); } + if ( + this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE') && + hasRole(user, Role.ADMIN) + ) { + currentPermissions.push(permissions.toggleReadOnlyMode); + } + user.permissions = currentPermissions; if (userFromDatabase?.Settings) { diff --git a/apps/api/src/services/configuration.service.ts b/apps/api/src/services/configuration.service.ts index 4a44e87db..1956a6b62 100644 --- a/apps/api/src/services/configuration.service.ts +++ b/apps/api/src/services/configuration.service.ts @@ -18,6 +18,7 @@ export class ConfigurationService { ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }), ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }), ENABLE_FEATURE_IMPORT: bool({ default: true }), + ENABLE_FEATURE_READ_ONLY_MODE: bool({ default: false }), ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }), ENABLE_FEATURE_STATISTICS: bool({ default: false }), ENABLE_FEATURE_SUBSCRIPTION: bool({ default: false }), diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index 6902a27ec..e475d32ec 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -9,6 +9,7 @@ export interface Environment extends CleanedEnvAccessors { ENABLE_FEATURE_CUSTOM_SYMBOLS: boolean; ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean; ENABLE_FEATURE_IMPORT: boolean; + ENABLE_FEATURE_READ_ONLY_MODE: boolean; ENABLE_FEATURE_SOCIAL_LOGIN: boolean; ENABLE_FEATURE_STATISTICS: boolean; ENABLE_FEATURE_SUBSCRIPTION: boolean; diff --git a/apps/api/src/services/property/property.service.ts b/apps/api/src/services/property/property.service.ts index 5b576f9bc..4760c3a94 100644 --- a/apps/api/src/services/property/property.service.ts +++ b/apps/api/src/services/property/property.service.ts @@ -14,7 +14,7 @@ export class PropertyService { public async get() { const response: { - [key: string]: object | string | string[]; + [key: string]: boolean | object | string | string[]; } = { [PROPERTY_CURRENCIES]: [] }; diff --git a/apps/client/src/app/components/admin-overview/admin-overview.component.ts b/apps/client/src/app/components/admin-overview/admin-overview.component.ts index 76c78378a..3725dd269 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.component.ts +++ b/apps/client/src/app/components/admin-overview/admin-overview.component.ts @@ -1,4 +1,5 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { MatSlideToggleChange } from '@angular/material/slide-toggle'; import { AdminService } from '@ghostfolio/client/services/admin.service'; import { CacheService } from '@ghostfolio/client/services/cache.service'; import { DataService } from '@ghostfolio/client/services/data.service'; @@ -6,6 +7,7 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; import { DEFAULT_DATE_FORMAT, PROPERTY_CURRENCIES, + PROPERTY_IS_READ_ONLY_MODE, PROPERTY_SYSTEM_MESSAGE } from '@ghostfolio/common/config'; import { InfoItem, User } from '@ghostfolio/common/interfaces'; @@ -32,6 +34,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { public defaultDateFormat = DEFAULT_DATE_FORMAT; public exchangeRates: { label1: string; label2: string; value: number }[]; public hasPermissionForSystemMessage: boolean; + public hasPermissionToToggleReadOnlyMode: boolean; public info: InfoItem; public lastDataGathering: string; public transactionCount: number; @@ -52,27 +55,32 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { ) { this.info = this.dataService.fetchInfo(); - this.hasPermissionForSystemMessage = hasPermission( - this.info.globalPermissions, - permissions.enableSystemMessage - ); - } - - /** - * Initializes the controller - */ - public ngOnInit() { - this.fetchAdminData(); - this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((state) => { if (state?.user) { this.user = state.user; + + this.hasPermissionForSystemMessage = hasPermission( + this.info.globalPermissions, + permissions.enableSystemMessage + ); + + this.hasPermissionToToggleReadOnlyMode = hasPermission( + this.user.permissions, + permissions.toggleReadOnlyMode + ); } }); } + /** + * Initializes the controller + */ + public ngOnInit() { + this.fetchAdminData(); + } + public formatDistanceToNow(aDateString: string) { if (aDateString) { const distanceString = formatDistanceToNowStrict(parseISO(aDateString), { @@ -147,6 +155,10 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { .subscribe(() => {}); } + public onReadOnlyModeChange(aEvent: MatSlideToggleChange) { + this.setReadOnlyMode(aEvent.checked); + } + public onSetSystemMessage() { const systemMessage = prompt('Please set your system message:'); @@ -223,4 +235,13 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { }, 300); }); } + + private setReadOnlyMode(aValue: boolean) { + this.dataService + .putAdminSetting(PROPERTY_IS_READ_ONLY_MODE, { + value: aValue ? 'true' : '' + }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => {}); + } } diff --git a/apps/client/src/app/components/admin-overview/admin-overview.html b/apps/client/src/app/components/admin-overview/admin-overview.html index ca81b5143..80bbf1ab6 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.html +++ b/apps/client/src/app/components/admin-overview/admin-overview.html @@ -146,6 +146,16 @@ +
+
Read-only Mode
+
+ +
+
diff --git a/apps/client/src/app/components/admin-overview/admin-overview.module.ts b/apps/client/src/app/components/admin-overview/admin-overview.module.ts index d87e0c5ad..f75f312ce 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.module.ts +++ b/apps/client/src/app/components/admin-overview/admin-overview.module.ts @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { CacheService } from '@ghostfolio/client/services/cache.service'; import { GfValueModule } from '@ghostfolio/ui/value'; @@ -10,7 +11,13 @@ import { AdminOverviewComponent } from './admin-overview.component'; @NgModule({ declarations: [AdminOverviewComponent], exports: [], - imports: [CommonModule, GfValueModule, MatButtonModule, MatCardModule], + imports: [ + CommonModule, + GfValueModule, + MatButtonModule, + MatCardModule, + MatSlideToggleModule + ], providers: [CacheService], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 6697c92f9..31076cccd 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -31,6 +31,7 @@ export const DEFAULT_DATE_FORMAT = 'dd.MM.yyyy'; export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy'; export const PROPERTY_CURRENCIES = 'CURRENCIES'; +export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE'; export const PROPERTY_LAST_DATA_GATHERING = 'LAST_DATA_GATHERING'; export const PROPERTY_LOCKED_DATA_GATHERING = 'LOCKED_DATA_GATHERING'; export const PROPERTY_STRIPE_CONFIG = 'STRIPE_CONFIG'; diff --git a/libs/common/src/lib/interfaces/admin-data.interface.ts b/libs/common/src/lib/interfaces/admin-data.interface.ts index ce4ea8c0b..a061269e7 100644 --- a/libs/common/src/lib/interfaces/admin-data.interface.ts +++ b/libs/common/src/lib/interfaces/admin-data.interface.ts @@ -4,7 +4,7 @@ export interface AdminData { dataGatheringProgress?: number; exchangeRates: { label1: string; label2: string; value: number }[]; lastDataGathering?: Date | 'IN_PROGRESS'; - settings: { [key: string]: object | string | string[] }; + settings: { [key: string]: boolean | object | string | string[] }; transactionCount: number; userCount: number; users: { diff --git a/libs/common/src/lib/interfaces/info-item.interface.ts b/libs/common/src/lib/interfaces/info-item.interface.ts index 08b511cf3..7ac5f3e9e 100644 --- a/libs/common/src/lib/interfaces/info-item.interface.ts +++ b/libs/common/src/lib/interfaces/info-item.interface.ts @@ -7,11 +7,12 @@ export interface InfoItem { currencies: string[]; demoAuthToken: string; globalPermissions: string[]; + isReadOnlyMode?: boolean; lastDataGathering?: Date; - systemMessage?: string; platforms: { id: string; name: string }[]; primaryDataSource: DataSource; statistics: Statistics; stripePublicKey?: string; subscriptions: Subscription[]; + systemMessage?: string; } diff --git a/libs/common/src/lib/permissions.ts b/libs/common/src/lib/permissions.ts index 8feab7466..b09353bfa 100644 --- a/libs/common/src/lib/permissions.ts +++ b/libs/common/src/lib/permissions.ts @@ -1,4 +1,5 @@ import { Role } from '@prisma/client'; +import { UserWithSettings } from './interfaces'; export const permissions = { accessAdminControl: 'accessAdminControl', @@ -18,6 +19,7 @@ export const permissions = { enableStatistics: 'enableStatistics', enableSubscription: 'enableSubscription', enableSystemMessage: 'enableSystemMessage', + toggleReadOnlyMode: 'toggleReadOnlyMode', updateAccount: 'updateAccount', updateAuthDevice: 'updateAuthDevice', updateOrder: 'updateOrder', @@ -75,3 +77,7 @@ export function getPermissions(aRole: Role): string[] { return []; } } + +export function hasRole(aUser: UserWithSettings, aRole: Role): boolean { + return aUser?.role === aRole; +}