Browse Source

Close own account with third-party authentication provider

pull/7450/head
Thomas Kaul 1 month ago
parent
commit
1043e37ad0
  1. 25
      apps/api/src/app/user/user.controller.ts
  2. 8
      apps/api/src/app/user/user.service.ts
  3. 37
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  4. 32
      apps/client/src/app/components/user-account-settings/user-account-settings.html
  5. 5
      libs/common/src/lib/dtos/delete-own-user.dto.ts
  6. 3
      libs/common/src/lib/interfaces/user.interface.ts

25
apps/api/src/app/user/user.controller.ts

@ -67,13 +67,14 @@ export class UserController {
public async deleteOwnUser(
@Body() data: DeleteOwnUserDto
): Promise<UserModel> {
const user = await this.validateAccessToken(
data.accessToken,
this.request.user.id
);
if (this.request.user.provider === 'ANONYMOUS') {
await this.validateAccessToken(data.accessToken, this.request.user.id);
} else {
await this.validateUserHasNoData(this.request.user.id);
}
return this.userService.deleteUser({
id: user.id
id: this.request.user.id
});
}
@ -243,4 +244,18 @@ export class UserController {
return user;
}
private async validateUserHasNoData(userId: string) {
const [accountsCount, activitiesCount] = await Promise.all([
this.prismaService.account.count({ where: { userId } }),
this.prismaService.order.count({ where: { userId } })
]);
if (accountsCount > 0 || activitiesCount > 0) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
}
}

8
apps/api/src/app/user/user.service.ts

