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( public async deleteOwnUser(
@Body() data: DeleteOwnUserDto @Body() data: DeleteOwnUserDto
): Promise<UserModel> { ): Promise<UserModel> {
const user = await this.validateAccessToken( if (this.request.user.provider === 'ANONYMOUS') {
data.accessToken, await this.validateAccessToken(data.accessToken, this.request.user.id);
this.request.user.id } else {
); await this.validateUserHasNoData(this.request.user.id);
}
return this.userService.deleteUser({ return this.userService.deleteUser({
id: user.id id: this.request.user.id
}); });
} }
@ -253,4 +254,18 @@ export class UserController {
return user; 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; locale?: string;
user: UserWithSettings; user: UserWithSettings;
}): Promise<IUser> { }): Promise<IUser> {
const { id, permissions, settings, subscription } = user; const { id, permissions, provider, settings, subscription } = user;
const [ const [
access, access,
@ -202,6 +202,7 @@ export class UserService {
activitiesCount, activitiesCount,
id, id,
permissions, permissions,
provider,
referralPartners, referralPartners,
subscription, subscription,
systemMessage, systemMessage,
@ -503,8 +504,11 @@ export class UserService {
let currentPermissions = getPermissions(user.role); let currentPermissions = getPermissions(user.role);
if (user.provider === 'ANONYMOUS') { if (!hasRole(user, Role.DEMO)) {
currentPermissions.push(permissions.deleteOwnUser); currentPermissions.push(permissions.deleteOwnUser);
}
if (user.provider === 'ANONYMOUS') {
currentPermissions.push(permissions.updateOwnAccessToken); 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 hasPermissionToUpdateViewMode: boolean;
protected hasPermissionToUpdateUserSettings: boolean; protected hasPermissionToUpdateUserSettings: boolean;
protected isAccessTokenHidden = true; protected isAccessTokenHidden = true;
protected isAnonymousAuthenticationProvider: boolean;
protected readonly isFingerprintSupported = this.doesBrowserSupportAuthn(); protected readonly isFingerprintSupported = this.doesBrowserSupportAuthn();
protected isLoading = true; protected isLoading = true;
protected isWebAuthnEnabled: boolean; protected isWebAuthnEnabled: boolean;
@ -108,6 +109,7 @@ export class GfUserAccountSettingsComponent implements OnInit {
'uk', 'uk',
'zh' 'zh'
]; ];
protected mustRemoveDataBeforeClosingAccount: boolean;
protected user: User; protected user: User;
private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly changeDetectorRef = inject(ChangeDetectorRef);
@ -163,6 +165,14 @@ export class GfUserAccountSettingsComponent implements OnInit {
permissions.requestOwnUserDeletion 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.hasPermissionToUpdateUserSettings = hasPermission(
this.user.permissions, this.user.permissions,
permissions.updateUserSettings permissions.updateUserSettings
@ -225,12 +235,16 @@ export class GfUserAccountSettingsComponent implements OnInit {
confirmFn: () => { confirmFn: () => {
this.dataService this.dataService
.deleteOwnUser({ .deleteOwnUser({
accessToken: this.deleteOwnUserForm.controls.accessToken.value accessToken: this.isAnonymousAuthenticationProvider
? this.deleteOwnUserForm.controls.accessToken.value
: undefined
}) })
.pipe( .pipe(
catchError(() => { catchError(() => {
this.notificationService.alert({ 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; 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="d-flex py-1">
<div class="pr-1 text-danger w-50" i18n>Danger Zone</div> <div class="pr-1 text-danger w-50" i18n>Danger Zone</div>
<div class="pl-1 w-50"> <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 <form
class="w-100" class="w-100"
[formGroup]="deleteOwnUserForm" [formGroup]="deleteOwnUserForm"
@ -338,17 +357,6 @@
Close Account Close Account
</button> </button>
</form> </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) { } @else if (hasPermissionToRequestOwnUserDeletion) {
<a <a
color="warn" 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 { export class DeleteOwnUserDto {
@IsOptional()
@IsString() @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 { SubscriptionType } from '@ghostfolio/common/enums';
import { AccountWithPlatform } from '@ghostfolio/common/types'; 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 { ReferralPartner } from './referral-partner.interface';
import { SubscriptionOffer } from './subscription-offer.interface'; import { SubscriptionOffer } from './subscription-offer.interface';
@ -16,6 +16,7 @@ export interface User {
dateOfFirstActivity: Date; dateOfFirstActivity: Date;
id: string; id: string;
permissions: string[]; permissions: string[];
provider: Provider;
referralPartners?: ReferralPartner[]; referralPartners?: ReferralPartner[];
settings: UserSettings; settings: UserSettings;
systemMessage?: SystemMessage; systemMessage?: SystemMessage;

Loading…
Cancel
Save