Feature/add user interface for granting and revoking public access (#439)
* Add user interface for granting and revoking public access * Update changelog
This commit is contained in:
parent
1296f95602
commit
2de0e75cb8
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Added the user interface for granting and revoking public access to share the portfolio
|
||||
|
||||
### Changed
|
||||
|
||||
- Moved the data enhancer calls from the data provider (`get()`) to the data gathering service to reduce traffic to 3rd party data providers
|
||||
@ -26,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Added a public page to share your portfolio
|
||||
- Added a public page to share the portfolio
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -1,10 +1,29 @@
|
||||
import { Access } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
getPermissions,
|
||||
hasPermission,
|
||||
permissions
|
||||
} from '@ghostfolio/common/permissions';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpException,
|
||||
Inject,
|
||||
Param,
|
||||
Post,
|
||||
UseGuards
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Access as AccessModel } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { AccessModule } from './access.module';
|
||||
import { AccessService } from './access.service';
|
||||
import { CreateAccessDto } from './create-access.dto';
|
||||
|
||||
@Controller('access')
|
||||
export class AccessController {
|
||||
@ -39,4 +58,49 @@ export class AccessController {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async createAccess(
|
||||
@Body() data: CreateAccessDto
|
||||
): Promise<AccessModel> {
|
||||
if (
|
||||
!hasPermission(
|
||||
getPermissions(this.request.user.role),
|
||||
permissions.createAccess
|
||||
)
|
||||
) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
return this.accessService.createAccess({
|
||||
User: { connect: { id: this.request.user.id } }
|
||||
});
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async deleteAccess(@Param('id') id: string): Promise<AccessModule> {
|
||||
if (
|
||||
!hasPermission(
|
||||
getPermissions(this.request.user.role),
|
||||
permissions.deleteAccess
|
||||
)
|
||||
) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
return this.accessService.deleteAccess({
|
||||
id_userId: {
|
||||
id,
|
||||
userId: this.request.user.id
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { AccessWithGranteeUser } from '@ghostfolio/common/types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { Access, Prisma } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class AccessService {
|
||||
@ -37,4 +37,18 @@ export class AccessService {
|
||||
where
|
||||
});
|
||||
}
|
||||
|
||||
public async createAccess(data: Prisma.AccessCreateInput): Promise<Access> {
|
||||
return this.prismaService.access.create({
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteAccess(
|
||||
where: Prisma.AccessWhereUniqueInput
|
||||
): Promise<Access> {
|
||||
return this.prismaService.access.delete({
|
||||
where
|
||||
});
|
||||
}
|
||||
}
|
||||
|
1
apps/api/src/app/access/create-access.dto.ts
Normal file
1
apps/api/src/app/access/create-access.dto.ts
Normal file
@ -0,0 +1 @@
|
||||
export class CreateAccessDto {}
|
@ -272,7 +272,7 @@ export class PortfolioController {
|
||||
return <any>res.json({ accounts: {}, holdings: {} });
|
||||
}
|
||||
|
||||
const { hasErrors, holdings } = await this.portfolioService.getDetails(
|
||||
const { holdings } = await this.portfolioService.getDetails(
|
||||
access.userId,
|
||||
access.userId
|
||||
);
|
||||
@ -281,10 +281,6 @@ export class PortfolioController {
|
||||
holdings: {}
|
||||
};
|
||||
|
||||
if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
|
||||
res.status(StatusCodes.ACCEPTED);
|
||||
}
|
||||
|
||||
const totalValue = Object.values(holdings)
|
||||
.filter((holding) => {
|
||||
return holding.assetClass === 'EQUITY';
|
||||
|
@ -1,24 +1,52 @@
|
||||
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
|
||||
<ng-container matColumnDef="granteeAlias">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>User</th>
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Grantee</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.granteeAlias }}
|
||||
</td></ng-container
|
||||
>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="type">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Type</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
<ng-container *ngIf="element.type === 'PUBLIC'">
|
||||
<ion-icon class="mr-1" name="link-outline"></ion-icon>
|
||||
{{ baseUrl }}/p/{{ element.id }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="element.type === 'RESTRICTED_VIEW'">
|
||||
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
|
||||
<ng-container>
|
||||
<ion-icon class="mr-1" name="lock-closed-outline"></ion-icon>
|
||||
Restricted View
|
||||
</ng-container>
|
||||
</td></ng-container
|
||||
>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="details">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Details</th>
|
||||
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
|
||||
<ng-container *ngIf="element.type === 'PUBLIC'">
|
||||
<ion-icon class="mr-1" name="link-outline"></ion-icon>
|
||||
<a href="{{ baseUrl }}/p/{{ element.id }}" target="_blank"
|
||||
>{{ baseUrl }}/p/{{ element.id }}</a
|
||||
>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell></th>
|
||||
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="transactionMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #transactionMenu="matMenu" xPosition="before">
|
||||
<button i18n mat-menu-item (click)="onDeleteAccess(element.id)">
|
||||
Revoke
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
|
@ -2,4 +2,12 @@
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--palette-primary-300), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit
|
||||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Access } from '@ghostfolio/common/interfaces';
|
||||
@ -16,18 +18,37 @@ import { Access } from '@ghostfolio/common/interfaces';
|
||||
})
|
||||
export class AccessTableComponent implements OnChanges, OnInit {
|
||||
@Input() accesses: Access[];
|
||||
@Input() showActions: boolean;
|
||||
|
||||
@Output() accessDeleted = new EventEmitter<string>();
|
||||
|
||||
public baseUrl = window.location.origin;
|
||||
public dataSource: MatTableDataSource<Access>;
|
||||
public displayedColumns = ['granteeAlias', 'type'];
|
||||
public displayedColumns = [];
|
||||
|
||||
public constructor() {}
|
||||
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngOnChanges() {
|
||||
this.displayedColumns = ['granteeAlias', 'type', 'details'];
|
||||
|
||||
if (this.showActions) {
|
||||
this.displayedColumns.push('actions');
|
||||
}
|
||||
|
||||
if (this.accesses) {
|
||||
this.dataSource = new MatTableDataSource(this.accesses);
|
||||
}
|
||||
}
|
||||
|
||||
public onDeleteAccess(aId: string) {
|
||||
const confirmation = confirm(
|
||||
'Do you really want to revoke this granted access?'
|
||||
);
|
||||
|
||||
if (confirmation) {
|
||||
this.accessDeleted.emit(aId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
|
||||
import { AccessTableComponent } from './access-table.component';
|
||||
@ -7,7 +9,7 @@ import { AccessTableComponent } from './access-table.component';
|
||||
@NgModule({
|
||||
declarations: [AccessTableComponent],
|
||||
exports: [AccessTableComponent],
|
||||
imports: [CommonModule, MatTableModule],
|
||||
imports: [CommonModule, MatButtonModule, MatMenuModule, MatTableModule],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
|
@ -5,20 +5,26 @@ import {
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import {
|
||||
MatSlideToggle,
|
||||
MatSlideToggleChange
|
||||
} from '@angular/material/slide-toggle';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
||||
import { DEFAULT_DATE_FORMAT, baseCurrency } from '@ghostfolio/common/config';
|
||||
import { Access, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { StripeService } from 'ngx-stripe';
|
||||
import { EMPTY, Subject } from 'rxjs';
|
||||
import { catchError, switchMap, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog/create-or-update-access-dialog.component';
|
||||
|
||||
@Component({
|
||||
host: { class: 'mb-5' },
|
||||
selector: 'gf-account-page',
|
||||
@ -35,7 +41,10 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
public couponId: string;
|
||||
public currencies: string[] = [];
|
||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||
public deviceType: string;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public hasPermissionToCreateAccess: boolean;
|
||||
public hasPermissionToDeleteAccess: boolean;
|
||||
public hasPermissionToUpdateViewMode: boolean;
|
||||
public hasPermissionToUpdateUserSettings: boolean;
|
||||
public price: number;
|
||||
@ -50,6 +59,10 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private dialog: MatDialog,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private stripeService: StripeService,
|
||||
private userService: UserService,
|
||||
public webAuthnService: WebAuthnService
|
||||
@ -65,6 +78,11 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
permissions.enableSubscription
|
||||
);
|
||||
|
||||
this.hasPermissionToDeleteAccess = hasPermission(
|
||||
globalPermissions,
|
||||
permissions.deleteAccess
|
||||
);
|
||||
|
||||
this.price = subscriptions?.[0]?.price;
|
||||
this.priceId = subscriptions?.[0]?.priceId;
|
||||
|
||||
@ -74,6 +92,16 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.hasPermissionToCreateAccess = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.createAccess
|
||||
);
|
||||
|
||||
this.hasPermissionToDeleteAccess = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.deleteAccess
|
||||
);
|
||||
|
||||
this.hasPermissionToUpdateUserSettings = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.updateUserSettings
|
||||
@ -87,12 +115,22 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
this.route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (params['createDialog']) {
|
||||
this.openCreateAccessDialog();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
@ -136,6 +174,17 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public onDeleteAccess(aId: string) {
|
||||
this.dataService
|
||||
.deleteAccess(aId)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public onRestrictedViewChange(aEvent: MatSlideToggleChange) {
|
||||
this.dataService
|
||||
.putUserSetting({ isRestrictedView: aEvent.checked })
|
||||
@ -175,6 +224,38 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private openCreateAccessDialog(): void {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateAccessDialog, {
|
||||
data: {
|
||||
access: {
|
||||
type: 'PUBLIC'
|
||||
}
|
||||
},
|
||||
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({})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
});
|
||||
}
|
||||
|
||||
private deregisterDevice() {
|
||||
this.webAuthnService
|
||||
.deregister()
|
||||
|
@ -132,10 +132,26 @@
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="accesses?.length > 0" class="row">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center" i18n>Granted Access</h3>
|
||||
<gf-access-table [accesses]="accesses"></gf-access-table>
|
||||
<gf-access-table
|
||||
[accesses]="accesses"
|
||||
[showActions]="hasPermissionToDeleteAccess"
|
||||
(accessDeleted)="onDeleteAccess($event)"
|
||||
></gf-access-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hasPermissionToCreateAccess" class="fab-container">
|
||||
<a
|
||||
class="align-items-center d-flex justify-content-center"
|
||||
color="primary"
|
||||
mat-fab
|
||||
[routerLink]="[]"
|
||||
[queryParams]="{ createDialog: true }"
|
||||
>
|
||||
<ion-icon name="add-outline" size="large"></ion-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,6 +12,7 @@ import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/acce
|
||||
|
||||
import { AccountPageRoutingModule } from './account-page-routing.module';
|
||||
import { AccountPageComponent } from './account-page.component';
|
||||
import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-dialog/create-or-update-access-dialog.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AccountPageComponent],
|
||||
@ -20,6 +21,7 @@ import { AccountPageComponent } from './account-page.component';
|
||||
AccountPageRoutingModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfCreateOrUpdateAccessDialogModule,
|
||||
GfPortfolioAccessTableModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
|
@ -2,6 +2,26 @@
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: block;
|
||||
|
||||
gf-access-table {
|
||||
overflow-x: auto;
|
||||
|
||||
table {
|
||||
min-width: 100%;
|
||||
|
||||
.mat-row,
|
||||
.mat-header-row {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fab-container {
|
||||
position: fixed;
|
||||
right: 2rem;
|
||||
bottom: 2rem;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-size: 90%;
|
||||
line-height: 1.2;
|
||||
|
@ -0,0 +1,37 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Inject,
|
||||
OnDestroy
|
||||
} from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'h-100' },
|
||||
selector: 'gf-create-or-update-access-dialog',
|
||||
styleUrls: ['./create-or-update-access-dialog.scss'],
|
||||
templateUrl: 'create-or-update-access-dialog.html'
|
||||
})
|
||||
export class CreateOrUpdateAccessDialog implements OnDestroy {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
public dialogRef: MatDialogRef<CreateOrUpdateAccessDialog>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateAccessDialogParams
|
||||
) {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
public onCancel(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
<form #addAccessForm="ngForm" class="d-flex flex-column h-100">
|
||||
<h1 i18n mat-dialog-title>Grant access</h1>
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Type</mat-label>
|
||||
<mat-select name="type" required [(value)]="data.access.type">
|
||||
<mat-option i18n value="PUBLIC">Public</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="justify-content-end" mat-dialog-actions>
|
||||
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
||||
<button
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[disabled]="!addAccessForm.form.valid"
|
||||
[mat-dialog-close]="data"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
@ -0,0 +1,25 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
|
||||
import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [CreateOrUpdateAccessDialog],
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
providers: []
|
||||
})
|
||||
export class GfCreateOrUpdateAccessDialogModule {}
|
@ -0,0 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.mat-dialog-content {
|
||||
max-height: unset;
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import { Access } from '@ghostfolio/common/interfaces';
|
||||
|
||||
export interface CreateOrUpdateAccessDialogParams {
|
||||
access: Access;
|
||||
}
|
@ -45,7 +45,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
private router: Router,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.routeQueryParams = route.queryParams
|
||||
this.route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (params['createDialog']) {
|
||||
|
@ -12,8 +12,8 @@
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Type</mat-label>
|
||||
<mat-select name="type" required [(value)]="data.account.accountType">
|
||||
<mat-option value="CASH" i18n>Cash</mat-option>
|
||||
<mat-option value="SECURITIES" i18n>Securities</mat-option>
|
||||
<mat-option i18n value="CASH">Cash</mat-option>
|
||||
<mat-option i18n value="SECURITIES">Securities</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
@ -1,7 +1,9 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3 class="d-flex justify-content-center mb-3" i18n>Portfolio</h3>
|
||||
<h3 class="h4 mb-3 text-center" i18n>
|
||||
Hello, someone has shared a <strong>Portfolio</strong> with you!
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="proportion-charts row">
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
|
||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||
import { ImportDataDto } from '@ghostfolio/api/app/import/import-data.dto';
|
||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||
import {
|
||||
@ -69,6 +69,10 @@ export class DataService {
|
||||
return this.http.get<AdminData>('/api/admin');
|
||||
}
|
||||
|
||||
public deleteAccess(aId: string) {
|
||||
return this.http.delete<any>(`/api/access/${aId}`);
|
||||
}
|
||||
|
||||
public deleteAccount(aId: string) {
|
||||
return this.http.delete<any>(`/api/account/${aId}`);
|
||||
}
|
||||
@ -197,6 +201,10 @@ export class DataService {
|
||||
return this.http.get<any>(`/api/auth/anonymous/${accessToken}`);
|
||||
}
|
||||
|
||||
public postAccess(aAccess: CreateAccessDto) {
|
||||
return this.http.post<OrderModel>(`/api/access`, aAccess);
|
||||
}
|
||||
|
||||
public postAccount(aAccount: CreateAccountDto) {
|
||||
return this.http.post<OrderModel>(`/api/account`, aAccount);
|
||||
}
|
||||
|
@ -7,9 +7,11 @@ export function isApiTokenAuthorized(aApiToken: string) {
|
||||
export const permissions = {
|
||||
accessAdminControl: 'accessAdminControl',
|
||||
accessFearAndGreedIndex: 'accessFearAndGreedIndex',
|
||||
createAccess: 'createAccess',
|
||||
createAccount: 'createAccount',
|
||||
createOrder: 'createOrder',
|
||||
createUserAccount: 'createUserAccount',
|
||||
deleteAccess: 'deleteAccess',
|
||||
deleteAccount: 'deleteAcccount',
|
||||
deleteAuthDevice: 'deleteAuthDevice',
|
||||
deleteOrder: 'deleteOrder',
|
||||
@ -38,8 +40,10 @@ export function getPermissions(aRole: Role): string[] {
|
||||
case 'ADMIN':
|
||||
return [
|
||||
permissions.accessAdminControl,
|
||||
permissions.createAccess,
|
||||
permissions.createAccount,
|
||||
permissions.createOrder,
|
||||
permissions.deleteAccess,
|
||||
permissions.deleteAccount,
|
||||
permissions.deleteAuthDevice,
|
||||
permissions.deleteOrder,
|
||||
@ -56,8 +60,10 @@ export function getPermissions(aRole: Role): string[] {
|
||||
|
||||
case 'USER':
|
||||
return [
|
||||
permissions.createAccess,
|
||||
permissions.createAccount,
|
||||
permissions.createOrder,
|
||||
permissions.deleteAccess,
|
||||
permissions.deleteAccount,
|
||||
permissions.deleteAuthDevice,
|
||||
permissions.deleteOrder,
|
||||
|
Loading…
x
Reference in New Issue
Block a user