From 2016f54cd754fec19af615787adc9888a4446a44 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:28:37 +0200 Subject: [PATCH] Extend user account deletion to users without security token --- apps/api/src/app/user/user.controller.ts | 16 ++++++++++++---- apps/api/src/app/user/user.service.ts | 15 ++++++++++++++- .../user-account-settings.component.ts | 6 ++++++ .../user-account-settings.html | 14 +++++++++++++- libs/common/src/lib/config.ts | 2 ++ libs/common/src/lib/dtos/delete-own-user.dto.ts | 5 +++-- 6 files changed, 50 insertions(+), 8 deletions(-) diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index 32ac353351..31a79eff43 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -75,10 +75,11 @@ export class UserController { public async deleteOwnUser( @Body() data: DeleteOwnUserDto ): Promise { - const user = await this.validateAccessToken( - data.accessToken, - this.request.user.id - ); + // Only users of the anonymous provider have a security token + const user = + this.request.user.provider === 'ANONYMOUS' + ? await this.validateAccessToken(data.accessToken, this.request.user.id) + : this.request.user; return this.userService.deleteUser({ id: user.id @@ -260,6 +261,13 @@ export class UserController { accessToken: string, userId: string ): Promise { + if (!accessToken) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + const hashedAccessToken = this.userService.createAccessToken({ password: accessToken, salt: this.configurationService.get('ACCESS_TOKEN_SALT') diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 2e54a7ddc3..e3b8651831 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -14,6 +14,7 @@ import { DEFAULT_CURRENCY, DEFAULT_DATE_RANGE, DEFAULT_LOCALE, + DELETE_OWN_USER_PERIOD, PROPERTY_API_KEY_GHOSTFOLIO, PROPERTY_IS_READ_ONLY_MODE, PROPERTY_MAX_DAILY_REQUESTS, @@ -45,7 +46,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { InjectThrottlerStorage, ThrottlerStorage } from '@nestjs/throttler'; import { Prisma, Role, User } from '@prisma/client'; -import { differenceInDays, subDays } from 'date-fns'; +import { addMilliseconds, differenceInDays, isBefore, subDays } from 'date-fns'; import { isNil, without } from 'lodash-es'; import { createHmac } from 'node:crypto'; @@ -438,6 +439,18 @@ export class UserService { currentPermissions.push(permissions.enableSubscriptionInterstitial); } + // Users without a security token (e.g. Google) can close their user + // account themselves only within a period after the registration + if ( + user.provider !== 'ANONYMOUS' && + isBefore( + new Date(), + addMilliseconds(user.createdAt, DELETE_OWN_USER_PERIOD) + ) + ) { + currentPermissions.push(permissions.deleteOwnUser); + } + currentPermissions = without( currentPermissions, permissions.accessHoldingsChart, 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 83f7ddd9ae..39696e7708 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 @@ -94,6 +94,7 @@ export class GfUserAccountSettingsComponent implements OnInit { protected hasImpersonationId: boolean; protected hasPermissionToDeleteOwnUser: boolean; protected hasPermissionToRequestOwnUserDeletion: boolean; + protected hasPermissionToUpdateOwnAccessToken: boolean; protected hasPermissionToUpdateViewMode: boolean; protected hasPermissionToUpdateUserSettings: boolean; protected isAccessTokenHidden = true; @@ -194,6 +195,11 @@ export class GfUserAccountSettingsComponent implements OnInit { permissions.requestOwnUserDeletion ); + this.hasPermissionToUpdateOwnAccessToken = hasPermission( + this.user.permissions, + permissions.updateOwnAccessToken + ); + this.hasPermissionToUpdateUserSettings = hasPermission( this.user.permissions, permissions.updateUserSettings 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 2d4c090dd2..b1dbfdd415 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 @@ -318,7 +318,10 @@
Danger Zone
- @if (hasPermissionToDeleteOwnUser) { + @if ( + hasPermissionToDeleteOwnUser && + hasPermissionToUpdateOwnAccessToken + ) {
+ } @else if (hasPermissionToDeleteOwnUser) { + } @else if ( hasPermissionToRequestOwnUserDeletion && (user?.accounts?.length > 0 || user?.activitiesCount > 0) diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 73bf5cc00c..a60a377dd0 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -198,6 +198,8 @@ export const DEFAULT_REDACTED_PATHS = [ 'valueInBaseCurrency' ]; +export const DELETE_OWN_USER_PERIOD = ms('2 weeks'); + // USX is handled separately export const DERIVED_CURRENCIES = [ { diff --git a/libs/common/src/lib/dtos/delete-own-user.dto.ts b/libs/common/src/lib/dtos/delete-own-user.dto.ts index 1e3f940cb1..bf1868c6ec 100644 --- a/libs/common/src/lib/dtos/delete-own-user.dto.ts +++ b/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; }