From 84be07f8dbd6a0241f3f316b150d77cacbb1f505 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:31:43 +0200 Subject: [PATCH] Add write access --- .../access-table/access-table.component.html | 4 ++++ .../access-table/access-table.component.ts | 4 ++++ .../home-holdings/home-holdings.component.ts | 22 ++++--------------- .../home-holdings/home-holdings.html | 6 +---- .../home-overview/home-overview.component.ts | 18 +++++---------- .../home-overview/home-overview.html | 6 +---- .../home-watchlist.component.ts | 19 +++------------- .../home-watchlist/home-watchlist.html | 2 +- ...reate-or-update-access-dialog.component.ts | 19 ++++++++++++---- .../create-or-update-access-dialog.html | 17 ++++++++++++++ .../pages/accounts/accounts-page.component.ts | 16 +++++++------- .../src/app/pages/accounts/accounts-page.html | 10 ++------- .../activities/activities-page.component.ts | 7 +++--- .../portfolio/activities/activities-page.html | 10 ++------- libs/common/src/lib/scopes.spec.ts | 15 +++++++++---- libs/common/src/lib/scopes.ts | 5 +++-- 16 files changed, 85 insertions(+), 95 deletions(-) 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 77da69d23..3189b6afc 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 @@ -25,6 +25,10 @@ Restricted view } + @if (hasScopesToWrite(element)) { + + Changes + } diff --git a/apps/client/src/app/components/access-table/access-table.component.ts b/apps/client/src/app/components/access-table/access-table.component.ts index 3886fc59c..3affcef7a 100644 --- a/apps/client/src/app/components/access-table/access-table.component.ts +++ b/apps/client/src/app/components/access-table/access-table.component.ts @@ -105,6 +105,10 @@ export class GfAccessTableComponent { return hasScope(scopesOfAccess, scopes.portfolioReadValues); } + protected hasScopesToWrite({ scopes: scopesOfAccess }: Access) { + return hasScope(scopesOfAccess, scopes.activityCreate); + } + protected onCopyUrlToClipboard(aId: string) { this.clipboard.copy(this.getPublicUrl(aId)); diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts index 45dcd1a97..ca6351429 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.component.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.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 { AssetProfileIdentifier, @@ -7,6 +6,7 @@ import { } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; +import { hasScope, scopes } from '@ghostfolio/common/scopes'; import { HoldingType, HoldingsViewMode, @@ -51,7 +51,6 @@ export class GfHomeHoldingsComponent implements OnInit { public static DEFAULT_HOLDINGS_VIEW_MODE: HoldingsViewMode = 'TABLE'; protected deviceType: string; - protected hasImpersonationId: boolean; protected hasPermissionToAccessHoldingsChart: boolean; protected hasPermissionToCreateActivity: boolean; protected holdings: PortfolioPosition[] | undefined; @@ -83,9 +82,6 @@ export class GfHomeHoldingsComponent implements OnInit { private readonly dataService = inject(DataService); private readonly destroyRef = inject(DestroyRef); private readonly deviceDetectorService = inject(DeviceDetectorService); - private readonly impersonationStorageService = inject( - ImpersonationStorageService - ); private readonly router = inject(Router); private readonly userService = inject(UserService); @@ -96,15 +92,6 @@ export class GfHomeHoldingsComponent implements OnInit { public ngOnInit() { this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; - this.impersonationStorageService - .onChangeHasImpersonation() - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((impersonationId) => { - this.hasImpersonationId = !!impersonationId; - - this.changeDetectorRef.markForCheck(); - }); - this.userService.stateChanged .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((state) => { @@ -116,10 +103,9 @@ export class GfHomeHoldingsComponent implements OnInit { permissions.accessHoldingsChart ); - this.hasPermissionToCreateActivity = hasPermission( - this.user.permissions, - permissions.createActivity - ); + this.hasPermissionToCreateActivity = + hasPermission(this.user.permissions, permissions.createActivity) && + hasScope(this.user.scopes, scopes.activityCreate); this.initialize(); } diff --git a/apps/client/src/app/components/home-holdings/home-holdings.html b/apps/client/src/app/components/home-holdings/home-holdings.html index d3b24e7af..5c6a95d8e 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.html +++ b/apps/client/src/app/components/home-holdings/home-holdings.html @@ -41,11 +41,7 @@ [locale]="user?.settings?.locale" (holdingClicked)="onHoldingClicked($event)" /> - @if ( - !hasImpersonationId && - hasPermissionToCreateActivity && - holdings?.length > 0 - ) { + @if (hasPermissionToCreateActivity && holdings?.length > 0) {
([]); - protected readonly hasImpersonationId = signal(false); protected readonly historicalDataItems = signal(null); protected readonly isLoadingPerformance = signal(true); protected readonly performance = signal(null); @@ -66,7 +65,10 @@ export class GfHomeOverviewComponent implements OnInit { ); protected readonly hasPermissionToCreateActivity = computed(() => { - return hasPermission(this.user()?.permissions, permissions.createActivity); + return ( + hasPermission(this.user()?.permissions, permissions.createActivity) && + hasScope(this.user()?.scopes, scopes.activityCreate) + ); }); protected readonly showDetails = computed(() => { @@ -86,9 +88,6 @@ 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); @@ -104,13 +103,6 @@ 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 90a628a17..de5b43045 100644 --- a/apps/client/src/app/components/home-overview/home-overview.html +++ b/apps/client/src/app/components/home-overview/home-overview.html @@ -1,11 +1,7 @@
- @if ( - !hasImpersonationId() && - hasPermissionToCreateActivity() && - user()?.activitiesCount === 0 - ) { + @if (hasPermissionToCreateActivity() && user()?.activitiesCount === 0) {

Welcome to Ghostfolio

diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts index 7492e4e92..3e0d48297 100644 --- a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts +++ b/apps/client/src/app/components/home-watchlist/home-watchlist.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 { DEFAULT_LOCALE } from '@ghostfolio/common/config'; import { @@ -7,6 +6,7 @@ import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { hasScope, scopes } from '@ghostfolio/common/scopes'; import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark'; import { GfFabComponent } from '@ghostfolio/ui/fab'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; @@ -44,7 +44,6 @@ import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/ templateUrl: './home-watchlist.html' }) export class GfHomeWatchlistComponent implements OnInit { - protected hasImpersonationId: boolean; protected hasPermissionToCreateWatchlistItem: boolean; protected hasPermissionToDeleteWatchlistItem: boolean; protected user: User; @@ -59,21 +58,11 @@ export class GfHomeWatchlistComponent implements OnInit { private readonly destroyRef = inject(DestroyRef); private readonly deviceDetectorService = inject(DeviceDetectorService); private readonly dialog = inject(MatDialog); - private readonly impersonationStorageService = inject( - ImpersonationStorageService - ); private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly userService = inject(UserService); public constructor() { - this.impersonationStorageService - .onChangeHasImpersonation() - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((impersonationId) => { - this.hasImpersonationId = !!impersonationId; - }); - this.route.queryParams .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((params) => { @@ -89,17 +78,15 @@ export class GfHomeWatchlistComponent implements OnInit { this.user = state.user; this.hasPermissionToCreateWatchlistItem = - !this.hasImpersonationId && hasPermission( this.user.permissions, permissions.createWatchlistItem - ); + ) && hasScope(this.user.scopes, scopes.watchlistCreate); this.hasPermissionToDeleteWatchlistItem = - !this.hasImpersonationId && hasPermission( this.user.permissions, permissions.deleteWatchlistItem - ); + ) && hasScope(this.user.scopes, scopes.watchlistDelete); this.changeDetectorRef.markForCheck(); } diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.html b/apps/client/src/app/components/home-watchlist/home-watchlist.html index e2865b9cf..cc27cdc42 100644 --- a/apps/client/src/app/components/home-watchlist/home-watchlist.html +++ b/apps/client/src/app/components/home-watchlist/home-watchlist.html @@ -21,6 +21,6 @@
-@if (!hasImpersonationId && hasPermissionToCreateWatchlistItem) { +@if (hasPermissionToCreateWatchlistItem) { } diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts index 12425b50e..34d8d569c 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts @@ -4,6 +4,7 @@ import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; import { SCOPES_OF_READ_ACCESS, SCOPES_OF_READ_RESTRICTED_ACCESS, + SCOPES_OF_WRITE_ACCESS, hasScope, scopes } from '@ghostfolio/common/scopes'; @@ -46,6 +47,7 @@ import { import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { StatusCodes } from 'http-status-codes'; import { EMPTY, catchError } from 'rxjs'; @@ -62,6 +64,7 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces'; MatFormFieldModule, MatInputModule, MatSelectModule, + MatSlideToggleModule, ReactiveFormsModule ], selector: 'gf-create-or-update-access-dialog', @@ -120,6 +123,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { access?.scopes, scopes.portfolioReadValues ), + hasScopesToWrite: hasScope(access?.scopes, scopes.activityCreate), type: [ { disabled: this.mode === 'update', value: access?.type ?? 'PRIVATE' }, Validators.required @@ -149,6 +153,8 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { 'hasScopeToReadValues' ); + const hasScopesToWriteControl = this.accessForm.get('hasScopesToWrite'); + if (accessType === 'PRIVATE') { granteeUserIdControl?.setValidators(Validators.required); this.accessForm.get('filters')?.setValue(null); @@ -156,8 +162,10 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { granteeUserIdControl?.clearValidators(); granteeUserIdControl?.setValue(null); - // A public access never exposes the monetary values + // A public access never exposes the monetary values and never + // changes data hasScopeToReadValuesControl?.setValue(false); + hasScopesToWriteControl?.setValue(false); } granteeUserIdControl?.updateValueAndValidity(); @@ -187,14 +195,17 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { } /** - * The dialog offers the read access only. The write scopes are not granted - * here yet. + * The write access is granted as one unit, hence the dialog offers a single + * control for all write scopes */ private buildScopes() { return [ ...(this.accessForm.get('hasScopeToReadValues')?.value ? SCOPES_OF_READ_ACCESS - : SCOPES_OF_READ_RESTRICTED_ACCESS) + : SCOPES_OF_READ_RESTRICTED_ACCESS), + ...(this.accessForm.get('hasScopesToWrite')?.value + ? SCOPES_OF_WRITE_ACCESS + : []) ]; } diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html index 601548e98..dd3168f33 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -44,6 +44,23 @@
+ @if (accessForm.get('type')?.value === 'PRIVATE') { +
+
+
Changes
+
+ Allow to add, to update and to delete data of your portfolio +
+
+
+ +
+
+ } @if (accessForm.get('type')?.value === 'PRIVATE') {
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 0f21ef55e..a5c739856 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -4,6 +4,7 @@ import { TransferBalanceDto } from '@ghostfolio/common/dtos'; import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; +import { hasScope, scopes } from '@ghostfolio/common/scopes'; import { AccountWithValue } from '@ghostfolio/common/types'; import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table'; import { GfFabComponent } from '@ghostfolio/ui/fab'; @@ -95,14 +96,13 @@ export class GfAccountsPageComponent implements OnInit { if (state?.user) { this.user = state.user; - this.hasPermissionToCreateAccount = hasPermission( - this.user.permissions, - permissions.createAccount - ); - this.hasPermissionToUpdateAccount = hasPermission( - this.user.permissions, - permissions.updateAccount - ); + this.hasPermissionToCreateAccount = + hasPermission(this.user.permissions, permissions.createAccount) && + hasScope(this.user.scopes, scopes.accountCreate); + + this.hasPermissionToUpdateAccount = + hasPermission(this.user.permissions, permissions.updateAccount) && + hasScope(this.user.scopes, scopes.accountUpdate); this.fetchAccounts(); } diff --git a/apps/client/src/app/pages/accounts/accounts-page.html b/apps/client/src/app/pages/accounts/accounts-page.html index 8935f70f5..068e307db 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.html +++ b/apps/client/src/app/pages/accounts/accounts-page.html @@ -8,9 +8,7 @@ [baseCurrency]="user?.settings?.baseCurrency" [locale]="user?.settings?.locale" [showActions]=" - !hasImpersonationId && - hasPermissionToUpdateAccount && - !user.settings.isRestrictedView + hasPermissionToUpdateAccount && !user.settings.isRestrictedView " [totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency" [totalValueInBaseCurrency]="totalValueInBaseCurrency" @@ -20,11 +18,7 @@
- @if ( - !hasImpersonationId && - hasPermissionToCreateAccount && - !user.settings.isRestrictedView - ) { + @if (hasPermissionToCreateAccount && !user.settings.isRestrictedView) { 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 c67629bb6..c087902c8 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 @@ -10,6 +10,7 @@ import { } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; +import { hasScope, scopes } from '@ghostfolio/common/scopes'; import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table'; import { GfFabComponent } from '@ghostfolio/ui/fab'; import { DataService } from '@ghostfolio/ui/services'; @@ -330,12 +331,12 @@ export class GfActivitiesPageComponent implements OnInit { this.user = aUser; this.hasPermissionToCreateActivity = - !this.hasImpersonationId && hasPermission(this.user.permissions, permissions.createActivity) && + hasScope(this.user.scopes, scopes.activityCreate) && !this.user.settings?.isRestrictedView; this.hasPermissionToDeleteActivity = - !this.hasImpersonationId && - hasPermission(this.user.permissions, permissions.deleteActivity); + hasPermission(this.user.permissions, permissions.deleteActivity) && + hasScope(this.user.scopes, scopes.activityDelete); } } 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 c69437742..a6de858bf 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.html +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -15,9 +15,7 @@ [pageIndex]="pageIndex" [pageSize]="pageSize" [showActions]=" - !hasImpersonationId && - hasPermissionToDeleteActivity && - !user.settings.isRestrictedView + hasPermissionToDeleteActivity && !user.settings.isRestrictedView " [sortColumn]="sortColumn" [sortDirection]="sortDirection" @@ -36,11 +34,7 @@ - @if ( - !hasImpersonationId && - hasPermissionToCreateActivity && - !user.settings.isRestrictedView - ) { + @if (hasPermissionToCreateActivity && !user.settings.isRestrictedView) {