Browse Source

Add write access

pull/7676/head
Thomas Kaul 1 week ago
parent
commit
84be07f8db
  1. 4
      apps/client/src/app/components/access-table/access-table.component.html
  2. 4
      apps/client/src/app/components/access-table/access-table.component.ts
  3. 22
      apps/client/src/app/components/home-holdings/home-holdings.component.ts
  4. 6
      apps/client/src/app/components/home-holdings/home-holdings.html
  5. 18
      apps/client/src/app/components/home-overview/home-overview.component.ts
  6. 6
      apps/client/src/app/components/home-overview/home-overview.html
  7. 19
      apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
  8. 2
      apps/client/src/app/components/home-watchlist/home-watchlist.html
  9. 19
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  10. 17
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html
  11. 16
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  12. 10
      apps/client/src/app/pages/accounts/accounts-page.html
  13. 7
      apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
  14. 10
      apps/client/src/app/pages/portfolio/activities/activities-page.html
  15. 15
      libs/common/src/lib/scopes.spec.ts
  16. 5
      libs/common/src/lib/scopes.ts

4
apps/client/src/app/components/access-table/access-table.component.html

@ -25,6 +25,10 @@
<ion-icon class="mr-1" name="lock-closed-outline" /> <ion-icon class="mr-1" name="lock-closed-outline" />
<ng-container i18n>Restricted view</ng-container> <ng-container i18n>Restricted view</ng-container>
} }
@if (hasScopesToWrite(element)) {
<ion-icon class="ml-2 mr-1" name="create-outline" />
<ng-container i18n>Changes</ng-container>
}
</div> </div>
</td> </td>
</ng-container> </ng-container>

4
apps/client/src/app/components/access-table/access-table.component.ts

