Browse Source

Feature/add support to remove received access (#7701)

* Add support to remove a received access on the access page

* Update changelog
pull/7690/head
Thomas Kaul 5 days ago
committed by GitHub
parent
commit
5b1945079e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 9
      apps/api/src/app/access/access.controller.ts
  3. 10
      apps/client/src/app/components/access-table/access-table.component.html
  4. 5
      apps/client/src/app/components/access-table/access-table.component.ts
  5. 50
      apps/client/src/app/components/user-account-access/user-account-access.component.ts
  6. 9
      apps/client/src/app/components/user-account-access/user-account-access.html
  7. 3
      libs/ui/src/lib/services/data.service.ts

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added support to remove a received access on the access page
- Extended the holdings table by the activities count in the _Copy AI prompt to clipboard for analysis_ action on the analysis page (experimental) - Extended the holdings table by the activities count in the _Copy AI prompt to clipboard for analysis_ action on the analysis page (experimental)
- Extended the holdings table by the activities count in the _Copy portfolio data to clipboard for AI prompt_ action on the analysis page (experimental) - Extended the holdings table by the activities count in the _Copy portfolio data to clipboard for AI prompt_ action on the analysis page (experimental)
- Extended the holdings table by the date of first activity in the _Copy AI prompt to clipboard for analysis_ action on the analysis page (experimental) - Extended the holdings table by the date of first activity in the _Copy AI prompt to clipboard for analysis_ action on the analysis page (experimental)

9
apps/api/src/app/access/access.controller.ts

@ -114,10 +114,13 @@ export class AccessController {
@Delete(':id') @Delete(':id')
@HasPermission(permissions.deleteAccess) @HasPermission(permissions.deleteAccess)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async deleteAccess(@Param('id') id: string): Promise<AccessModel> { public async deleteAccess(@Param('id') id: string): Promise<void> {
const originalAccess = await this.accessService.access({ const originalAccess = await this.accessService.access({
id, id,
userId: this.request.user.id OR: [
{ granteeUserId: this.request.user.id },
{ userId: this.request.user.id }
]
}); });
if (!originalAccess) { if (!originalAccess) {
@ -127,7 +130,7 @@ export class AccessController {
); );
} }
return this.accessService.deleteAccess({ await this.accessService.deleteAccess({
id id
}); });
} }

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

@ -61,7 +61,9 @@
class="no-max-width" class="no-max-width"
xPosition="before" xPosition="before"
> >
@if (user()?.settings?.isExperimentalFeatures) { @if (
!isReceivedAccess() && user()?.settings?.isExperimentalFeatures
) {
<button mat-menu-item (click)="onUpdateAccess(element.id)"> <button mat-menu-item (click)="onUpdateAccess(element.id)">
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" /> <ion-icon class="mr-2" name="create-outline" />
@ -78,7 +80,7 @@
</button> </button>
} }
@if ( @if (
user()?.settings?.isExperimentalFeatures || (!isReceivedAccess() && user()?.settings?.isExperimentalFeatures) ||
element.type === 'PUBLIC' element.type === 'PUBLIC'
) { ) {
<hr class="my-0" /> <hr class="my-0" />
@ -86,7 +88,11 @@
<button mat-menu-item (click)="onDeleteAccess(element.id)"> <button mat-menu-item (click)="onDeleteAccess(element.id)">
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<ion-icon class="mr-2" name="remove-circle-outline" /> <ion-icon class="mr-2" name="remove-circle-outline" />
@if (isReceivedAccess()) {
<span i18n>Remove</span>
} @else {
<span i18n>Revoke</span> <span i18n>Revoke</span>
}
</span> </span>
</button> </button>
</mat-menu> </mat-menu>

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

@ -52,6 +52,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
}) })
export class GfAccessTableComponent { export class GfAccessTableComponent {
public readonly accesses = input.required<Access[]>(); public readonly accesses = input.required<Access[]>();
public readonly isReceivedAccess = input<boolean>(false);
public readonly showActions = input<boolean>(false); public readonly showActions = input<boolean>(false);
public readonly user = input.required<User>(); public readonly user = input.required<User>();
@ -119,7 +120,9 @@ export class GfAccessTableComponent {
this.accessDeleted.emit(aId); this.accessDeleted.emit(aId);
}, },
confirmType: ConfirmationDialogType.Warn, confirmType: ConfirmationDialogType.Warn,
title: $localize`Do you really want to revoke this granted access?` title: this.isReceivedAccess()
? $localize`Do you really want to remove this received access?`
: $localize`Do you really want to revoke this granted access?`
}); });
} }

50
apps/client/src/app/components/user-account-access/user-account-access.component.ts

