From 37538eb71014421416b28cdbeac657dbc3d6154e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Marinho?= Date: Wed, 29 May 2024 19:35:34 +0100 Subject: [PATCH] Feature/add to close user account --- apps/api/src/app/user/user.controller.ts | 30 ++++++++++ .../user-account-settings.component.ts | 39 +++++++++++++ .../user-account-settings.html | 58 +++++++++++++++++++ .../user-account-settings.module.ts | 7 ++- .../user-account-settings.scss | 9 +++ apps/client/src/app/services/data.service.ts | 4 ++ libs/common/src/lib/permissions.ts | 3 + 7 files changed, 148 insertions(+), 2 deletions(-) diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index 39e78dcdc..05642119e 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -1,5 +1,6 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { User, UserSettings } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; @@ -32,6 +33,7 @@ import { UserService } from './user.service'; @Controller('user') export class UserController { public constructor( + private readonly configurationService: ConfigurationService, private readonly jwtService: JwtService, private readonly propertyService: PropertyService, @Inject(REQUEST) private readonly request: RequestWithUser, @@ -54,6 +56,34 @@ export class UserController { }); } + @Delete('self/:accessToken') + @HasPermission(permissions.deleteOwnUser) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + public async deleteOwnUser( + @Param('accessToken') accessToken: string + ): Promise { + const hashedAccessToken = this.userService.createAccessToken( + accessToken, + this.configurationService.get('ACCESS_TOKEN_SALT') + ); + + const [user] = await this.userService.users({ + where: { accessToken: hashedAccessToken } + }); + + if (!user) { + throw new HttpException( + getReasonPhrase(StatusCodes.NOT_FOUND), + StatusCodes.NOT_FOUND + ); + } + + return this.userService.deleteUser({ + id: user.id, + accessToken: hashedAccessToken + }); + } + @Get() @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getUser( diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts index c6d7f8e89..0eda31586 100644 --- a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts +++ b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts @@ -4,6 +4,7 @@ import { KEY_TOKEN, SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service'; +import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { downloadAsFile } from '@ghostfolio/common/helper'; @@ -17,6 +18,7 @@ import { OnDestroy, OnInit } from '@angular/core'; +import { FormBuilder, Validators } from '@angular/forms'; import { MatSlideToggleChange } from '@angular/material/slide-toggle'; import { format, parseISO } from 'date-fns'; import { uniq } from 'lodash'; @@ -33,8 +35,13 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit { public appearancePlaceholder = $localize`Auto`; public baseCurrency: string; public currencies: string[] = []; + public deleteOwnUserForm = this.formBuilder.group({ + accessToken: ['', Validators.required] + }); + public hasPermissionToDeleteOwnUser: boolean; public hasPermissionToUpdateViewMode: boolean; public hasPermissionToUpdateUserSettings: boolean; + public isAccessTokenHidden = true; public isWebAuthnEnabled: boolean; public language = document.documentElement.lang; public locales = [ @@ -58,7 +65,9 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit { public constructor( private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, + private formBuilder: FormBuilder, private settingsStorageService: SettingsStorageService, + private tokenStorageService: TokenStorageService, private userService: UserService, public webAuthnService: WebAuthnService ) { @@ -83,6 +92,11 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit { permissions.updateViewMode ); + this.hasPermissionToDeleteOwnUser = hasPermission( + this.user.permissions, + permissions.deleteOwnUser + ); + this.locales.push(this.user.settings.locale); this.locales = uniq(this.locales.sort()); @@ -99,6 +113,31 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit { return !(this.language === 'de' || this.language === 'en'); } + public onCloseAccount() { + const confirmation = confirm( + $localize`Do you really want to close your account?` + ); + + const accessToken = this.deleteOwnUserForm.get('accessToken').value; + + if (confirmation) { + this.dataService + .deleteOwnUser(accessToken) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe({ + next: () => { + this.tokenStorageService.signOut(); + this.userService.remove(); + + document.location.href = `/${document.documentElement.lang}`; + }, + error: () => { + alert($localize`Oops! Incorrect Security Token.`); + } + }); + } + } + public onChangeUserSetting(aKey: string, aValue: string) { this.dataService .putUserSetting({ [aKey]: aValue }) diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.html b/apps/client/src/app/components/user-account-settings/user-account-settings.html index 1ad24e22e..1bc9f05af 100644 --- a/apps/client/src/app/components/user-account-settings/user-account-settings.html +++ b/apps/client/src/app/components/user-account-settings/user-account-settings.html @@ -232,6 +232,64 @@ + @if (hasPermissionToDeleteOwnUser) { +
+ @if (hasPermissionToDeleteOwnUser) { +
+
+
Danger Zone
+
+ + Security Token + + + +
+
+ +
+
+
+ +
+
+
+ } + } diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.module.ts b/apps/client/src/app/components/user-account-settings/user-account-settings.module.ts index 89626a96c..e0fe2e1e2 100644 --- a/apps/client/src/app/components/user-account-settings/user-account-settings.module.ts +++ b/apps/client/src/app/components/user-account-settings/user-account-settings.module.ts @@ -1,11 +1,12 @@ import { GfValueComponent } from '@ghostfolio/ui/value'; import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { RouterModule } from '@angular/router'; @@ -22,10 +23,12 @@ import { UserAccountSettingsComponent } from './user-account-settings.component' MatButtonModule, MatCardModule, MatFormFieldModule, + MatInputModule, MatSelectModule, MatSlideToggleModule, ReactiveFormsModule, RouterModule - ] + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class GfUserAccountSettingsModule {} diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.scss b/apps/client/src/app/components/user-account-settings/user-account-settings.scss index 22555407a..684f26ec7 100644 --- a/apps/client/src/app/components/user-account-settings/user-account-settings.scss +++ b/apps/client/src/app/components/user-account-settings/user-account-settings.scss @@ -15,6 +15,15 @@ font-size: 90%; line-height: 1.2; } + + .danger-zone { + &-text { + color: rgba(var(--palette-warn-500), 1); + } + &-hr { + background-color: rgba(var(--palette-warn-500), 1); + } + } } :host-context(.is-dark-theme) { diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 18f1b966d..4a64fd807 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -271,6 +271,10 @@ export class DataService { return this.http.delete(`/api/v1/benchmark/${dataSource}/${symbol}`); } + public deleteOwnUser(accessToken: string) { + return this.http.delete(`/api/v1/user/self/${accessToken}`); + } + public deleteUser(aId: string) { return this.http.delete(`/api/v1/user/${aId}`); } diff --git a/libs/common/src/lib/permissions.ts b/libs/common/src/lib/permissions.ts index 7c8b8ccbe..f742e8209 100644 --- a/libs/common/src/lib/permissions.ts +++ b/libs/common/src/lib/permissions.ts @@ -20,6 +20,7 @@ export const permissions = { deletePlatform: 'deletePlatform', deleteTag: 'deleteTag', deleteUser: 'deleteUser', + deleteOwnUser: 'deleteOwnUser', enableFearAndGreedIndex: 'enableFearAndGreedIndex', enableImport: 'enableImport', enableBlog: 'enableBlog', @@ -56,6 +57,7 @@ export function getPermissions(aRole: Role): string[] { permissions.deleteAccess, permissions.deleteAccount, permissions.deleteAuthDevice, + permissions.deleteOwnUser, permissions.deleteOrder, permissions.deletePlatform, permissions.deleteTag, @@ -79,6 +81,7 @@ export function getPermissions(aRole: Role): string[] { permissions.createAccount, permissions.createAccountBalance, permissions.createOrder, + permissions.deleteOwnUser, permissions.deleteAccess, permissions.deleteAccount, permissions.deleteAccountBalance,