diff --git a/CHANGELOG.md b/CHANGELOG.md index 23346e159..f3ef271cc 100644 --- a/CHANGELOG.md +++ b/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) diff --git a/apps/api/src/app/access/access.controller.ts b/apps/api/src/app/access/access.controller.ts index 0b8ae15ea..b8772e3bb 100644 --- a/apps/api/src/app/access/access.controller.ts +++ b/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 { + public async deleteAccess(@Param('id') id: string): Promise { 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 }); } 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 4edaed42d..6dde5604c 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 @@ -61,7 +61,9 @@ class="no-max-width" xPosition="before" > - @if (user()?.settings?.isExperimentalFeatures) { + @if ( + !isReceivedAccess() && user()?.settings?.isExperimentalFeatures + ) { } @if ( - user()?.settings?.isExperimentalFeatures || + (!isReceivedAccess() && user()?.settings?.isExperimentalFeatures) || element.type === 'PUBLIC' ) {
@@ -86,7 +88,11 @@ 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 900184110..7477ca138 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 @@ -52,6 +52,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; }) export class GfAccessTableComponent { public readonly accesses = input.required(); + public readonly isReceivedAccess = input(false); public readonly showActions = input(false); public readonly user = input.required(); @@ -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?` }); } diff --git a/apps/client/src/app/components/user-account-access/user-account-access.component.ts b/apps/client/src/app/components/user-account-access/user-account-access.component.ts index 57857803c..eb1a590d4 100644 --- a/apps/client/src/app/components/user-account-access/user-account-access.component.ts +++ b/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: () => { - this.update(); - } + .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(); }); } diff --git a/apps/client/src/app/components/user-account-access/user-account-access.html b/apps/client/src/app/components/user-account-access/user-account-access.html index 62b1648bb..a2f755029 100644 --- a/apps/client/src/app/components/user-account-access/user-account-access.html +++ b/apps/client/src/app/components/user-account-access/user-account-access.html @@ -53,7 +53,14 @@
@if (accessesGet.length > 0) {

Received Access

- + }

Granted Access diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts index fb9376041..fc9c90886 100644 --- a/libs/ui/src/lib/services/data.service.ts +++ b/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(`/api/v1/access/${aId}`); + return this.http.delete(`/api/v1/access/${aId}`); } public deleteAccount(aId: string) {