diff --git a/CHANGELOG.md b/CHANGELOG.md index 41f949b84..954887ea4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Introduced the option to update the cash balance of an account when adding an activity +- Added support for the management of platforms in the admin control panel ### Changed diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 83179a402..90f1bd605 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -34,6 +34,7 @@ import { RedisCacheModule } from './redis-cache/redis-cache.module'; import { SubscriptionModule } from './subscription/subscription.module'; import { SymbolModule } from './symbol/symbol.module'; import { UserModule } from './user/user.module'; +import { PlatformModule } from './platform/platform.module'; @Module({ imports: [ @@ -63,6 +64,7 @@ import { UserModule } from './user/user.module'; InfoModule, LogoModule, OrderModule, + PlatformModule, PortfolioModule, PrismaModule, RedisCacheModule, diff --git a/apps/api/src/app/import/import.module.ts b/apps/api/src/app/import/import.module.ts index 16f4bcdd6..943f95c70 100644 --- a/apps/api/src/app/import/import.module.ts +++ b/apps/api/src/app/import/import.module.ts @@ -7,7 +7,7 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration/conf import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; -import { PlatformModule } from '@ghostfolio/api/services/platform/platform.module'; +import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { Module } from '@nestjs/common'; diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 98dc0e249..cd2d98be5 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -6,7 +6,7 @@ import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; -import { PlatformService } from '@ghostfolio/api/services/platform/platform.service'; +import { PlatformService } from '@ghostfolio/api/app/platform/platform.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { parseDate } from '@ghostfolio/common/helper'; import { UniqueAsset } from '@ghostfolio/common/interfaces'; @@ -130,7 +130,7 @@ export class ImportService { } } }), - this.platformService.get() + this.platformService.getPlatforms() ]); for (const account of accountsDto) { diff --git a/apps/api/src/app/platform/create-platform.dto.ts b/apps/api/src/app/platform/create-platform.dto.ts new file mode 100644 index 000000000..a61f21743 --- /dev/null +++ b/apps/api/src/app/platform/create-platform.dto.ts @@ -0,0 +1,9 @@ +import { IsString } from 'class-validator'; + +export class CreatePlatformDto { + @IsString() + name: string; + + @IsString() + url: string; +} diff --git a/apps/api/src/app/platform/platform.controller.ts b/apps/api/src/app/platform/platform.controller.ts new file mode 100644 index 000000000..988905c4b --- /dev/null +++ b/apps/api/src/app/platform/platform.controller.ts @@ -0,0 +1,113 @@ +import { + Body, + Controller, + Delete, + Get, + HttpException, + Inject, + Param, + Post, + Put, + UseGuards +} from '@nestjs/common'; +import { PlatformService } from './platform.service'; +import { AuthGuard } from '@nestjs/passport'; +import { Platform } from '@prisma/client'; +import { CreatePlatformDto } from './create-platform.dto'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { RequestWithUser } from '@ghostfolio/common/types'; +import { REQUEST } from '@nestjs/core'; +import { getReasonPhrase, StatusCodes } from 'http-status-codes'; +import { UpdatePlatformDto } from './update-platform.dto'; + +@Controller('platform') +export class PlatformController { + public constructor( + private readonly platformService: PlatformService, + @Inject(REQUEST) private readonly request: RequestWithUser + ) {} + + @Get() + @UseGuards(AuthGuard('jwt')) + public async getPlatforms(): Promise { + return this.platformService.getPlatforms(); + } + + @Post() + @UseGuards(AuthGuard('jwt')) + public async createPlatform( + @Body() data: CreatePlatformDto + ): Promise { + if ( + !hasPermission(this.request.user.permissions, permissions.createPlatform) + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + return this.platformService.createPlatform(data); + } + + @Put(':id') + @UseGuards(AuthGuard('jwt')) + public async updatePlatform( + @Param('id') id: string, + @Body() data: UpdatePlatformDto + ) { + if ( + !hasPermission(this.request.user.permissions, permissions.updatePlatform) + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + const originalPlatform = await this.platformService.getPlatform({ + id + }); + + if (!originalPlatform) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + return this.platformService.updatePlatform({ + data: { + ...data + }, + where: { + id + } + }); + } + + @Delete(':id') + @UseGuards(AuthGuard('jwt')) + public async deletePlatform(@Param('id') id: string) { + if ( + !hasPermission(this.request.user.permissions, permissions.deletePlatform) + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + const originalPlatform = await this.platformService.getPlatform({ + id + }); + + if (!originalPlatform) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + return this.platformService.deletePlatform({ id }); + } +} diff --git a/apps/api/src/services/platform/platform.module.ts b/apps/api/src/app/platform/platform.module.ts similarity index 75% rename from apps/api/src/services/platform/platform.module.ts rename to apps/api/src/app/platform/platform.module.ts index 8167a37e0..04ccdf4d6 100644 --- a/apps/api/src/services/platform/platform.module.ts +++ b/apps/api/src/app/platform/platform.module.ts @@ -1,9 +1,11 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { Module } from '@nestjs/common'; +import { PlatformController } from './platform.controller'; import { PlatformService } from './platform.service'; @Module({ + controllers: [PlatformController], exports: [PlatformService], imports: [PrismaModule], providers: [PlatformService] diff --git a/apps/api/src/app/platform/platform.service.ts b/apps/api/src/app/platform/platform.service.ts new file mode 100644 index 000000000..9832252a4 --- /dev/null +++ b/apps/api/src/app/platform/platform.service.ts @@ -0,0 +1,45 @@ +import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; +import { Injectable } from '@nestjs/common'; +import { Platform, Prisma } from '@prisma/client'; + +@Injectable() +export class PlatformService { + public constructor(private readonly prismaService: PrismaService) {} + + public async getPlatforms(): Promise { + return this.prismaService.platform.findMany(); + } + + public async getPlatform( + platformWhereUniqueInput: Prisma.PlatformWhereUniqueInput + ): Promise { + return this.prismaService.platform.findUnique({ + where: platformWhereUniqueInput + }); + } + + public async createPlatform(data: Prisma.PlatformCreateInput) { + return this.prismaService.platform.create({ + data + }); + } + + public async updatePlatform({ + data, + where + }: { + data: Prisma.PlatformUpdateInput; + where: Prisma.PlatformWhereUniqueInput; + }): Promise { + return this.prismaService.platform.update({ + data, + where + }); + } + + public async deletePlatform( + where: Prisma.PlatformWhereUniqueInput + ): Promise { + return this.prismaService.platform.delete({ where }); + } +} diff --git a/apps/api/src/app/platform/update-platform.dto.ts b/apps/api/src/app/platform/update-platform.dto.ts new file mode 100644 index 000000000..ec6f2687c --- /dev/null +++ b/apps/api/src/app/platform/update-platform.dto.ts @@ -0,0 +1,12 @@ +import { IsString } from 'class-validator'; + +export class UpdatePlatformDto { + @IsString() + id: string; + + @IsString() + name: string; + + @IsString() + url: string; +} diff --git a/apps/api/src/services/platform/platform.service.ts b/apps/api/src/services/platform/platform.service.ts deleted file mode 100644 index 51bd4d73d..000000000 --- a/apps/api/src/services/platform/platform.service.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class PlatformService { - public constructor(private readonly prismaService: PrismaService) {} - - public async get() { - return this.prismaService.platform.findMany(); - } -} diff --git a/apps/client/src/app/components/platform/create-or-update-platform-dialog/create-or-update-account-platform.component.ts b/apps/client/src/app/components/platform/create-or-update-platform-dialog/create-or-update-account-platform.component.ts new file mode 100644 index 000000000..65dd6f755 --- /dev/null +++ b/apps/client/src/app/components/platform/create-or-update-platform-dialog/create-or-update-account-platform.component.ts @@ -0,0 +1,30 @@ +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Subject } from 'rxjs'; + +import { CreateOrUpdatePlatformDialogParams } from './interfaces/interfaces'; + +@Component({ + host: { class: 'h-100' }, + selector: 'gf-create-or-update-platform-dialog', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./create-or-update-platform-dialog.scss'], + templateUrl: 'create-or-update-platform-dialog.html' +}) +export class CreateOrUpdatePlatformDialog { + private unsubscribeSubject = new Subject(); + + public constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: CreateOrUpdatePlatformDialogParams + ) {} + + public onCancel() { + this.dialogRef.close(); + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } +} diff --git a/apps/client/src/app/components/platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html b/apps/client/src/app/components/platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html new file mode 100644 index 000000000..06f6ab72e --- /dev/null +++ b/apps/client/src/app/components/platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html @@ -0,0 +1,29 @@ +
+