@ -105,6 +105,10 @@ export class GfAccessTableComponent {
return hasScope(scopesOfAccess, scopes.portfolioReadValues); return hasScope(scopesOfAccess, scopes.portfolioReadValues);
} }
protected hasScopesToWrite({ scopes: scopesOfAccess }: Access) {
return hasScope(scopesOfAccess, scopes.activityCreate);
}
protected onCopyUrlToClipboard(aId: string) { protected onCopyUrlToClipboard(aId: string) {
this.clipboard.copy(this.getPublicUrl(aId)); this.clipboard.copy(this.getPublicUrl(aId));

22
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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
@ -7,6 +6,7 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { hasScope, scopes } from '@ghostfolio/common/scopes';
import { import {
HoldingType, HoldingType,
HoldingsViewMode, HoldingsViewMode,
@ -51,7 +51,6 @@ export class GfHomeHoldingsComponent implements OnInit {
public static DEFAULT_HOLDINGS_VIEW_MODE: HoldingsViewMode = 'TABLE'; public static DEFAULT_HOLDINGS_VIEW_MODE: HoldingsViewMode = 'TABLE';
protected deviceType: string; protected deviceType: string;
protected hasImpersonationId: boolean;
protected hasPermissionToAccessHoldingsChart: boolean; protected hasPermissionToAccessHoldingsChart: boolean;
protected hasPermissionToCreateActivity: boolean; protected hasPermissionToCreateActivity: boolean;
protected holdings: PortfolioPosition[] | undefined; protected holdings: PortfolioPosition[] | undefined;
@ -83,9 +82,6 @@ export class GfHomeHoldingsComponent implements OnInit {
private readonly dataService = inject(DataService); private readonly dataService = inject(DataService);
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService); private readonly deviceDetectorService = inject(DeviceDetectorService);
private readonly impersonationStorageService = inject(
ImpersonationStorageService
);
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly userService = inject(UserService); private readonly userService = inject(UserService);
@ -96,15 +92,6 @@ export class GfHomeHoldingsComponent implements OnInit {
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType;
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
this.changeDetectorRef.markForCheck();
});
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
@ -116,10 +103,9 @@ export class GfHomeHoldingsComponent implements OnInit {
permissions.accessHoldingsChart permissions.accessHoldingsChart
); );
this.hasPermissionToCreateActivity = hasPermission( this.hasPermissionToCreateActivity =
this.user.permissions, hasPermission(this.user.permissions, permissions.createActivity) &&
permissions.createActivity hasScope(this.user.scopes, scopes.activityCreate);
);
this.initialize(); this.initialize();
} }

6
apps/client/src/app/components/home-holdings/home-holdings.html

@ -41,11 +41,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
(holdingClicked)="onHoldingClicked($event)" (holdingClicked)="onHoldingClicked($event)"
/> />
@if ( @if (hasPermissionToCreateActivity && holdings?.length > 0) {
!hasImpersonationId &&
hasPermissionToCreateActivity &&
holdings?.length > 0
) {
<div class="text-center"> <div class="text-center">
<a <a
class="mt-3" class="mt-3"

18
apps/client/src/app/components/home-overview/home-overview.component.ts

@ -1,6 +1,5 @@
import { GfPortfolioPerformanceComponent } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.component'; import { GfPortfolioPerformanceComponent } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.component';
import { LayoutService } from '@ghostfolio/client/core/layout.service'; 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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
@ -15,6 +14,7 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { hasScope, scopes } from '@ghostfolio/common/scopes';
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
@ -46,7 +46,6 @@ import { DeviceDetectorService } from 'ngx-device-detector';
}) })
export class GfHomeOverviewComponent implements OnInit { export class GfHomeOverviewComponent implements OnInit {
protected readonly errors = signal<AssetProfileIdentifier[]>([]); protected readonly errors = signal<AssetProfileIdentifier[]>([]);
protected readonly hasImpersonationId = signal(false);
protected readonly historicalDataItems = signal<LineChartItem[] | null>(null); protected readonly historicalDataItems = signal<LineChartItem[] | null>(null);
protected readonly isLoadingPerformance = signal(true); protected readonly isLoadingPerformance = signal(true);
protected readonly performance = signal<PortfolioPerformance | null>(null); protected readonly performance = signal<PortfolioPerformance | null>(null);
@ -66,7 +65,10 @@ export class GfHomeOverviewComponent implements OnInit {
); );
protected readonly hasPermissionToCreateActivity = computed(() => { 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(() => { protected readonly showDetails = computed(() => {
@ -86,9 +88,6 @@ export class GfHomeOverviewComponent implements OnInit {
private readonly dataService = inject(DataService); private readonly dataService = inject(DataService);
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService); private readonly deviceDetectorService = inject(DeviceDetectorService);
private readonly impersonationStorageService = inject(
ImpersonationStorageService
);
private readonly layoutService = inject(LayoutService); private readonly layoutService = inject(LayoutService);
private readonly userService = inject(UserService); private readonly userService = inject(UserService);
@ -104,13 +103,6 @@ export class GfHomeOverviewComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => {
this.hasImpersonationId.set(!!impersonationId);
});
this.layoutService.shouldReloadContent$ this.layoutService.shouldReloadContent$
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {

6
apps/client/src/app/components/home-overview/home-overview.html

@ -1,11 +1,7 @@
<div <div
class="align-items-center container d-flex flex-column h-100 justify-content-center overview p-0 position-relative" class="align-items-center container d-flex flex-column h-100 justify-content-center overview p-0 position-relative"
> >
@if ( @if (hasPermissionToCreateActivity() && user()?.activitiesCount === 0) {
!hasImpersonationId() &&
hasPermissionToCreateActivity() &&
user()?.activitiesCount === 0
) {
<div class="justify-content-center row w-100"> <div class="justify-content-center row w-100">
<div class="col introduction"> <div class="col introduction">
<h4 i18n>Welcome to Ghostfolio</h4> <h4 i18n>Welcome to Ghostfolio</h4>

19
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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_LOCALE } from '@ghostfolio/common/config'; import { DEFAULT_LOCALE } from '@ghostfolio/common/config';
import { import {
@ -7,6 +6,7 @@ import {
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { hasScope, scopes } from '@ghostfolio/common/scopes';
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark'; import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
import { GfFabComponent } from '@ghostfolio/ui/fab'; import { GfFabComponent } from '@ghostfolio/ui/fab';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
@ -44,7 +44,6 @@ import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/
templateUrl: './home-watchlist.html' templateUrl: './home-watchlist.html'
}) })
export class GfHomeWatchlistComponent implements OnInit { export class GfHomeWatchlistComponent implements OnInit {
protected hasImpersonationId: boolean;
protected hasPermissionToCreateWatchlistItem: boolean; protected hasPermissionToCreateWatchlistItem: boolean;
protected hasPermissionToDeleteWatchlistItem: boolean; protected hasPermissionToDeleteWatchlistItem: boolean;
protected user: User; protected user: User;
@ -59,21 +58,11 @@ export class GfHomeWatchlistComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService); private readonly deviceDetectorService = inject(DeviceDetectorService);
private readonly dialog = inject(MatDialog); private readonly dialog = inject(MatDialog);
private readonly impersonationStorageService = inject(
ImpersonationStorageService
);
private readonly route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly userService = inject(UserService); private readonly userService = inject(UserService);
public constructor() { public constructor() {
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
});
this.route.queryParams this.route.queryParams
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((params) => { .subscribe((params) => {
@ -89,17 +78,15 @@ export class GfHomeWatchlistComponent implements OnInit {
this.user = state.user; this.user = state.user;
this.hasPermissionToCreateWatchlistItem = this.hasPermissionToCreateWatchlistItem =
!this.hasImpersonationId &&
hasPermission( hasPermission(
this.user.permissions, this.user.permissions,
permissions.createWatchlistItem permissions.createWatchlistItem
); ) && hasScope(this.user.scopes, scopes.watchlistCreate);
this.hasPermissionToDeleteWatchlistItem = this.hasPermissionToDeleteWatchlistItem =
!this.hasImpersonationId &&
hasPermission( hasPermission(
this.user.permissions, this.user.permissions,
permissions.deleteWatchlistItem permissions.deleteWatchlistItem
); ) && hasScope(this.user.scopes, scopes.watchlistDelete);
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }

2
apps/client/src/app/components/home-watchlist/home-watchlist.html

@ -21,6 +21,6 @@
</div> </div>
</div> </div>
</div> </div>
@if (!hasImpersonationId && hasPermissionToCreateWatchlistItem) { @if (hasPermissionToCreateWatchlistItem) {
<gf-fab [queryParams]="{ createWatchlistItemDialog: true }" /> <gf-fab [queryParams]="{ createWatchlistItemDialog: true }" />
} }

19
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 { import {
SCOPES_OF_READ_ACCESS, SCOPES_OF_READ_ACCESS,
SCOPES_OF_READ_RESTRICTED_ACCESS, SCOPES_OF_READ_RESTRICTED_ACCESS,
SCOPES_OF_WRITE_ACCESS,
hasScope, hasScope,
scopes scopes
} from '@ghostfolio/common/scopes'; } from '@ghostfolio/common/scopes';
@ -46,6 +47,7 @@ import {
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import { EMPTY, catchError } from 'rxjs'; import { EMPTY, catchError } from 'rxjs';
@ -62,6 +64,7 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
MatSelectModule, MatSelectModule,
MatSlideToggleModule,
ReactiveFormsModule ReactiveFormsModule
], ],
selector: 'gf-create-or-update-access-dialog', selector: 'gf-create-or-update-access-dialog',
@ -120,6 +123,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
access?.scopes, access?.scopes,
scopes.portfolioReadValues scopes.portfolioReadValues
), ),
hasScopesToWrite: hasScope(access?.scopes, scopes.activityCreate),
type: [ type: [
{ disabled: this.mode === 'update', value: access?.type ?? 'PRIVATE' }, { disabled: this.mode === 'update', value: access?.type ?? 'PRIVATE' },
Validators.required Validators.required
@ -149,6 +153,8 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
'hasScopeToReadValues' 'hasScopeToReadValues'
); );
const hasScopesToWriteControl = this.accessForm.get('hasScopesToWrite');
if (accessType === 'PRIVATE') { if (accessType === 'PRIVATE') {
granteeUserIdControl?.setValidators(Validators.required); granteeUserIdControl?.setValidators(Validators.required);
this.accessForm.get('filters')?.setValue(null); this.accessForm.get('filters')?.setValue(null);
@ -156,8 +162,10 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
granteeUserIdControl?.clearValidators(); granteeUserIdControl?.clearValidators();
granteeUserIdControl?.setValue(null); 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); hasScopeToReadValuesControl?.setValue(false);
hasScopesToWriteControl?.setValue(false);
} }
granteeUserIdControl?.updateValueAndValidity(); granteeUserIdControl?.updateValueAndValidity();
@ -187,14 +195,17 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
} }
/** /**
* The dialog offers the read access only. The write scopes are not granted * The write access is granted as one unit, hence the dialog offers a single
* here yet. * control for all write scopes
*/ */
private buildScopes() { private buildScopes() {
return [ return [
...(this.accessForm.get('hasScopeToReadValues')?.value ...(this.accessForm.get('hasScopeToReadValues')?.value
? SCOPES_OF_READ_ACCESS ? SCOPES_OF_READ_ACCESS
: SCOPES_OF_READ_RESTRICTED_ACCESS) : SCOPES_OF_READ_RESTRICTED_ACCESS),
...(this.accessForm.get('hasScopesToWrite')?.value
? SCOPES_OF_WRITE_ACCESS
: [])
]; ];
} }

17
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html

@ -44,6 +44,23 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@if (accessForm.get('type')?.value === 'PRIVATE') {
<div class="align-items-center d-flex mb-3">
<div class="w-50">
<div i18n>Changes</div>
<div class="hint-text text-muted" i18n>
Allow to add, to update and to delete data of your portfolio
</div>
</div>
<div class="pl-1 w-50">
<mat-slide-toggle
color="primary"
formControlName="hasScopesToWrite"
hideIcon="true"
/>
</div>
</div>
}
@if (accessForm.get('type')?.value === 'PRIVATE') { @if (accessForm.get('type')?.value === 'PRIVATE') {
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">

16
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 { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { hasScope, scopes } from '@ghostfolio/common/scopes';
import { AccountWithValue } from '@ghostfolio/common/types'; import { AccountWithValue } from '@ghostfolio/common/types';
import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table'; import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table';
import { GfFabComponent } from '@ghostfolio/ui/fab'; import { GfFabComponent } from '@ghostfolio/ui/fab';
@ -95,14 +96,13 @@ export class GfAccountsPageComponent implements OnInit {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
this.hasPermissionToCreateAccount = hasPermission( this.hasPermissionToCreateAccount =
this.user.permissions, hasPermission(this.user.permissions, permissions.createAccount) &&
permissions.createAccount hasScope(this.user.scopes, scopes.accountCreate);
);
this.hasPermissionToUpdateAccount = hasPermission( this.hasPermissionToUpdateAccount =
this.user.permissions, hasPermission(this.user.permissions, permissions.updateAccount) &&
permissions.updateAccount hasScope(this.user.scopes, scopes.accountUpdate);
);
this.fetchAccounts(); this.fetchAccounts();
} }

10
apps/client/src/app/pages/accounts/accounts-page.html

@ -8,9 +8,7 @@
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[showActions]=" [showActions]="
!hasImpersonationId && hasPermissionToUpdateAccount && !user.settings.isRestrictedView
hasPermissionToUpdateAccount &&
!user.settings.isRestrictedView
" "
[totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency" [totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency"
[totalValueInBaseCurrency]="totalValueInBaseCurrency" [totalValueInBaseCurrency]="totalValueInBaseCurrency"
@ -20,11 +18,7 @@
</div> </div>
</div> </div>
@if ( @if (hasPermissionToCreateAccount && !user.settings.isRestrictedView) {
!hasImpersonationId &&
hasPermissionToCreateAccount &&
!user.settings.isRestrictedView
) {
<gf-fab <gf-fab
[routerLink]="internalRoutes.accounts.subRoutes.create.routerLink" [routerLink]="internalRoutes.accounts.subRoutes.create.routerLink"
/> />

7
apps/client/src/app/pages/portfolio/activities/activities-page.component.ts

@ -10,6 +10,7 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { hasScope, scopes } from '@ghostfolio/common/scopes';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table'; import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
import { GfFabComponent } from '@ghostfolio/ui/fab'; import { GfFabComponent } from '@ghostfolio/ui/fab';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
@ -330,12 +331,12 @@ export class GfActivitiesPageComponent implements OnInit {
this.user = aUser; this.user = aUser;
this.hasPermissionToCreateActivity = this.hasPermissionToCreateActivity =
!this.hasImpersonationId &&
hasPermission(this.user.permissions, permissions.createActivity) && hasPermission(this.user.permissions, permissions.createActivity) &&
hasScope(this.user.scopes, scopes.activityCreate) &&
!this.user.settings?.isRestrictedView; !this.user.settings?.isRestrictedView;
this.hasPermissionToDeleteActivity = this.hasPermissionToDeleteActivity =
!this.hasImpersonationId && hasPermission(this.user.permissions, permissions.deleteActivity) &&
hasPermission(this.user.permissions, permissions.deleteActivity); hasScope(this.user.scopes, scopes.activityDelete);
} }
} }

10
apps/client/src/app/pages/portfolio/activities/activities-page.html

@ -15,9 +15,7 @@
[pageIndex]="pageIndex" [pageIndex]="pageIndex"
[pageSize]="pageSize" [pageSize]="pageSize"
[showActions]=" [showActions]="
!hasImpersonationId && hasPermissionToDeleteActivity && !user.settings.isRestrictedView
hasPermissionToDeleteActivity &&
!user.settings.isRestrictedView
" "
[sortColumn]="sortColumn" [sortColumn]="sortColumn"
[sortDirection]="sortDirection" [sortDirection]="sortDirection"
@ -36,11 +34,7 @@
</div> </div>
</div> </div>
@if ( @if (hasPermissionToCreateActivity && !user.settings.isRestrictedView) {
!hasImpersonationId &&
hasPermissionToCreateActivity &&
!user.settings.isRestrictedView
) {
<gf-fab <gf-fab
[routerLink]=" [routerLink]="
internalRoutes.portfolio.subRoutes.activities.subRoutes.create internalRoutes.portfolio.subRoutes.activities.subRoutes.create

15
libs/common/src/lib/scopes.spec.ts

@ -82,18 +82,25 @@ describe('Scopes', () => {
).toEqual([]); ).toEqual([]);
}); });
// TODO: Remove this expectation once the dialog allows to configure the it('Gives the write scopes', () => {
// write scopes
it('Gives no write scope', () => {
const scopesOfAccess = getScopesOfAccess({ const scopesOfAccess = getScopesOfAccess({
granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d', granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d',
scopes: [...SCOPES_OF_READ_ACCESS, ...SCOPES_OF_WRITE_ACCESS] scopes: [...SCOPES_OF_READ_ACCESS, ...SCOPES_OF_WRITE_ACCESS]
}); });
for (const scope of SCOPES_OF_WRITE_ACCESS) { for (const scope of SCOPES_OF_WRITE_ACCESS) {
expect(scopesOfAccess).not.toContain(scope); expect(scopesOfAccess).toContain(scope);
} }
}); });
it('Drops an unknown scope', () => {
expect(
getScopesOfAccess({
granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d',
scopes: [scopes.portfolioRead, 'portfolio:write']
})
).toEqual([scopes.portfolioRead]);
});
}); });
describe('Get scopes of public access', () => { describe('Get scopes of public access', () => {

5
libs/common/src/lib/scopes.ts

@ -67,8 +67,9 @@ export function getScopesOfAccess({
const scopesToEvaluate = scopesOfAccess ?? []; const scopesToEvaluate = scopesOfAccess ?? [];
if (granteeUserId) { if (granteeUserId) {
// TODO: Permit the write scopes once the dialog allows to configure them // An unknown scope is dropped, so that a scope which has been removed from
return SCOPES_OF_READ_ACCESS.filter((scope) => { // the vocabulary cannot stay effective
return Object.values(scopes).filter((scope) => {
return scopesToEvaluate.includes(scope); return scopesToEvaluate.includes(scope);
}); });
} }

Loading…
Cancel
Save