From 62f85293e2e2106ab847e81fb1c57f509e7e1df0 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Sat, 6 Jan 2024 10:27:21 +0100 Subject: [PATCH] #2820 Grant private access (#2822) * Grant private access * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/access/access.controller.ts | 33 +++++++++++---- apps/api/src/app/access/access.module.ts | 3 +- apps/api/src/app/access/create-access.dto.ts | 4 +- .../access-table/access-table.component.html | 2 +- ...reate-or-update-access-dialog.component.ts | 41 +++++++++++++++++-- .../create-or-update-access-dialog.html | 17 ++++++++ .../user-account-access.component.ts | 26 ++++-------- .../user-account-settings.html | 4 +- .../src/lib/interfaces/access.interface.ts | 4 +- 10 files changed, 99 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ccfda047..631332ed6 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 grant private access - Added a hint for _Time-Weighted Rate of Return_ (TWR) to the portfolio summary tab on the home page - Added support for REST APIs (`JSON`) via the scraper configuration - Enabled the _Redis_ authentication in the `docker-compose` files diff --git a/apps/api/src/app/access/access.controller.ts b/apps/api/src/app/access/access.controller.ts index 47f6c08b8..b673bb734 100644 --- a/apps/api/src/app/access/access.controller.ts +++ b/apps/api/src/app/access/access.controller.ts @@ -1,5 +1,6 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { Access } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; @@ -26,6 +27,7 @@ import { CreateAccessDto } from './create-access.dto'; export class AccessController { public constructor( private readonly accessService: AccessService, + private readonly configurationService: ConfigurationService, @Inject(REQUEST) private readonly request: RequestWithUser ) {} @@ -65,13 +67,30 @@ export class AccessController { public async createAccess( @Body() data: CreateAccessDto ): Promise { - return this.accessService.createAccess({ - alias: data.alias || undefined, - GranteeUser: data.granteeUserId - ? { connect: { id: data.granteeUserId } } - : undefined, - User: { connect: { id: this.request.user.id } } - }); + if ( + this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && + this.request.user.subscription.type === 'Basic' + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + try { + return await this.accessService.createAccess({ + alias: data.alias || undefined, + GranteeUser: data.granteeUserId + ? { connect: { id: data.granteeUserId } } + : undefined, + User: { connect: { id: this.request.user.id } } + }); + } catch { + throw new HttpException( + getReasonPhrase(StatusCodes.BAD_REQUEST), + StatusCodes.BAD_REQUEST + ); + } } @Delete(':id') diff --git a/apps/api/src/app/access/access.module.ts b/apps/api/src/app/access/access.module.ts index b9813d173..7f466d35c 100644 --- a/apps/api/src/app/access/access.module.ts +++ b/apps/api/src/app/access/access.module.ts @@ -1,3 +1,4 @@ +import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { Module } from '@nestjs/common'; @@ -7,7 +8,7 @@ import { AccessService } from './access.service'; @Module({ controllers: [AccessController], exports: [AccessService], - imports: [PrismaModule], + imports: [ConfigurationModule, PrismaModule], providers: [AccessService] }) export class AccessModule {} diff --git a/apps/api/src/app/access/create-access.dto.ts b/apps/api/src/app/access/create-access.dto.ts index b9cf8892d..a6a24690d 100644 --- a/apps/api/src/app/access/create-access.dto.ts +++ b/apps/api/src/app/access/create-access.dto.ts @@ -1,4 +1,4 @@ -import { IsOptional, IsString } from 'class-validator'; +import { IsOptional, IsString, IsUUID } from 'class-validator'; export class CreateAccessDto { @IsOptional() @@ -6,7 +6,7 @@ export class CreateAccessDto { alias?: string; @IsOptional() - @IsString() + @IsUUID() granteeUserId?: string; @IsOptional() 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 761bce9f9..e150d16ac 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 @@ -14,7 +14,7 @@ - Type + Permission
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 2aa38f4d7..495ad3354 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 @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, + ChangeDetectorRef, Component, Inject, OnDestroy @@ -7,7 +8,9 @@ import { import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto'; -import { Subject } from 'rxjs'; +import { DataService } from '@ghostfolio/client/services/data.service'; +import { StatusCodes } from 'http-status-codes'; +import { EMPTY, Subject, catchError, takeUntil } from 'rxjs'; import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces'; @@ -24,15 +27,32 @@ export class CreateOrUpdateAccessDialog implements OnDestroy { private unsubscribeSubject = new Subject(); public constructor( + private changeDetectorRef: ChangeDetectorRef, @Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateAccessDialogParams, public dialogRef: MatDialogRef, + private dataService: DataService, private formBuilder: FormBuilder ) {} ngOnInit() { this.accessForm = this.formBuilder.group({ alias: [this.data.access.alias], - type: [this.data.access.type, Validators.required] + type: [this.data.access.type, Validators.required], + userId: [this.data.access.grantee, Validators.required] + }); + + this.accessForm.get('type').valueChanges.subscribe((value) => { + const userIdControl = this.accessForm.get('userId'); + + if (value === 'PRIVATE') { + userIdControl.setValidators(Validators.required); + } else { + userIdControl.clearValidators(); + } + + userIdControl.updateValueAndValidity(); + + this.changeDetectorRef.markForCheck(); }); } @@ -43,10 +63,25 @@ export class CreateOrUpdateAccessDialog implements OnDestroy { public onSubmit() { const access: CreateAccessDto = { alias: this.accessForm.controls['alias'].value, + granteeUserId: this.accessForm.controls['userId'].value, type: this.accessForm.controls['type'].value }; - this.dialogRef.close({ access }); + this.dataService + .postAccess(access) + .pipe( + catchError((error) => { + if (error.status === StatusCodes.BAD_REQUEST) { + alert($localize`Oops! Could not grant access.`); + } + + return EMPTY; + }), + takeUntil(this.unsubscribeSubject) + ) + .subscribe(() => { + this.dialogRef.close({ access }); + }); } public ngOnDestroy() { diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html index c2afc51fb..31b9d7626 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -21,10 +21,27 @@ Type + Private Public
+ + @if (accessForm.controls['type'].value === 'PRIVATE') { +
+ + Ghostfolio User ID + + +
+ }
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 1bd1d85d6..999563831 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 @@ -105,32 +105,20 @@ export class UserAccountAccessComponent implements OnDestroy, OnInit { data: { access: { alias: '', - type: 'PUBLIC' + type: 'PRIVATE' } }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', width: this.deviceType === 'mobile' ? '100vw' : '50rem' }); - dialogRef - .afterClosed() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((data: any) => { - const access: CreateAccessDto = data?.access; - - if (access) { - this.dataService - .postAccess({ alias: access.alias }) - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe({ - next: () => { - this.update(); - } - }); - } + dialogRef.afterClosed().subscribe((access) => { + if (access) { + this.update(); + } - this.router.navigate(['.'], { relativeTo: this.route }); - }); + this.router.navigate(['.'], { relativeTo: this.route }); + }); } private update() { diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.html b/apps/client/src/app/components/user-account-settings/user-account-settings.html index b77b5e94c..0a9462a58 100644 --- a/apps/client/src/app/components/user-account-settings/user-account-settings.html +++ b/apps/client/src/app/components/user-account-settings/user-account-settings.html @@ -201,7 +201,9 @@
-
User ID
+
+ Ghostfolio User ID +
{{ user?.id }}
diff --git a/libs/common/src/lib/interfaces/access.interface.ts b/libs/common/src/lib/interfaces/access.interface.ts index 27503c872..299616cf4 100644 --- a/libs/common/src/lib/interfaces/access.interface.ts +++ b/libs/common/src/lib/interfaces/access.interface.ts @@ -1,6 +1,6 @@ export interface Access { alias?: string; - grantee: string; + grantee?: string; id: string; - type: 'PUBLIC' | 'RESTRICTED_VIEW'; + type: 'PRIVATE' | 'PUBLIC' | 'RESTRICTED_VIEW'; }