Browse Source

Merge 1043e37ad0 into c90bb1dfe5

pull/7450/merge
Thomas Kaul 3 weeks ago
committed by GitHub
parent
commit
616375c658
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 25
      apps/api/src/app/user/user.controller.ts
  2. 8
      apps/api/src/app/user/user.service.ts
  3. 18
      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

@ -69,13 +69,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
});
}
@ -253,4 +254,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

@ -118,7 +118,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,
@ -202,6 +202,7 @@ export class UserService {
activitiesCount,
id,
permissions,
provider,
referralPartners,
subscription,
systemMessage,
@ -503,8 +504,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);
}

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

@ -86,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 isLoading = true;
protected isWebAuthnEnabled: boolean;
@ -108,6 +109,7 @@ export class GfUserAccountSettingsComponent implements OnInit {
'uk',
'zh'
];
protected mustRemoveDataBeforeClosingAccount: boolean;
protected user: User;
private readonly changeDetectorRef = inject(ChangeDetectorRef);
@ -163,6 +165,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
@ -225,12 +235,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

@ -293,7 +293,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"
@ -338,17 +357,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