Update platform

+

Add platform

+
+
+ + Name + + +
+
+ + Url + + +
+
+
+ + +
+
diff --git a/apps/client/src/app/components/platform/create-or-update-platform-dialog/create-or-update-platform-dialog.module.ts b/apps/client/src/app/components/platform/create-or-update-platform-dialog/create-or-update-platform-dialog.module.ts new file mode 100644 index 000000000..bb8c1e636 --- /dev/null +++ b/apps/client/src/app/components/platform/create-or-update-platform-dialog/create-or-update-platform-dialog.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { CreateOrUpdatePlatformDialog } from './create-or-update-account-platform.component'; +import { CommonModule } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; + +@NgModule({ + declarations: [CreateOrUpdatePlatformDialog], + imports: [ + CommonModule, + FormsModule, + MatButtonModule, + MatDialogModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule + ] +}) +export class GfCreateOrUpdatePlatformDialogModule {} diff --git a/apps/client/src/app/components/platform/create-or-update-platform-dialog/create-or-update-platform-dialog.scss b/apps/client/src/app/components/platform/create-or-update-platform-dialog/create-or-update-platform-dialog.scss new file mode 100644 index 000000000..b63df0134 --- /dev/null +++ b/apps/client/src/app/components/platform/create-or-update-platform-dialog/create-or-update-platform-dialog.scss @@ -0,0 +1,7 @@ +:host { + display: block; + + .mat-mdc-dialog-content { + max-height: unset; + } +} diff --git a/apps/client/src/app/components/platform/create-or-update-platform-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/platform/create-or-update-platform-dialog/interfaces/interfaces.ts new file mode 100644 index 000000000..be4af5407 --- /dev/null +++ b/apps/client/src/app/components/platform/create-or-update-platform-dialog/interfaces/interfaces.ts @@ -0,0 +1,5 @@ +import { Platform } from '@prisma/client'; + +export interface CreateOrUpdatePlatformDialogParams { + platform: Platform; +} diff --git a/apps/client/src/app/components/platform/platform.component.html b/apps/client/src/app/components/platform/platform.component.html new file mode 100644 index 000000000..b4560e0c4 --- /dev/null +++ b/apps/client/src/app/components/platform/platform.component.html @@ -0,0 +1,92 @@ +
+
+
+ + + + + + + + + + + + + + + + + +
+ Name + + + {{ element.name }} + + Url + + {{ element.url }} + + + + + + +
+
+
+ +
+ + + +
+
diff --git a/apps/client/src/app/components/platform/platform.component.scss b/apps/client/src/app/components/platform/platform.component.scss new file mode 100644 index 000000000..8d0a69f27 --- /dev/null +++ b/apps/client/src/app/components/platform/platform.component.scss @@ -0,0 +1,12 @@ +@import 'apps/client/src/styles/ghostfolio-style'; + +:host { + display: block; + + .fab-container { + position: fixed; + right: 2rem; + bottom: 4rem; + z-index: 999; + } +} diff --git a/apps/client/src/app/components/platform/platform.component.ts b/apps/client/src/app/components/platform/platform.component.ts new file mode 100644 index 000000000..66dc1707f --- /dev/null +++ b/apps/client/src/app/components/platform/platform.component.ts @@ -0,0 +1,222 @@ +import { + ChangeDetectorRef, + Component, + OnDestroy, + OnInit, + ViewChild +} from '@angular/core'; +import { MatSort } from '@angular/material/sort'; +import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto'; +import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto'; +import { get } from 'lodash'; +import { MatTableDataSource } from '@angular/material/table'; +import { ActivatedRoute, Router } from '@angular/router'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { Platform, Platform as PlatformModel } from '@prisma/client'; +import { Subject, takeUntil } from 'rxjs'; +import { CreateOrUpdatePlatformDialog } from './create-or-update-platform-dialog/create-or-update-account-platform.component'; +import { MatDialog } from '@angular/material/dialog'; +import { DeviceDetectorService } from 'ngx-device-detector'; +import { AdminService } from '@ghostfolio/client/services/admin.service'; + +@Component({ + selector: 'gf-platform-overview', + styleUrls: ['./platform.component.scss'], + templateUrl: './platform.component.html' +}) +export class AdminPlatformComponent implements OnInit, OnDestroy { + @ViewChild(MatSort) sort: MatSort; + + public dataSource: MatTableDataSource = new MatTableDataSource(); + public deviceType: string; + public displayedColumns = ['name', 'url', 'actions']; + public hasPermissionToCreatePlatform: boolean; + public hasPermissionToDeletePlatform: boolean; + public platforms: PlatformModel[]; + + private unsubscribeSubject = new Subject(); + + public constructor( + private adminService: AdminService, + private changeDetectorRef: ChangeDetectorRef, + private deviceService: DeviceDetectorService, + private dialog: MatDialog, + private route: ActivatedRoute, + private router: Router, + private userService: UserService + ) { + this.route.queryParams + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((params) => { + if (params['createDialog'] && this.hasPermissionToCreatePlatform) { + this.openCreatePlatformDialog(); + } else if (params['editDialog']) { + if (this.platforms) { + const platform = this.platforms.find(({ id }) => { + return id === params['platformId']; + }); + + this.openUpdatePlatformDialog(platform); + } else { + this.router.navigate(['.'], { relativeTo: this.route }); + } + } + }); + } + + public ngOnInit() { + this.deviceType = this.deviceService.getDeviceInfo().deviceType; + + this.userService.stateChanged + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((state) => { + if (state?.user) { + const user = state.user; + + this.hasPermissionToCreatePlatform = hasPermission( + user.permissions, + permissions.createPlatform + ); + this.hasPermissionToDeletePlatform = hasPermission( + user.permissions, + permissions.deletePlatform + ); + + this.changeDetectorRef.markForCheck(); + } + }); + + this.fetchPlatforms(); + } + + public onDeletePlatform(aId: string) { + const confirmation = confirm( + $localize`Do you really want to delete this platform?` + ); + + if (confirmation) { + this.deletePlatform(aId); + } + } + + public onUpdatePlatform(aPlatform: PlatformModel) { + this.router.navigate([], { + queryParams: { platformId: aPlatform.id, editDialog: true } + }); + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } + + private deletePlatform(aId: string) { + this.adminService + .deletePlatform(aId) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe({ + next: () => { + this.userService + .get(true) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(); + + this.fetchPlatforms(); + } + }); + } + + private fetchPlatforms() { + this.adminService + .fetchPlatforms() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((platforms) => { + this.platforms = platforms; + this.dataSource = new MatTableDataSource(platforms); + this.dataSource.sort = this.sort; + this.dataSource.sortingDataAccessor = get; + + this.changeDetectorRef.markForCheck(); + }); + } + + private openCreatePlatformDialog() { + const dialogRef = this.dialog.open(CreateOrUpdatePlatformDialog, { + data: { + platform: { + name: null, + url: null + } + }, + + height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', + width: this.deviceType === 'mobile' ? '100vw' : '50rem' + }); + + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((data) => { + const platform: CreatePlatformDto = data?.platform; + + if (platform) { + this.adminService + .postPlatform(platform) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe({ + next: () => { + this.userService + .get(true) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(); + + this.fetchPlatforms(); + } + }); + } + + this.router.navigate(['.'], { relativeTo: this.route }); + }); + } + + private openUpdatePlatformDialog({ id, name, url }) { + const dialogRef = this.dialog.open(CreateOrUpdatePlatformDialog, { + data: { + platform: { + id, + name, + url + } + }, + + height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', + width: this.deviceType === 'mobile' ? '100vw' : '50rem' + }); + + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((data) => { + const platform: UpdatePlatformDto = data?.platform; + + if (platform) { + this.adminService + .putPlatform(platform) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe({ + next: () => { + this.userService + .get(true) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(); + + this.fetchPlatforms(); + } + }); + } + + this.router.navigate(['.'], { relativeTo: this.route }); + }); + } +} diff --git a/apps/client/src/app/components/platform/platform.module.ts b/apps/client/src/app/components/platform/platform.module.ts new file mode 100644 index 000000000..b5906234f --- /dev/null +++ b/apps/client/src/app/components/platform/platform.module.ts @@ -0,0 +1,26 @@ +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { AdminPlatformComponent } from './platform.component'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { MatButtonModule } from '@angular/material/button'; +import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; +import { GfCreateOrUpdatePlatformDialogModule } from './create-or-update-platform-dialog/create-or-update-platform-dialog.module'; +import { MatMenuModule } from '@angular/material/menu'; +import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module'; + +@NgModule({ + declarations: [AdminPlatformComponent], + imports: [ + CommonModule, + GfCreateOrUpdatePlatformDialogModule, + GfSymbolIconModule, + MatButtonModule, + MatMenuModule, + MatSortModule, + MatTableModule, + RouterModule + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GfAdminPlatformModule {} diff --git a/apps/client/src/app/pages/admin/admin-page-routing.module.ts b/apps/client/src/app/pages/admin/admin-page-routing.module.ts index 49ce08ad3..28cd84617 100644 --- a/apps/client/src/app/pages/admin/admin-page-routing.module.ts +++ b/apps/client/src/app/pages/admin/admin-page-routing.module.ts @@ -4,6 +4,7 @@ import { AdminJobsComponent } from '@ghostfolio/client/components/admin-jobs/adm import { AdminMarketDataComponent } from '@ghostfolio/client/components/admin-market-data/admin-market-data.component'; import { AdminOverviewComponent } from '@ghostfolio/client/components/admin-overview/admin-overview.component'; import { AdminUsersComponent } from '@ghostfolio/client/components/admin-users/admin-users.component'; +import { AdminPlatformComponent } from '@ghostfolio/client/components/platform/platform.component'; import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; import { AdminPageComponent } from './admin-page.component'; @@ -24,7 +25,16 @@ const routes: Routes = [ component: AdminOverviewComponent, title: $localize`Admin Control` }, - { path: 'users', component: AdminUsersComponent, title: $localize`Users` } + { + path: 'users', + component: AdminUsersComponent, + title: $localize`Users` + }, + { + path: 'platforms', + component: AdminPlatformComponent, + title: $localize`Platforms` + } ], component: AdminPageComponent, path: '' diff --git a/apps/client/src/app/pages/admin/admin-page.component.ts b/apps/client/src/app/pages/admin/admin-page.component.ts index 231dc068c..912c49077 100644 --- a/apps/client/src/app/pages/admin/admin-page.component.ts +++ b/apps/client/src/app/pages/admin/admin-page.component.ts @@ -31,6 +31,11 @@ export class AdminPageComponent implements OnDestroy, OnInit { path: 'overview' }, { iconName: 'people-outline', label: $localize`Users`, path: 'users' }, + { + iconName: 'briefcase-outline', + label: $localize`Platforms`, + path: 'platforms' + }, { iconName: 'server-outline', label: $localize`Market Data`, diff --git a/apps/client/src/app/pages/admin/admin-page.module.ts b/apps/client/src/app/pages/admin/admin-page.module.ts index fa7f0e35b..ba37d81a5 100644 --- a/apps/client/src/app/pages/admin/admin-page.module.ts +++ b/apps/client/src/app/pages/admin/admin-page.module.ts @@ -5,6 +5,7 @@ import { GfAdminJobsModule } from '@ghostfolio/client/components/admin-jobs/admi import { GfAdminMarketDataModule } from '@ghostfolio/client/components/admin-market-data/admin-market-data.module'; import { GfAdminOverviewModule } from '@ghostfolio/client/components/admin-overview/admin-overview.module'; import { GfAdminUsersModule } from '@ghostfolio/client/components/admin-users/admin-users.module'; +import { GfAdminPlatformModule } from '@ghostfolio/client/components/platform/platform.module'; import { CacheService } from '@ghostfolio/client/services/cache.service'; import { AdminPageRoutingModule } from './admin-page-routing.module'; @@ -19,6 +20,7 @@ import { AdminPageComponent } from './admin-page.component'; GfAdminJobsModule, GfAdminMarketDataModule, GfAdminOverviewModule, + GfAdminPlatformModule, GfAdminUsersModule, MatTabsModule ], diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index 50e66fd39..7d26668e8 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -2,6 +2,8 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto'; import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto'; +import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto'; +import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { @@ -10,7 +12,7 @@ import { EnhancedSymbolProfile, UniqueAsset } from '@ghostfolio/common/interfaces'; -import { DataSource, MarketData } from '@prisma/client'; +import { DataSource, MarketData, Platform } from '@prisma/client'; import { JobStatus } from 'bull'; import { format, parseISO } from 'date-fns'; import { Observable, map } from 'rxjs'; @@ -37,6 +39,10 @@ export class AdminService { }); } + public deletePlatform(aId: string) { + return this.http.delete(`/api/v1/platform/${aId}`); + } + public deleteProfileData({ dataSource, symbol }: UniqueAsset) { return this.http.delete( `/api/v1/admin/profile-data/${dataSource}/${symbol}` @@ -74,6 +80,10 @@ export class AdminService { }); } + public fetchPlatforms() { + return this.http.get('/api/v1/platform'); + } + public gather7Days() { return this.http.post('/api/v1/admin/gather', {}); } @@ -138,6 +148,10 @@ export class AdminService { ); } + public postPlatform(aPlatform: CreatePlatformDto) { + return this.http.post(`/api/v1/platform`, aPlatform); + } + public putMarketData({ dataSource, date, @@ -156,4 +170,11 @@ export class AdminService { return this.http.put(url, marketData); } + + public putPlatform(aPlatform: UpdatePlatformDto) { + return this.http.put( + `/api/v1/platform/${aPlatform.id}`, + aPlatform + ); + } } diff --git a/libs/common/src/lib/permissions.ts b/libs/common/src/lib/permissions.ts index 46c78acd0..30bd627bc 100644 --- a/libs/common/src/lib/permissions.ts +++ b/libs/common/src/lib/permissions.ts @@ -6,11 +6,13 @@ export const permissions = { createAccess: 'createAccess', createAccount: 'createAccount', createOrder: 'createOrder', + createPlatform: 'createPlatform', createUserAccount: 'createUserAccount', deleteAccess: 'deleteAccess', deleteAccount: 'deleteAcccount', deleteAuthDevice: 'deleteAuthDevice', deleteOrder: 'deleteOrder', + deletePlatform: 'deletePlatform', deleteUser: 'deleteUser', enableFearAndGreedIndex: 'enableFearAndGreedIndex', enableImport: 'enableImport', @@ -26,6 +28,7 @@ export const permissions = { updateAccount: 'updateAccount', updateAuthDevice: 'updateAuthDevice', updateOrder: 'updateOrder', + updatePlatform: 'updatePlatform', updateUserSettings: 'updateUserSettings', updateViewMode: 'updateViewMode' }; @@ -38,14 +41,17 @@ export function getPermissions(aRole: Role): string[] { permissions.createAccess, permissions.createAccount, permissions.createOrder, + permissions.createPlatform, permissions.deleteAccess, permissions.deleteAccount, permissions.deleteAuthDevice, permissions.deleteOrder, + permissions.deletePlatform, permissions.deleteUser, permissions.updateAccount, permissions.updateAuthDevice, permissions.updateOrder, + permissions.updatePlatform, permissions.updateUserSettings, permissions.updateViewMode ];