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 3189b6afc..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,17 +18,16 @@ Permission
- @if (hasScopeToReadValues(element)) { + @if (hasScopesToWrite(element)) { + + View and manage + } @else if (hasScopeToReadValues(element)) { View } @else { 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 21dcc1a12..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 @@ -2,7 +2,7 @@ import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { Access, User } from '@ghostfolio/common/interfaces'; import { publicRoutes } from '@ghostfolio/common/routes/routes'; import { - SCOPES_OF_WRITE_ACCESS, + hasAnyScopeOfWriteAccess, hasScope, scopes } from '@ghostfolio/common/scopes'; @@ -110,9 +110,7 @@ export class GfAccessTableComponent { } protected hasScopesToWrite({ scopes: scopesOfAccess }: Access) { - return SCOPES_OF_WRITE_ACCESS.some((scope) => { - return hasScope(scopesOfAccess, scope); - }); + return hasAnyScopeOfWriteAccess(scopesOfAccess); } protected onCopyUrlToClipboard(aId: string) { 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-watchlist/home-watchlist.component.ts b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts index 3e0d48297..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 @@ -136,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/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 62f287610..ca675ba7a 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 @@ -6,6 +6,7 @@ import { SCOPES_OF_READ_RESTRICTED_ACCESS, SCOPES_OF_WRITE_ACCESS, Scope, + hasAnyScopeOfWriteAccess, hasScope, scopes } from '@ghostfolio/common/scopes'; @@ -186,7 +187,21 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { } private buildScopes(): Scope[] { - switch (this.accessForm.get('accessLevel')?.value as AccessLevel) { + 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': @@ -236,11 +251,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { } private getAccessLevel(scopesOfAccess: string[] | undefined): AccessLevel { - if ( - SCOPES_OF_WRITE_ACCESS.some((scope) => { - return hasScope(scopesOfAccess, scope); - }) - ) { + if (hasAnyScopeOfWriteAccess(scopesOfAccess)) { return 'CREATE_READ_UPDATE_DELETE'; } 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 848239299..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 @@ -112,7 +112,7 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit { if ( !account || !hasPermission(user?.permissions, permissions.updateAccount) || - this.isReadOnlyMode(user, scopes.accountUpdate) + this.isWriteRestricted(user, scopes.accountUpdate) ) { this.navigateBack(); @@ -141,7 +141,7 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit { if ( !hasPermission(user?.permissions, permissions.createAccount) || - this.isReadOnlyMode(user, scopes.accountCreate) + this.isWriteRestricted(user, scopes.accountCreate) ) { this.navigateBack(); @@ -182,10 +182,9 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit { this.dialogRef?.close(); } - private isReadOnlyMode(user: User, requiredScope: Scope) { + private isWriteRestricted(user: User, requiredScope: Scope) { return ( - (!!this.impersonationStorageService.getId() && - !hasScope(user?.scopes, requiredScope)) || + !hasScope(user?.scopes, requiredScope) || !!user?.settings?.isRestrictedView ); } @@ -222,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 039c3f245..41072948a 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -1,3 +1,4 @@ +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'; @@ -48,6 +49,7 @@ export class GfAccountsPageComponent implements OnInit { protected totalValueInBaseCurrency = 0; protected user: User; + private hasImpersonationId: boolean; private isInitialFetch = true; private readonly deviceType = computed( @@ -59,6 +61,9 @@ 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); @@ -75,6 +80,13 @@ export class GfAccountsPageComponent implements OnInit { } public ngOnInit() { + this.impersonationStorageService + .onChangeHasImpersonation() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((impersonationId) => { + this.hasImpersonationId = !!impersonationId; + }); + this.userService.stateChanged .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((state) => { @@ -137,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/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts index 657600b70..fb2c750ea 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 @@ -77,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; 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 e8cd8ca72..e1c213183 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,11 @@ [dataSource]="dataSource" [deviceType]="deviceType" [hasActivities]="user?.activitiesCount > 0" - [hasPermissionToCreateActivity]=" - hasPermissionToCreateActivity && !hasImpersonationId - " + [hasPermissionToCreateActivity]="hasPermissionToCreateActivity" [hasPermissionToDeleteActivity]="hasPermissionToDeleteActivity" [hasPermissionToExportActivities]="!hasImpersonationId" [hasPermissionToFilterByType]="user?.settings?.isExperimentalFeatures" - [hasPermissionToImportActivities]=" - hasPermissionToCreateActivity && !hasImpersonationId - " + [hasPermissionToImportActivities]="hasPermissionToImportActivities" [hasPermissionToUpdateActivity]="hasPermissionToUpdateActivity" [locale]="user?.settings?.locale" [pageIndex]="pageIndex" @@ -43,7 +39,7 @@ - @if (hasPermissionToCreateActivity && !user.settings.isRestrictedView) { + @if (hasPermissionToCreateActivity) { - @if (hasPermissionToExportActivities) { + @if (hasPermissionToImportActivities) { + + } + @if (hasPermissionToExportActivities || hasPermissionToDeleteActivity) { + @if (hasPermissionToImportActivities) { + + } @if (hasPermissionToExportActivities) { } -
- + @if (hasPermissionToDeleteActivity) { + @if ( + hasPermissionToExportActivities || hasPermissionToImportActivities + ) { +
+ } + + } } @@ -504,15 +514,17 @@ } - - - - Clone... - - + @if (hasPermissionToCreateActivity) { + + + + Clone... + + + }