From 67dd8fbf0fd6d58b8a5b215c1a3e392f7fc1e6fb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 21 Aug 2026 20:31:50 +0200 Subject: [PATCH] Task/add write access to create or update access dialog (#7676) * Add write access * Update changelog --- CHANGELOG.md | 4 + .../access-table/access-table.component.html | 5 +- .../access-table/access-table.component.ts | 10 +- .../account-detail-dialog.html | 4 +- .../interfaces/interfaces.ts | 1 + .../holding-detail-dialog.html | 4 +- .../home-holdings/home-holdings.component.ts | 22 +-- .../home-holdings/home-holdings.html | 6 +- .../home-overview/home-overview.component.ts | 6 +- .../home-watchlist.component.ts | 28 ++-- .../home-watchlist/home-watchlist.html | 2 +- ...reate-or-update-access-dialog.component.ts | 69 +++++--- .../create-or-update-access-dialog.html | 12 +- .../interfaces/interfaces.ts | 3 + .../account-dialog-host.component.ts | 13 +- .../pages/accounts/accounts-page.component.ts | 30 ++-- .../src/app/pages/accounts/accounts-page.html | 11 +- .../activities/activities-page.component.ts | 21 ++- .../portfolio/activities/activities-page.html | 13 +- .../activity-dialog-host.component.ts | 13 +- .../allocations/allocations-page.component.ts | 4 + libs/common/src/lib/scopes.spec.ts | 40 ++++- libs/common/src/lib/scopes.ts | 11 +- .../accounts-table.component.html | 46 ++--- .../accounts-table.component.ts | 2 + .../activities-table.component.html | 158 ++++++++++-------- .../activities-table.component.ts | 2 + 27 files changed, 330 insertions(+), 210 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5642465c0..8faa1260f 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 the permission to manage data to the grant access dialog and to the access table to share the portfolio (experimental) + ### Changed - Extended the account selector options to include the platform and currency 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..38122c39c 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 @@ -18,7 +18,10 @@ Permission
- @if (hasScopeToReadValues(element)) { + @if (hasScopesToWrite(element)) { + + View and manage + } @else if (hasScopeToReadValues(element)) { View } @else { 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..58a9e8034 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 @@ -1,7 +1,11 @@ import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { Access, User } from '@ghostfolio/common/interfaces'; import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { hasScope, scopes } from '@ghostfolio/common/scopes'; +import { + hasAnyScopeOfWriteAccess, + hasScope, + scopes +} from '@ghostfolio/common/scopes'; import { NotificationService } from '@ghostfolio/ui/notifications'; import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard'; @@ -105,6 +109,10 @@ export class GfAccessTableComponent { return hasScope(scopesOfAccess, scopes.portfolioReadValues); } + protected hasScopesToWrite({ scopes: scopesOfAccess }: Access) { + return hasAnyScopeOfWriteAccess(scopesOfAccess); + } + protected onCopyUrlToClipboard(aId: string) { this.clipboard.copy(this.getPublicUrl(aId)); diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html index 1ca0a27dc..a87179ecb 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html @@ -148,11 +148,13 @@ [baseCurrency]="user?.settings?.baseCurrency" [dataSource]="dataSource" [deviceType]="data.deviceType" - [hasPermissionToCreateActivity]="false" + [hasActivities]="true" + [hasPermissionToCreateActivity]="data.hasPermissionToCreateActivity" [hasPermissionToDeleteActivity]="false" [hasPermissionToExportActivities]="!showValuesInPercentage()" [hasPermissionToFilter]="false" [hasPermissionToOpenDetails]="false" + [hasPermissionToUpdateActivity]="data.hasPermissionToUpdateActivity" [locale]="user?.settings?.locale" [pageIndex]="pageIndex" [pageSize]="pageSize" diff --git a/apps/client/src/app/components/account-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/account-detail-dialog/interfaces/interfaces.ts index 0e7d04f2c..6036a503f 100644 --- a/apps/client/src/app/components/account-detail-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/account-detail-dialog/interfaces/interfaces.ts @@ -2,6 +2,7 @@ export interface AccountDetailDialogParams { accountId: string; deviceType: string; hasPermissionToCreateActivity: boolean; + hasPermissionToUpdateActivity: boolean; impersonationId: string | null; } diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html index e6884c003..35a8ed3aa 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -379,13 +379,15 @@ [baseCurrency]="data.baseCurrency" [dataSource]="dataSource" [deviceType]="data.deviceType" - [hasPermissionToCreateActivity]="false" + [hasActivities]="true" + [hasPermissionToCreateActivity]="data.hasPermissionToCreateActivity" [hasPermissionToDeleteActivity]="false" [hasPermissionToExportActivities]=" !data.impersonationId && !user?.settings?.isRestrictedView " [hasPermissionToFilter]="false" [hasPermissionToOpenDetails]="false" + [hasPermissionToUpdateActivity]="data.hasPermissionToUpdateActivity" [locale]="data.locale" [pageIndex]="pageIndex" [pageSize]="pageSize" 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) {
{ - return hasPermission(this.user()?.permissions, permissions.createActivity); + return ( + hasPermission(this.user()?.permissions, permissions.createActivity) && + hasScope(this.user()?.scopes, scopes.activityCreate) + ); }); protected readonly showDetails = computed(() => { 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..91cab29a9 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(); } @@ -149,6 +136,15 @@ export class GfHomeWatchlistComponent implements OnInit { .subscribe((user) => { this.user = user; + if ( + !hasPermission(user?.permissions, permissions.createWatchlistItem) || + !hasScope(user?.scopes, scopes.watchlistCreate) + ) { + this.router.navigate(['.'], { relativeTo: this.route }); + + return; + } + const dialogRef = this.dialog.open< GfCreateWatchlistItemDialogComponent, CreateWatchlistItemDialogParams 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..40c4fe703 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,9 @@ import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; import { SCOPES_OF_READ_ACCESS, SCOPES_OF_READ_RESTRICTED_ACCESS, + SCOPES_OF_WRITE_ACCESS, + Scope, + hasAnyScopeOfWriteAccess, hasScope, scopes } from '@ghostfolio/common/scopes'; @@ -49,7 +52,10 @@ import { MatSelectModule } from '@angular/material/select'; import { StatusCodes } from 'http-status-codes'; import { EMPTY, catchError } from 'rxjs'; -import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces'; +import { + AccessLevel, + CreateOrUpdateAccessDialogParams +} from './interfaces/interfaces'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -105,21 +111,22 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { ); } + public get canGrantWriteAccess() { + return this.hasExperimentalFeatures; + } + public ngOnInit() { const access = this.data?.access; const isPublic = access?.type === 'PUBLIC'; this.accessForm = this.formBuilder.group({ + accessLevel: this.getAccessLevel(access?.scopes), alias: [access?.alias ?? ''], filters: [null], granteeUserId: [ access?.grantee ?? null, isPublic ? null : Validators.required ], - hasScopeToReadValues: hasScope( - access?.scopes, - scopes.portfolioReadValues - ), type: [ { disabled: this.mode === 'update', value: access?.type ?? 'PRIVATE' }, Validators.required @@ -145,10 +152,6 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { .subscribe((accessType) => { const granteeUserIdControl = this.accessForm.get('granteeUserId'); - const hasScopeToReadValuesControl = this.accessForm.get( - 'hasScopeToReadValues' - ); - if (accessType === 'PRIVATE') { granteeUserIdControl?.setValidators(Validators.required); this.accessForm.get('filters')?.setValue(null); @@ -156,8 +159,9 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { granteeUserIdControl?.clearValidators(); granteeUserIdControl?.setValue(null); - // A public access never exposes the monetary values - hasScopeToReadValuesControl?.setValue(false); + // A public access never exposes the monetary values and never + // changes data + this.accessForm.get('accessLevel')?.setValue('READ_RESTRICTED'); } granteeUserIdControl?.updateValueAndValidity(); @@ -186,16 +190,29 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { ); } - /** - * The dialog offers the read access only. The write scopes are not granted - * here yet. - */ - private buildScopes() { - return [ - ...(this.accessForm.get('hasScopeToReadValues')?.value - ? SCOPES_OF_READ_ACCESS - : SCOPES_OF_READ_RESTRICTED_ACCESS) - ]; + private buildScopes(): Scope[] { + const accessLevel = this.accessForm.get('accessLevel') + ?.value as AccessLevel; + + const scopesOfAccess = this.data.access?.scopes ?? []; + + if ( + scopesOfAccess.length > 0 && + accessLevel === this.getAccessLevel(scopesOfAccess) + ) { + return Object.values(scopes).filter((scope) => { + return hasScope(scopesOfAccess, scope); + }); + } + + switch (accessLevel) { + case 'CREATE_READ_UPDATE_DELETE': + return [...SCOPES_OF_READ_ACCESS, ...SCOPES_OF_WRITE_ACCESS]; + case 'READ': + return [...SCOPES_OF_READ_ACCESS]; + default: + return [...SCOPES_OF_READ_RESTRICTED_ACCESS]; + } } private async createAccess() { @@ -237,6 +254,16 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { } } + private getAccessLevel(scopesOfAccess: string[] | undefined): AccessLevel { + if (hasAnyScopeOfWriteAccess(scopesOfAccess)) { + return 'CREATE_READ_UPDATE_DELETE'; + } + + return hasScope(scopesOfAccess, scopes.portfolioReadValues) + ? 'READ' + : 'READ_RESTRICTED'; + } + private loadHoldings() { this.dataService .fetchPortfolioHoldings() 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..545dde228 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 @@ -36,10 +36,16 @@
Permission - - Restricted view + + Restricted view @if (accessForm.get('type')?.value === 'PRIVATE') { - View + View + View and manage } diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts index 8d1ac0ba9..ca7cee25a 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts @@ -3,3 +3,6 @@ import { Access } from '@ghostfolio/common/interfaces'; export interface CreateOrUpdateAccessDialogParams { access?: Access; } + +export type AccessLevel = + 'CREATE_READ_UPDATE_DELETE' | 'READ' | 'READ_RESTRICTED'; 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..29f7eb78b 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.isWriteRestricted(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.isWriteRestricted(user, scopes.accountCreate) ) { this.navigateBack(); @@ -181,9 +182,9 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit { this.dialogRef?.close(); } - private isReadOnlyMode(user: User) { + private isWriteRestricted(user: User, requiredScope: Scope) { return ( - !!this.impersonationStorageService.getId() || + !hasScope(user?.scopes, requiredScope) || !!user?.settings?.isRestrictedView ); } @@ -220,6 +221,10 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit { hasPermissionToCreateActivity: !impersonationId && hasPermission(user?.permissions, permissions.createActivity) && + !user?.settings?.isRestrictedView, + hasPermissionToUpdateActivity: + !impersonationId && + hasPermission(user?.permissions, permissions.updateActivity) && !user?.settings?.isRestrictedView }, height: this.deviceType() === 'mobile' ? '98vh' : '80vh', 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..41072948a 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'; @@ -41,13 +42,14 @@ 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; protected user: User; + private hasImpersonationId: boolean; private isInitialFetch = true; private readonly deviceType = computed( @@ -77,16 +79,12 @@ 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.hasImpersonationId = !!impersonationId; }); this.userService.stateChanged @@ -95,14 +93,17 @@ 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.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); this.fetchAccounts(); } @@ -148,6 +149,7 @@ export class GfAccountsPageComponent implements OnInit { this.totalValueInBaseCurrency = totalValueInBaseCurrency; if ( + !this.hasImpersonationId && this.accounts?.length <= 0 && this.hasPermissionToCreateAccount && this.isInitialFetch diff --git a/apps/client/src/app/pages/accounts/accounts-page.html b/apps/client/src/app/pages/accounts/accounts-page.html index 8935f70f5..6b9adece1 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.html +++ b/apps/client/src/app/pages/accounts/accounts-page.html @@ -6,10 +6,11 @@ [accounts]="accounts" [activitiesCount]="activitiesCount" [baseCurrency]="user?.settings?.baseCurrency" + [hasPermissionToDeleteAccount]="hasPermissionToDeleteAccount" + [hasPermissionToUpdateAccount]="hasPermissionToUpdateAccount" [locale]="user?.settings?.locale" [showActions]=" - !hasImpersonationId && - hasPermissionToUpdateAccount && + (hasPermissionToDeleteAccount || hasPermissionToUpdateAccount) && !user.settings.isRestrictedView " [totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency" @@ -20,11 +21,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..dbeedff32 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'; @@ -53,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; @@ -75,6 +77,12 @@ export class GfActivitiesPageComponent implements OnInit { private readonly router = inject(Router); private readonly userService = inject(UserService); + protected get hasPermissionToImportActivities() { + // An import always writes to the own portfolio, hence it is not available + // while the user impersonates a different user + return this.hasPermissionToCreateActivity && !this.hasImpersonationId; + } + public ngOnInit() { this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; @@ -301,6 +309,7 @@ export class GfActivitiesPageComponent implements OnInit { this.totalItems = count; if ( + !this.hasImpersonationId && this.hasPermissionToCreateActivity && this.user?.activitiesCount === 0 ) { @@ -330,12 +339,18 @@ 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) && + !this.user.settings?.isRestrictedView; + + this.hasPermissionToUpdateActivity = + hasPermission(this.user.permissions, permissions.updateActivity) && + hasScope(this.user.scopes, scopes.activityUpdate) && + !this.user.settings?.isRestrictedView; } } 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..e1c213183 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.html +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -11,12 +11,15 @@ [hasPermissionToDeleteActivity]="hasPermissionToDeleteActivity" [hasPermissionToExportActivities]="!hasImpersonationId" [hasPermissionToFilterByType]="user?.settings?.isExperimentalFeatures" + [hasPermissionToImportActivities]="hasPermissionToImportActivities" + [hasPermissionToUpdateActivity]="hasPermissionToUpdateActivity" [locale]="user?.settings?.locale" [pageIndex]="pageIndex" [pageSize]="pageSize" [showActions]=" - !hasImpersonationId && - hasPermissionToDeleteActivity && + (hasPermissionToCreateActivity || + hasPermissionToDeleteActivity || + hasPermissionToUpdateActivity) && !user.settings.isRestrictedView " [sortColumn]="sortColumn" @@ -36,11 +39,7 @@ - @if ( - !hasImpersonationId && - hasPermissionToCreateActivity && - !user.settings.isRestrictedView - ) { + @if (hasPermissionToCreateActivity) {
+ @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..20c6e34b7 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -19,17 +19,19 @@ } - @if (hasPermissionToCreateActivity) { + @if (hasPermissionToImportActivities || hasPermissionToDeleteActivity) {
- - @if (hasPermissionToExportActivities) { + @if (hasPermissionToImportActivities) { + + } + @if (hasPermissionToExportActivities || hasPermissionToDeleteActivity) { + @if (hasPermissionToImportActivities) { + + } @if (hasPermissionToExportActivities) { } -
- + @if (hasPermissionToDeleteActivity) { + @if ( + hasPermissionToExportActivities || hasPermissionToImportActivities + ) { +
+ } + + }
} @@ -397,7 +407,7 @@ @if ( - !hasPermissionToCreateActivity && hasPermissionToExportActivities + !hasPermissionToImportActivities && hasPermissionToExportActivities ) { } - @if (hasPermissionToCreateActivity) { + @if (hasPermissionToImportActivities) { } - - - - Edit... - - - - - - Clone... - - - -
+ @if (hasPermissionToUpdateActivity) { + + + + Edit... + + + } + @if (hasPermissionToCreateActivity) { + + + + Clone... + + + } + @if (hasPermissionToExportActivities) { + +
+ }