@ -1,4 +1,5 @@
import { GfAccessTableComponent } from '@ghostfolio/client/components/access-table/access-table.component'; import { GfAccessTableComponent } from '@ghostfolio/client/components/access-table/access-table.component';
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 { CreateAccessDto } from '@ghostfolio/common/dtos'; import { CreateAccessDto } from '@ghostfolio/common/dtos';
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
@ -36,7 +37,7 @@ import { addIcons } from 'ionicons';
import { addOutline, eyeOffOutline, eyeOutline } from 'ionicons/icons'; import { addOutline, eyeOffOutline, eyeOutline } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { EMPTY } from 'rxjs'; import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError, switchMap } from 'rxjs/operators';
import { GfCreateOrUpdateAccessDialogComponent } from './create-or-update-access-dialog/create-or-update-access-dialog.component'; import { GfCreateOrUpdateAccessDialogComponent } from './create-or-update-access-dialog/create-or-update-access-dialog.component';
import { CreateOrUpdateAccessDialogParams } from './create-or-update-access-dialog/interfaces/interfaces'; import { CreateOrUpdateAccessDialogParams } from './create-or-update-access-dialog/interfaces/interfaces';
@ -63,6 +64,7 @@ import { CreateOrUpdateAccessDialogParams } from './create-or-update-access-dial
export class GfUserAccountAccessComponent implements OnInit { export class GfUserAccountAccessComponent implements OnInit {
protected accessesGet: Access[]; protected accessesGet: Access[];
protected accessesGive: Access[]; protected accessesGive: Access[];
protected hasImpersonationId: boolean;
protected hasPermissionToCreateAccess: boolean; protected hasPermissionToCreateAccess: boolean;
protected hasPermissionToDeleteAccess: boolean; protected hasPermissionToDeleteAccess: boolean;
protected hasPermissionToUpdateOwnAccessToken: boolean; protected hasPermissionToUpdateOwnAccessToken: boolean;
@ -84,6 +86,9 @@ export class GfUserAccountAccessComponent 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 notificationService = inject(NotificationService); private readonly notificationService = inject(NotificationService);
private readonly route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router); private readonly router = inject(Router);
@ -97,6 +102,15 @@ export class GfUserAccountAccessComponent implements OnInit {
permissions.deleteAccess permissions.deleteAccess
); );
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) => {
@ -142,11 +156,39 @@ export class GfUserAccountAccessComponent implements OnInit {
protected onDeleteAccess(aId: string) { protected onDeleteAccess(aId: string) {
this.dataService this.dataService
.deleteAccess(aId) .deleteAccess(aId)
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(
.subscribe({ catchError(() => {
next: () => { this.notificationService.alert({
title: $localize`Oops! Could not revoke the granted access.`
});
return EMPTY;
}),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => {
this.update(); this.update();
});
} }
protected onDeleteReceivedAccess(aId: string) {
this.dataService
.deleteAccess(aId)
.pipe(
switchMap(() => {
return this.userService.get(true);
}),
catchError(() => {
this.notificationService.alert({
title: $localize`Oops! Could not remove the received access.`
});
return EMPTY;
}),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => {
this.update();
}); });
} }

9
apps/client/src/app/components/user-account-access/user-account-access.html

@ -53,7 +53,14 @@
<div class="container"> <div class="container">
@if (accessesGet.length > 0) { @if (accessesGet.length > 0) {
<h1 class="h3 mb-3 text-center" i18n>Received Access</h1> <h1 class="h3 mb-3 text-center" i18n>Received Access</h1>
<gf-access-table class="mb-5" [accesses]="accessesGet" [user]="user" /> <gf-access-table
class="mb-5"
[accesses]="accessesGet"
[isReceivedAccess]="true"
[showActions]="hasPermissionToDeleteAccess && !hasImpersonationId"
[user]="user"
(accessDeleted)="onDeleteReceivedAccess($event)"
/>
} }
<h1 class="align-items-center d-flex h3 justify-content-center mb-3"> <h1 class="align-items-center d-flex h3 justify-content-center mb-3">
<span i18n>Granted Access</span> <span i18n>Granted Access</span>

3
libs/ui/src/lib/services/data.service.ts

@ -70,7 +70,6 @@ import { inject, Service } from '@angular/core';
import { SortDirection } from '@angular/material/sort'; import { SortDirection } from '@angular/material/sort';
import { utc } from '@date-fns/utc'; import { utc } from '@date-fns/utc';
import { import {
Access as AccessModel,
Account, Account,
AccountBalance, AccountBalance,
DataSource, DataSource,
@ -316,7 +315,7 @@ export class DataService {
} }
public deleteAccess(aId: string) { public deleteAccess(aId: string) {
return this.http.delete<AccessModel>(`/api/v1/access/${aId}`); return this.http.delete<void>(`/api/v1/access/${aId}`);
} }
public deleteAccount(aId: string) { public deleteAccount(aId: string) {

Loading…
Cancel
Save