From 3d48fbeb3e9b8c2fb30943ac41b0c2f393ffa98f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 21 Aug 2026 10:23:18 +0200 Subject: [PATCH] Add write access --- .../home-overview/home-overview.component.ts | 12 +++++ .../home-overview/home-overview.html | 6 ++- .../account-dialog-host.component.ts | 10 +++-- .../pages/accounts/accounts-page.component.ts | 21 +++------ .../src/app/pages/accounts/accounts-page.html | 5 ++- .../activities/activities-page.component.ts | 6 +++ .../portfolio/activities/activities-page.html | 13 +++++- .../activity-dialog-host.component.ts | 10 +++-- libs/common/src/lib/scopes.spec.ts | 8 ++++ .../accounts-table.component.html | 44 ++++++++++--------- .../accounts-table.component.ts | 2 + .../activities-table.component.html | 28 ++++++------ .../activities-table.component.ts | 2 + 13 files changed, 106 insertions(+), 61 deletions(-) diff --git a/apps/client/src/app/components/home-overview/home-overview.component.ts b/apps/client/src/app/components/home-overview/home-overview.component.ts index 2397fb186..de83a3228 100644 --- a/apps/client/src/app/components/home-overview/home-overview.component.ts +++ b/apps/client/src/app/components/home-overview/home-overview.component.ts @@ -1,5 +1,6 @@ import { GfPortfolioPerformanceComponent } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.component'; import { LayoutService } from '@ghostfolio/client/core/layout.service'; +import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { DEFAULT_CURRENCY, @@ -46,6 +47,7 @@ import { DeviceDetectorService } from 'ngx-device-detector'; }) export class GfHomeOverviewComponent implements OnInit { protected readonly errors = signal([]); + protected readonly hasImpersonationId = signal(false); protected readonly historicalDataItems = signal(null); protected readonly isLoadingPerformance = signal(true); protected readonly performance = signal(null); @@ -88,6 +90,9 @@ export class GfHomeOverviewComponent implements OnInit { private readonly dataService = inject(DataService); private readonly destroyRef = inject(DestroyRef); private readonly deviceDetectorService = inject(DeviceDetectorService); + private readonly impersonationStorageService = inject( + ImpersonationStorageService + ); private readonly layoutService = inject(LayoutService); private readonly userService = inject(UserService); @@ -103,6 +108,13 @@ export class GfHomeOverviewComponent implements OnInit { } public ngOnInit() { + this.impersonationStorageService + .onChangeHasImpersonation() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((impersonationId) => { + this.hasImpersonationId.set(!!impersonationId); + }); + this.layoutService.shouldReloadContent$ .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { diff --git a/apps/client/src/app/components/home-overview/home-overview.html b/apps/client/src/app/components/home-overview/home-overview.html index de5b43045..90a628a17 100644 --- a/apps/client/src/app/components/home-overview/home-overview.html +++ b/apps/client/src/app/components/home-overview/home-overview.html @@ -1,7 +1,11 @@
- @if (hasPermissionToCreateActivity() && user()?.activitiesCount === 0) { + @if ( + !hasImpersonationId() && + hasPermissionToCreateActivity() && + user()?.activitiesCount === 0 + ) {

Welcome to Ghostfolio

diff --git a/apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts b/apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts index 3734889a3..848239299 100644 --- a/apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts +++ b/apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts @@ -9,6 +9,7 @@ import { CreateAccountDto, UpdateAccountDto } from '@ghostfolio/common/dtos'; import { AccountResponse, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; +import { Scope, hasScope, scopes } from '@ghostfolio/common/scopes'; import { DataService } from '@ghostfolio/ui/services'; import { @@ -111,7 +112,7 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit { if ( !account || !hasPermission(user?.permissions, permissions.updateAccount) || - this.isReadOnlyMode(user) + this.isReadOnlyMode(user, scopes.accountUpdate) ) { this.navigateBack(); @@ -140,7 +141,7 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit { if ( !hasPermission(user?.permissions, permissions.createAccount) || - this.isReadOnlyMode(user) + this.isReadOnlyMode(user, scopes.accountCreate) ) { this.navigateBack(); @@ -181,9 +182,10 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit { this.dialogRef?.close(); } - private isReadOnlyMode(user: User) { + private isReadOnlyMode(user: User, requiredScope: Scope) { return ( - !!this.impersonationStorageService.getId() || + (!!this.impersonationStorageService.getId() && + !hasScope(user?.scopes, requiredScope)) || !!user?.settings?.isRestrictedView ); } diff --git a/apps/client/src/app/pages/accounts/accounts-page.component.ts b/apps/client/src/app/pages/accounts/accounts-page.component.ts index a5c739856..039c3f245 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -1,4 +1,3 @@ -import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { TransferBalanceDto } from '@ghostfolio/common/dtos'; import { User } from '@ghostfolio/common/interfaces'; @@ -42,8 +41,8 @@ export class GfAccountsPageComponent implements OnInit { protected accounts: AccountWithValue[]; protected activitiesCount = 0; protected hasPermissionToCreateAccount: boolean; + protected hasPermissionToDeleteAccount: boolean; protected hasPermissionToUpdateAccount: boolean; - protected impersonationId: string | null; protected readonly internalRoutes = internalRoutes; protected totalBalanceInBaseCurrency = 0; protected totalValueInBaseCurrency = 0; @@ -60,9 +59,6 @@ export class GfAccountsPageComponent implements OnInit { private readonly destroyRef = inject(DestroyRef); private readonly deviceDetectorService = inject(DeviceDetectorService); private readonly dialog = inject(MatDialog); - private readonly impersonationStorageService = inject( - ImpersonationStorageService - ); private readonly notificationService = inject(NotificationService); private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); @@ -78,18 +74,7 @@ export class GfAccountsPageComponent implements OnInit { }); } - protected get hasImpersonationId() { - return !!this.impersonationId; - } - public ngOnInit() { - this.impersonationStorageService - .onChangeHasImpersonation() - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((impersonationId) => { - this.impersonationId = impersonationId; - }); - this.userService.stateChanged .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((state) => { @@ -100,6 +85,10 @@ export class GfAccountsPageComponent implements OnInit { hasPermission(this.user.permissions, permissions.createAccount) && hasScope(this.user.scopes, scopes.accountCreate); + this.hasPermissionToDeleteAccount = + hasPermission(this.user.permissions, permissions.deleteAccount) && + hasScope(this.user.scopes, scopes.accountDelete); + this.hasPermissionToUpdateAccount = hasPermission(this.user.permissions, permissions.updateAccount) && hasScope(this.user.scopes, scopes.accountUpdate); diff --git a/apps/client/src/app/pages/accounts/accounts-page.html b/apps/client/src/app/pages/accounts/accounts-page.html index 068e307db..6b9adece1 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.html +++ b/apps/client/src/app/pages/accounts/accounts-page.html @@ -6,9 +6,12 @@ [accounts]="accounts" [activitiesCount]="activitiesCount" [baseCurrency]="user?.settings?.baseCurrency" + [hasPermissionToDeleteAccount]="hasPermissionToDeleteAccount" + [hasPermissionToUpdateAccount]="hasPermissionToUpdateAccount" [locale]="user?.settings?.locale" [showActions]=" - hasPermissionToUpdateAccount && !user.settings.isRestrictedView + (hasPermissionToDeleteAccount || hasPermissionToUpdateAccount) && + !user.settings.isRestrictedView " [totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency" [totalValueInBaseCurrency]="totalValueInBaseCurrency" diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts index c087902c8..657600b70 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts @@ -54,6 +54,7 @@ export class GfActivitiesPageComponent implements OnInit { protected hasImpersonationId: boolean; protected hasPermissionToCreateActivity: boolean; protected hasPermissionToDeleteActivity: boolean; + protected hasPermissionToUpdateActivity: boolean; protected readonly internalRoutes = internalRoutes; protected pageIndex = 0; protected readonly pageSize = DEFAULT_PAGE_SIZE; @@ -302,6 +303,7 @@ export class GfActivitiesPageComponent implements OnInit { this.totalItems = count; if ( + !this.hasImpersonationId && this.hasPermissionToCreateActivity && this.user?.activitiesCount === 0 ) { @@ -338,5 +340,9 @@ export class GfActivitiesPageComponent implements OnInit { this.hasPermissionToDeleteActivity = hasPermission(this.user.permissions, permissions.deleteActivity) && hasScope(this.user.scopes, scopes.activityDelete); + + this.hasPermissionToUpdateActivity = + hasPermission(this.user.permissions, permissions.updateActivity) && + hasScope(this.user.scopes, scopes.activityUpdate); } } diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.html b/apps/client/src/app/pages/portfolio/activities/activities-page.html index a6de858bf..e8cd8ca72 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.html +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -7,15 +7,24 @@ [dataSource]="dataSource" [deviceType]="deviceType" [hasActivities]="user?.activitiesCount > 0" - [hasPermissionToCreateActivity]="hasPermissionToCreateActivity" + [hasPermissionToCreateActivity]=" + hasPermissionToCreateActivity && !hasImpersonationId + " [hasPermissionToDeleteActivity]="hasPermissionToDeleteActivity" [hasPermissionToExportActivities]="!hasImpersonationId" [hasPermissionToFilterByType]="user?.settings?.isExperimentalFeatures" + [hasPermissionToImportActivities]=" + hasPermissionToCreateActivity && !hasImpersonationId + " + [hasPermissionToUpdateActivity]="hasPermissionToUpdateActivity" [locale]="user?.settings?.locale" [pageIndex]="pageIndex" [pageSize]="pageSize" [showActions]=" - hasPermissionToDeleteActivity && !user.settings.isRestrictedView + (hasPermissionToCreateActivity || + hasPermissionToDeleteActivity || + hasPermissionToUpdateActivity) && + !user.settings.isRestrictedView " [sortColumn]="sortColumn" [sortDirection]="sortDirection" diff --git a/apps/client/src/app/pages/portfolio/activities/activity-dialog-host/activity-dialog-host.component.ts b/apps/client/src/app/pages/portfolio/activities/activity-dialog-host/activity-dialog-host.component.ts index 6b70acbad..299945fc0 100644 --- a/apps/client/src/app/pages/portfolio/activities/activity-dialog-host/activity-dialog-host.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/activity-dialog-host/activity-dialog-host.component.ts @@ -4,6 +4,7 @@ import { CreateOrderDto, UpdateOrderDto } from '@ghostfolio/common/dtos'; import { Activity, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; +import { Scope, hasScope, scopes } from '@ghostfolio/common/scopes'; import { DataService } from '@ghostfolio/ui/services'; import { @@ -94,7 +95,7 @@ export class GfActivityDialogHostComponent implements OnDestroy, OnInit { if ( !activity || !hasPermission(user?.permissions, permissions.updateActivity) || - this.isReadOnlyMode(user) + this.isReadOnlyMode(user, scopes.activityUpdate) ) { this.navigateBack(); @@ -115,7 +116,7 @@ export class GfActivityDialogHostComponent implements OnDestroy, OnInit { // Cloning creates a new activity as well if ( !hasPermission(user?.permissions, permissions.createActivity) || - this.isReadOnlyMode(user) + this.isReadOnlyMode(user, scopes.activityCreate) ) { this.navigateBack(); @@ -156,9 +157,10 @@ export class GfActivityDialogHostComponent implements OnDestroy, OnInit { this.dialogRef?.close(); } - private isReadOnlyMode(user: User) { + private isReadOnlyMode(user: User, requiredScope: Scope) { return ( - !!this.impersonationStorageService.getId() || + (!!this.impersonationStorageService.getId() && + !hasScope(user?.scopes, requiredScope)) || !!user?.settings?.isRestrictedView ); } diff --git a/libs/common/src/lib/scopes.spec.ts b/libs/common/src/lib/scopes.spec.ts index e96f1bed3..ea436d7e6 100644 --- a/libs/common/src/lib/scopes.spec.ts +++ b/libs/common/src/lib/scopes.spec.ts @@ -136,6 +136,14 @@ describe('Scopes', () => { getScopesOfAccess({ scopes: [...SCOPES_OF_READ_ACCESS] }) ).not.toContain(scopes.portfolioReadValues); }); + + // The dialog offers the write scopes for a private access only, hence this + // function is the sole barrier for a public access + it('Gives no write scope', () => { + expect( + getScopesOfAccess({ scopes: [...SCOPES_OF_WRITE_ACCESS] }) + ).toEqual([]); + }); }); describe('Get scopes of own access', () => { diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.html b/libs/ui/src/lib/accounts-table/accounts-table.component.html index 0b4bb72ce..05e54534a 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.html +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -338,26 +338,30 @@ } - - - - Edit... - - -
- + @if (hasPermissionToUpdateAccount()) { + + + + Edit... + + + } + @if (hasPermissionToDeleteAccount()) { +
+ + } diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.ts b/libs/ui/src/lib/accounts-table/accounts-table.component.ts index cca3aa7b0..93b394951 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.ts +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.ts @@ -59,7 +59,9 @@ export class GfAccountsTableComponent { public readonly accounts = input.required(); public readonly activitiesCount = input(); public readonly baseCurrency = input(); + public readonly hasPermissionToDeleteAccount = input(); public readonly hasPermissionToOpenDetails = input(true); + public readonly hasPermissionToUpdateAccount = input(); public readonly locale = input(getLocale()); public readonly showActions = input(); public readonly showActivitiesCount = input(true); diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index 9c10b5cb7..2159d89d3 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -19,7 +19,7 @@ }
- @if (hasPermissionToCreateActivity) { + @if (hasPermissionToImportActivities) {
} - @if (hasPermissionToCreateActivity) { + @if (hasPermissionToImportActivities) { } - - - - Edit... - - + @if (hasPermissionToUpdateActivity) { + + + + Edit... + + + }