* Grant private access * Update changelog
This commit is contained in:
parent
6a048cee85
commit
62f85293e2
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### 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 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
|
- Added support for REST APIs (`JSON`) via the scraper configuration
|
||||||
- Enabled the _Redis_ authentication in the `docker-compose` files
|
- Enabled the _Redis_ authentication in the `docker-compose` files
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
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 { Access } from '@ghostfolio/common/interfaces';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
@ -26,6 +27,7 @@ import { CreateAccessDto } from './create-access.dto';
|
|||||||
export class AccessController {
|
export class AccessController {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly accessService: AccessService,
|
private readonly accessService: AccessService,
|
||||||
|
private readonly configurationService: ConfigurationService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -65,13 +67,30 @@ export class AccessController {
|
|||||||
public async createAccess(
|
public async createAccess(
|
||||||
@Body() data: CreateAccessDto
|
@Body() data: CreateAccessDto
|
||||||
): Promise<AccessModel> {
|
): Promise<AccessModel> {
|
||||||
return this.accessService.createAccess({
|
if (
|
||||||
alias: data.alias || undefined,
|
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||||
GranteeUser: data.granteeUserId
|
this.request.user.subscription.type === 'Basic'
|
||||||
? { connect: { id: data.granteeUserId } }
|
) {
|
||||||
: undefined,
|
throw new HttpException(
|
||||||
User: { connect: { id: this.request.user.id } }
|
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')
|
@Delete(':id')
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
@ -7,7 +8,7 @@ import { AccessService } from './access.service';
|
|||||||
@Module({
|
@Module({
|
||||||
controllers: [AccessController],
|
controllers: [AccessController],
|
||||||
exports: [AccessService],
|
exports: [AccessService],
|
||||||
imports: [PrismaModule],
|
imports: [ConfigurationModule, PrismaModule],
|
||||||
providers: [AccessService]
|
providers: [AccessService]
|
||||||
})
|
})
|
||||||
export class AccessModule {}
|
export class AccessModule {}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { IsOptional, IsString } from 'class-validator';
|
import { IsOptional, IsString, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
export class CreateAccessDto {
|
export class CreateAccessDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -6,7 +6,7 @@ export class CreateAccessDto {
|
|||||||
alias?: string;
|
alias?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsUUID()
|
||||||
granteeUserId?: string;
|
granteeUserId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="type">
|
<ng-container matColumnDef="type">
|
||||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Type</th>
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th>
|
||||||
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
|
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
|
||||||
<div class="align-items-center d-flex">
|
<div class="align-items-center d-flex">
|
||||||
<ion-icon class="mr-1" name="lock-closed-outline" />
|
<ion-icon class="mr-1" name="lock-closed-outline" />
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
Inject,
|
Inject,
|
||||||
OnDestroy
|
OnDestroy
|
||||||
@ -7,7 +8,9 @@ import {
|
|||||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
|
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';
|
import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
@ -24,15 +27,32 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
|
|||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateAccessDialogParams,
|
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateAccessDialogParams,
|
||||||
public dialogRef: MatDialogRef<CreateOrUpdateAccessDialog>,
|
public dialogRef: MatDialogRef<CreateOrUpdateAccessDialog>,
|
||||||
|
private dataService: DataService,
|
||||||
private formBuilder: FormBuilder
|
private formBuilder: FormBuilder
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.accessForm = this.formBuilder.group({
|
this.accessForm = this.formBuilder.group({
|
||||||
alias: [this.data.access.alias],
|
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() {
|
public onSubmit() {
|
||||||
const access: CreateAccessDto = {
|
const access: CreateAccessDto = {
|
||||||
alias: this.accessForm.controls['alias'].value,
|
alias: this.accessForm.controls['alias'].value,
|
||||||
|
granteeUserId: this.accessForm.controls['userId'].value,
|
||||||
type: this.accessForm.controls['type'].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() {
|
public ngOnDestroy() {
|
||||||
|
@ -21,10 +21,27 @@
|
|||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Type</mat-label>
|
<mat-label i18n>Type</mat-label>
|
||||||
<mat-select formControlName="type">
|
<mat-select formControlName="type">
|
||||||
|
<mat-option i18n value="PRIVATE">Private</mat-option>
|
||||||
<mat-option i18n value="PUBLIC">Public</mat-option>
|
<mat-option i18n value="PUBLIC">Public</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (accessForm.controls['type'].value === 'PRIVATE') {
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label
|
||||||
|
>Ghostfolio <ng-container i18n>User ID</ng-container></mat-label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
formControlName="userId"
|
||||||
|
matInput
|
||||||
|
type="text"
|
||||||
|
(keydown.enter)="$event.stopPropagation()"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="justify-content-end" mat-dialog-actions>
|
<div class="justify-content-end" mat-dialog-actions>
|
||||||
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
|
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
|
||||||
|
@ -105,32 +105,20 @@ export class UserAccountAccessComponent implements OnDestroy, OnInit {
|
|||||||
data: {
|
data: {
|
||||||
access: {
|
access: {
|
||||||
alias: '',
|
alias: '',
|
||||||
type: 'PUBLIC'
|
type: 'PRIVATE'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef
|
dialogRef.afterClosed().subscribe((access) => {
|
||||||
.afterClosed()
|
if (access) {
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
this.update();
|
||||||
.subscribe((data: any) => {
|
}
|
||||||
const access: CreateAccessDto = data?.access;
|
|
||||||
|
|
||||||
if (access) {
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
this.dataService
|
});
|
||||||
.postAccess({ alias: access.alias })
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
|
@ -201,7 +201,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="align-items-center d-flex mt-4 py-1">
|
<div class="align-items-center d-flex mt-4 py-1">
|
||||||
<div class="pr-1 w-50" i18n>User ID</div>
|
<div class="pr-1 w-50">
|
||||||
|
Ghostfolio <ng-container i18n>User ID</ng-container>
|
||||||
|
</div>
|
||||||
<div class="pl-1 text-monospace w-50">{{ user?.id }}</div>
|
<div class="pl-1 text-monospace w-50">{{ user?.id }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="align-items-center d-flex py-1">
|
<div class="align-items-center d-flex py-1">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export interface Access {
|
export interface Access {
|
||||||
alias?: string;
|
alias?: string;
|
||||||
grantee: string;
|
grantee?: string;
|
||||||
id: string;
|
id: string;
|
||||||
type: 'PUBLIC' | 'RESTRICTED_VIEW';
|
type: 'PRIVATE' | 'PUBLIC' | 'RESTRICTED_VIEW';
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user