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 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 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)

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

@ -114,10 +114,13 @@ export class AccessController {
@Delete(':id')
@HasPermission(permissions.deleteAccess)
@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({
id,
userId: this.request.user.id
OR: [
{ granteeUserId: this.request.user.id },
{ userId: this.request.user.id }
]
});
if (!originalAccess) {
@ -127,7 +130,7 @@ export class AccessController {
);
}
return this.accessService.deleteAccess({
await this.accessService.deleteAccess({
id
});
}

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

@ -61,7 +61,9 @@
class="no-max-width"
xPosition="before"
>
@if (user()?.settings?.isExperimentalFeatures) {
@if (
!isReceivedAccess() && user()?.settings?.isExperimentalFeatures
) {
<button mat-menu-item (click)="onUpdateAccess(element.id)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" />
@ -78,7 +80,7 @@
</button>
}
@if (
user()?.settings?.isExperimentalFeatures ||
(!isReceivedAccess() && user()?.settings?.isExperimentalFeatures) ||
element.type === 'PUBLIC'
) {
<hr class="my-0" />
@ -86,7 +88,11 @@
<button mat-menu-item (click)="onDeleteAccess(element.id)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="remove-circle-outline" />
@if (isReceivedAccess()) {
<span i18n>Remove</span>
} @else {
<span i18n>Revoke</span>
}
</span>
</button>
</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 {
public readonly accesses = input.required<Access[]>();
public readonly isReceivedAccess = input<boolean>(false);
public readonly showActions = input<boolean>(false);
public readonly user = input.required<User>();
@ -119,7 +120,9 @@ export class GfAccessTableComponent {
this.accessDeleted.emit(aId);
},
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 { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { CreateAccessDto } from '@ghostfolio/common/dtos';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
@ -36,7 +37,7 @@ import { addIcons } from 'ionicons';
import { addOutline, eyeOffOutline, eyeOutline } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
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 { 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 {
protected accessesGet: Access[];
protected accessesGive: Access[];
protected hasImpersonationId: boolean;
protected hasPermissionToCreateAccess: boolean;
protected hasPermissionToDeleteAccess: boolean;
protected hasPermissionToUpdateOwnAccessToken: boolean;
@ -84,6 +86,9 @@ export class GfUserAccountAccessComponent 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);
@ -97,6 +102,15 @@ export class GfUserAccountAccessComponent implements OnInit {
permissions.deleteAccess
);
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
this.changeDetectorRef.markForCheck();
});
this.userService.stateChanged
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => {
@ -142,11 +156,39 @@ export class GfUserAccountAccessComponent implements OnInit {
protected onDeleteAccess(aId: string) {
this.dataService
.deleteAccess(aId)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: () => {
.pipe(
catchError(() => {
this.notificationService.alert({
title: $localize`Oops! Could not revoke the granted access.`
});
return EMPTY;
}),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => {
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">
@if (accessesGet.length > 0) {
<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">
<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 { utc } from '@date-fns/utc';
import {
Access as AccessModel,
Account,
AccountBalance,
DataSource,
@ -316,7 +315,7 @@ export class DataService {
}
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) {

Loading…
Cancel
Save