@ -110,7 +110,7 @@ export class UserService {
locale?: string;
user: UserWithSettings;
}): Promise<IUser> {
const { id, permissions, settings, subscription } = user;
const { id, permissions, provider, settings, subscription } = user;
const [
access,
@ -193,6 +193,7 @@ export class UserService {
activitiesCount,
id,
permissions,
provider,
referralPartners,
subscription,
systemMessage,
@ -462,8 +463,11 @@ export class UserService {
let currentPermissions = getPermissions(user.role);
if (user.provider === 'ANONYMOUS') {
if (!hasRole(user, Role.DEMO)) {
currentPermissions.push(permissions.deleteOwnUser);
}
if (user.provider === 'ANONYMOUS') {
currentPermissions.push(permissions.updateOwnAccessToken);
}

37
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts

@ -5,7 +5,10 @@ import {
} from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { E_MAIL_LINE_BREAK } from '@ghostfolio/common/config';
import {
DEFAULT_LANGUAGE_CODE,
E_MAIL_LINE_BREAK
} from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { downloadAsFile } from '@ghostfolio/common/helper';
import { User } from '@ghostfolio/common/interfaces';
@ -83,6 +86,7 @@ export class GfUserAccountSettingsComponent implements OnInit {
protected hasPermissionToUpdateViewMode: boolean;
protected hasPermissionToUpdateUserSettings: boolean;
protected isAccessTokenHidden = true;
protected isAnonymousAuthenticationProvider: boolean;
protected readonly isFingerprintSupported = this.doesBrowserSupportAuthn();
protected isWebAuthnEnabled: boolean;
protected readonly language = document.documentElement.lang;
@ -104,6 +108,7 @@ export class GfUserAccountSettingsComponent implements OnInit {
'uk',
'zh'
];
protected mustRemoveDataBeforeClosingAccount: boolean;
protected user: User;
private readonly changeDetectorRef = inject(ChangeDetectorRef);
@ -127,6 +132,14 @@ export class GfUserAccountSettingsComponent implements OnInit {
if (state?.user) {
this.user = state.user;
const userDetailUrl = [
window.location.origin,
DEFAULT_LANGUAGE_CODE,
internalRoutes.adminControl.path,
internalRoutes.adminControl.subRoutes.users.path,
this.user.id
].join('/');
this.closeUserAccountMailHref = `mailto:hi@ghostfol.io?subject=Delete Account&body=${[
'Hello',
'',
@ -134,7 +147,11 @@ export class GfUserAccountSettingsComponent implements OnInit {
'',
`User ID: ${this.user.id}`,
'',
'Kind regards'
'Kind regards',
'',
'',
'---',
userDetailUrl
].join(E_MAIL_LINE_BREAK)}`;
this.hasPermissionToDeleteOwnUser = hasPermission(
@ -147,6 +164,14 @@ export class GfUserAccountSettingsComponent implements OnInit {
permissions.requestOwnUserDeletion
);
this.isAnonymousAuthenticationProvider =
this.user.provider === 'ANONYMOUS';
this.mustRemoveDataBeforeClosingAccount =
(!this.isAnonymousAuthenticationProvider ||
this.hasPermissionToRequestOwnUserDeletion) &&
(this.user.accounts?.length > 0 || this.user.activitiesCount > 0);
this.hasPermissionToUpdateUserSettings = hasPermission(
this.user.permissions,
permissions.updateUserSettings
@ -207,12 +232,16 @@ export class GfUserAccountSettingsComponent implements OnInit {
confirmFn: () => {
this.dataService
.deleteOwnUser({
accessToken: this.deleteOwnUserForm.controls.accessToken.value
accessToken: this.isAnonymousAuthenticationProvider
? this.deleteOwnUserForm.controls.accessToken.value
: undefined
})
.pipe(
catchError(() => {
this.notificationService.alert({
title: $localize`Oops! Incorrect Security Token.`
title: this.isAnonymousAuthenticationProvider
? $localize`Oops! Incorrect Security Token.`
: $localize`Oops! Your account could not be closed.`
});
return EMPTY;

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

@ -292,7 +292,26 @@
<div class="d-flex py-1">
<div class="pr-1 text-danger w-50" i18n>Danger Zone</div>
<div class="pl-1 w-50">
@if (hasPermissionToDeleteOwnUser) {
@if (mustRemoveDataBeforeClosingAccount) {
<button color="warn" i18n mat-flat-button [disabled]="true">
Close Account
</button>
<div class="hint-text mt-2 text-muted" i18n>
For security reasons, please delete all activities and accounts
first before your Ghostfolio account can be closed.
</div>
} @else if (
hasPermissionToDeleteOwnUser && !isAnonymousAuthenticationProvider
) {
<button
color="warn"
i18n
mat-flat-button
(click)="onCloseAccount()"
>
Close Account
</button>
} @else if (hasPermissionToDeleteOwnUser) {
<form
class="w-100"
[formGroup]="deleteOwnUserForm"
@ -337,17 +356,6 @@
Close Account
</button>
</form>
} @else if (
hasPermissionToRequestOwnUserDeletion &&
(user?.accounts?.length > 0 || user?.activitiesCount > 0)
) {
<button color="warn" i18n mat-flat-button [disabled]="true">
Close Account
</button>
<div class="hint-text mt-2 text-muted" i18n>
For security reasons, please delete all activities and accounts
first before your Ghostfolio account can be closed.
</div>
} @else if (hasPermissionToRequestOwnUserDeletion) {
<a
color="warn"

5
libs/common/src/lib/dtos/delete-own-user.dto.ts

@ -1,6 +1,7 @@
import { IsString } from 'class-validator';
import { IsOptional, IsString } from 'class-validator';
export class DeleteOwnUserDto {
@IsOptional()
@IsString()
accessToken: string;
accessToken?: string;
}

3
libs/common/src/lib/interfaces/user.interface.ts

@ -1,7 +1,7 @@
import { SubscriptionType } from '@ghostfolio/common/enums';
import { AccountWithPlatform } from '@ghostfolio/common/types';
import { Access, Tag } from '@prisma/client';
import { Access, Provider, Tag } from '@prisma/client';
import { ReferralPartner } from './referral-partner.interface';
import { SubscriptionOffer } from './subscription-offer.interface';
@ -16,6 +16,7 @@ export interface User {
dateOfFirstActivity: Date;
id: string;
permissions: string[];
provider: Provider;
referralPartners?: ReferralPartner[];
settings: UserSettings;
systemMessage?: SystemMessage;

Loading…
Cancel
Save