Browse Source

Feature/add to close user account

pull/3444/head
José Marinho 1 year ago
parent
commit
37538eb710
  1. 30
      apps/api/src/app/user/user.controller.ts
  2. 39
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  3. 58
      apps/client/src/app/components/user-account-settings/user-account-settings.html
  4. 7
      apps/client/src/app/components/user-account-settings/user-account-settings.module.ts
  5. 9
      apps/client/src/app/components/user-account-settings/user-account-settings.scss
  6. 4
      apps/client/src/app/services/data.service.ts
  7. 3
      libs/common/src/lib/permissions.ts

30
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<UserModel> {
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(

39
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 })

58
apps/client/src/app/components/user-account-settings/user-account-settings.html

@ -232,6 +232,64 @@
</button>
</div>
</div>
@if (hasPermissionToDeleteOwnUser) {
<hr class="danger-zone-hr" />
@if (hasPermissionToDeleteOwnUser) {
<form
class="w-100"
[formGroup]="deleteOwnUserForm"
(ngSubmit)="onCloseAccount()"
>
<div class="align-items-center d-flex mt-4 py-1">
<div class="pr-1 w-50 danger-zone-text" i18n>Danger Zone</div>
<div class="pl-1 w-50">
<mat-form-field
appearance="outline"
class="without-hint w-100"
color="warn"
>
<mat-label class="danger-zone-text" i18n
>Security Token</mat-label
>
<input
formControlName="accessToken"
matInput
[type]="isAccessTokenHidden ? 'password' : 'text'"
/>
<button
mat-button
matSuffix
type="button"
(click)="isAccessTokenHidden = !isAccessTokenHidden"
>
<ion-icon
[name]="
isAccessTokenHidden ? 'eye-outline' : 'eye-off-outline'
"
/>
</button>
</mat-form-field>
</div>
</div>
<div class="align-items-center d-flex py-1">
<div class="pr-1 w-50"></div>
<div class="pl-1 text-monospace w-50">
<button
color="warn"
mat-flat-button
type="submit"
[disabled]="
!(deleteOwnUserForm.dirty && deleteOwnUserForm.valid)
"
>
<span i18n>Close Account</span>
</button>
</div>
</div>
</form>
}
}
</div>
</div>
</div>

7
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 {}

9
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) {

4
apps/client/src/app/services/data.service.ts

@ -271,6 +271,10 @@ export class DataService {
return this.http.delete<any>(`/api/v1/benchmark/${dataSource}/${symbol}`);
}
public deleteOwnUser(accessToken: string) {
return this.http.delete<any>(`/api/v1/user/self/${accessToken}`);
}
public deleteUser(aId: string) {
return this.http.delete<any>(`/api/v1/user/${aId}`);
}

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

Loading…
Cancel
Save