Feature/allow to delete users (#64)

* Allow to delete users

* Update changelog
pull/65/head
Thomas 3 years ago committed by GitHub
parent a84256dc03
commit 163f4a3d3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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<UserModel> {
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<User> {

@ -163,6 +163,28 @@ export class UserService {
}
public async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
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
});

@ -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<void>();
@ -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();
}
);
}
}

@ -82,6 +82,7 @@
<th class="mat-header-cell pr-2 py-2" i18n>Transactions</th>
<th class="mat-header-cell pr-2 py-2" i18n>Engagement</th>
<th class="mat-header-cell pr-3 py-2" i18n>Last Activitiy</th>
<th class="mat-header-cell pr-3 py-2"></th>
</tr>
</thead>
<tbody>
@ -102,6 +103,26 @@
<td class="mat-cell pr-3 py-2">
{{ formatDistanceToNow(userItem.Analytics?.updatedAt) }}
</td>
<td class="mat-cell pr-3 py-2">
<button
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="accountMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-vertical"></ion-icon>
</button>
<mat-menu #accountMenu="matMenu" xPosition="before">
<button
i18n
mat-menu-item
[disabled]="userItem.id === user?.id"
(click)="onDeleteUser(userItem.id)"
>
Delete
</button>
</mat-menu>
</td>
</tr>
</tbody>
</table>

@ -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]

@ -48,6 +48,10 @@ export class DataService {
return this.http.delete<any>(`/api/order/${aId}`);
}
public deleteUser(aId: string) {
return this.http.delete<any>(`/api/user/${aId}`);
}
public fetchAccesses() {
return this.http.get<Access[]>('/api/access');
}

@ -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,

Loading…
Cancel
Save