diff --git a/CHANGELOG.md b/CHANGELOG.md index 76892389a..8abfe092e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support for deleting users in the admin control panel + ### Changed - Eliminated the platform attribute from the transaction model diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index c843c1ec4..bf8258586 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -3,6 +3,7 @@ import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper'; import { Body, Controller, + Delete, Get, HttpException, Inject, @@ -21,6 +22,7 @@ import { UserItem } from './interfaces/user-item.interface'; import { User } from './interfaces/user.interface'; import { UpdateUserSettingsDto } from './update-user-settings.dto'; import { UserService } from './user.service'; +import { User as UserModel } from '@prisma/client'; @Controller('user') export class UserController { @@ -30,6 +32,27 @@ export class UserController { private readonly userService: UserService ) {} + @Delete(':id') + @UseGuards(AuthGuard('jwt')) + public async deleteUser(@Param('id') id: string): Promise { + if ( + !hasPermission( + getPermissions(this.request.user.role), + permissions.deleteUser + ) || + id === this.request.user.id + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + return this.userService.deleteUser({ + id + }); + } + @Get() @UseGuards(AuthGuard('jwt')) public async getUser(@Param('id') id: string): Promise { diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index d35f9c9d3..34d34cac9 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -163,6 +163,28 @@ export class UserService { } public async deleteUser(where: Prisma.UserWhereUniqueInput): Promise { + await this.prisma.access.deleteMany({ + where: { OR: [{ granteeUserId: where.id }, { userId: where.id }] } + }); + + await this.prisma.account.deleteMany({ + where: { userId: where.id } + }); + + await this.prisma.analytics.delete({ + where: { userId: where.id } + }); + + await this.prisma.order.deleteMany({ + where: { userId: where.id } + }); + + try { + await this.prisma.settings.delete({ + where: { userId: where.id } + }); + } catch {} + return this.prisma.user.delete({ where }); 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 d37483972..d00a28d0e 100644 --- a/apps/client/src/app/pages/admin/admin-page.component.ts +++ b/apps/client/src/app/pages/admin/admin-page.component.ts @@ -1,8 +1,10 @@ import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { AdminData } from '@ghostfolio/api/app/admin/interfaces/admin-data.interface'; +import { User } from '@ghostfolio/api/app/user/interfaces/user.interface'; import { AdminService } from '@ghostfolio/client/services/admin.service'; import { CacheService } from '@ghostfolio/client/services/cache.service'; import { DataService } from '@ghostfolio/client/services/data.service'; +import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/helper'; import { formatDistanceToNow, isValid, parseISO, sub } from 'date-fns'; import { Subject } from 'rxjs'; @@ -20,6 +22,7 @@ export class AdminPageComponent implements OnInit { public lastDataGathering: string; public transactionCount: number; public userCount: number; + public user: User; public users: AdminData['users']; private unsubscribeSubject = new Subject(); @@ -31,46 +34,24 @@ export class AdminPageComponent implements OnInit { private adminService: AdminService, private cacheService: CacheService, private cd: ChangeDetectorRef, - private dataService: DataService + private dataService: DataService, + private tokenStorageService: TokenStorageService ) {} /** * Initializes the controller */ public ngOnInit() { - this.dataService - .fetchAdminData() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe( - ({ - exchangeRates, - lastDataGathering, - transactionCount, - userCount, - users - }) => { - this.exchangeRates = exchangeRates; - this.users = users; - - if (isValid(parseISO(lastDataGathering?.toString()))) { - this.lastDataGathering = formatDistanceToNow( - new Date(lastDataGathering), - { - addSuffix: true - } - ); - } else if (lastDataGathering === 'IN_PROGRESS') { - this.dataGatheringInProgress = true; - } else { - this.lastDataGathering = '-'; - } - - this.transactionCount = transactionCount; - this.userCount = userCount; + this.fetchAdminData(); - this.cd.markForCheck(); - } - ); + this.tokenStorageService + .onChangeHasToken() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.dataService.fetchUser().subscribe((user) => { + this.user = user; + }); + }); } public onFlushCache() { @@ -112,8 +93,56 @@ export class AdminPageComponent implements OnInit { return ''; } + public onDeleteUser(aId: string) { + const confirmation = confirm('Do you really want to delete this user?'); + + if (confirmation) { + this.dataService.deleteUser(aId).subscribe({ + next: () => { + this.fetchAdminData(); + } + }); + } + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); } + + private fetchAdminData() { + this.dataService + .fetchAdminData() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe( + ({ + exchangeRates, + lastDataGathering, + transactionCount, + userCount, + users + }) => { + this.exchangeRates = exchangeRates; + this.users = users; + + if (isValid(parseISO(lastDataGathering?.toString()))) { + this.lastDataGathering = formatDistanceToNow( + new Date(lastDataGathering), + { + addSuffix: true + } + ); + } else if (lastDataGathering === 'IN_PROGRESS') { + this.dataGatheringInProgress = true; + } else { + this.lastDataGathering = '-'; + } + + this.transactionCount = transactionCount; + this.userCount = userCount; + + this.cd.markForCheck(); + } + ); + } } diff --git a/apps/client/src/app/pages/admin/admin-page.html b/apps/client/src/app/pages/admin/admin-page.html index af77e5b8a..3f3cf89dd 100644 --- a/apps/client/src/app/pages/admin/admin-page.html +++ b/apps/client/src/app/pages/admin/admin-page.html @@ -82,6 +82,7 @@ Transactions Engagement Last Activitiy + @@ -102,6 +103,26 @@ {{ formatDistanceToNow(userItem.Analytics?.updatedAt) }} + + + + + + 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 dae85ecea..afcd6db61 100644 --- a/apps/client/src/app/pages/admin/admin-page.module.ts +++ b/apps/client/src/app/pages/admin/admin-page.module.ts @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; +import { MatMenuModule } from '@angular/material/menu'; import { CacheService } from '@ghostfolio/client/services/cache.service'; import { AdminPageRoutingModule } from './admin-page-routing.module'; @@ -14,7 +15,8 @@ import { AdminPageComponent } from './admin-page.component'; AdminPageRoutingModule, CommonModule, MatButtonModule, - MatCardModule + MatCardModule, + MatMenuModule ], providers: [CacheService], schemas: [CUSTOM_ELEMENTS_SCHEMA] diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index d7232227d..625089fe1 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -48,6 +48,10 @@ export class DataService { return this.http.delete(`/api/order/${aId}`); } + public deleteUser(aId: string) { + return this.http.delete(`/api/user/${aId}`); + } + public fetchAccesses() { return this.http.get('/api/access'); } diff --git a/libs/helper/src/lib/permissions.ts b/libs/helper/src/lib/permissions.ts index 8964d770d..dc14cbb5a 100644 --- a/libs/helper/src/lib/permissions.ts +++ b/libs/helper/src/lib/permissions.ts @@ -12,6 +12,7 @@ export const permissions = { createUserAccount: 'createUserAccount', deleteAccount: 'deleteAcccount', deleteOrder: 'deleteOrder', + deleteUser: 'deleteUser', enableSocialLogin: 'enableSocialLogin', enableSubscription: 'enableSubscription', readForeignPortfolio: 'readForeignPortfolio', @@ -36,6 +37,7 @@ export function getPermissions(aRole: Role): string[] { permissions.createOrder, permissions.deleteAccount, permissions.deleteOrder, + permissions.deleteUser, permissions.readForeignPortfolio, permissions.updateAccount, permissions.updateOrder,