Compare commits

...

4 Commits

Author SHA1 Message Date
Thomas Kaul 8687b9465a
Release 3.82.0 (#8098) 23 hours ago
Thomas Kaul 20203ec33c
Task/remove unnecessary data source index of market data database table (#8063) 23 hours ago
Thomas Kaul 635d8616b9
Task/refresh cryptocurrencies list (20261008) (#8091) 23 hours ago
Thomas Kaul 8ba968dc07
Feature/user account deletion for Google users (#8093) 23 hours ago
  1. 5
      CHANGELOG.md
  2. 26
      apps/api/src/app/user/user.controller.ts
  3. 14
      apps/api/src/app/user/user.service.ts
  4. 9
      apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
  5. 20
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  6. 14
      apps/client/src/app/components/user-account-settings/user-account-settings.html
  7. 2
      libs/common/src/lib/config.ts
  8. 5
      libs/common/src/lib/dtos/delete-own-user.dto.ts
  9. 4
      package-lock.json
  10. 2
      package.json
  11. 2
      prisma/migrations/20261005010000_removed_index_for_data_source_from_market_data/migration.sql
  12. 1
      prisma/schema.prisma

5
CHANGELOG.md

@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
## 3.82.0 - 2026-10-09
### Added
@ -13,7 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Extended the user account deletion flow in the user settings of the user account page to users without a _Security Token_ within 14 days after the registration
- Improved the _Storybook_ stories of the account selector, accounts table, activities table and holdings table components
- Removed an unnecessary index from the market data database table
- Refreshed the cryptocurrencies list
- Improved the language localization for Spanish (`es`)
### Fixed

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

@ -71,14 +71,21 @@ export class UserController {
@Delete()
@HasPermission(permissions.deleteOwnUser)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
public async deleteOwnUser(
@Body() data: DeleteOwnUserDto
@Body() data: DeleteOwnUserDto,
@Impersonation() { isActive }: ImpersonationContext
): Promise<UserModel> {
const user = await this.validateAccessToken(
data.accessToken,
this.request.user.id
);
const user = this.request.user;
if (user.provider === 'ANONYMOUS') {
await this.validateAccessToken(data.accessToken, user.id);
} else if (isActive) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.userService.deleteUser({
id: user.id
@ -260,6 +267,13 @@ export class UserController {
accessToken: string,
userId: string
): Promise<UserModel> {
if (!accessToken) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const hashedAccessToken = this.userService.createAccessToken({
password: accessToken,
salt: this.configurationService.get('ACCESS_TOKEN_SALT')

14
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,17 @@ export class UserService {
currentPermissions.push(permissions.enableSubscriptionInterstitial);
}
if (
!hasRole(user, Role.DEMO) &&
user.provider !== 'ANONYMOUS' &&
isBefore(
new Date(),
addMilliseconds(user.createdAt, DELETE_OWN_USER_PERIOD)
)
) {
currentPermissions.push(permissions.deleteOwnUser);
}
currentPermissions = without(
currentPermissions,
permissions.accessHoldingsChart,

9
apps/api/src/assets/cryptocurrencies/cryptocurrencies.json

@ -3367,6 +3367,7 @@
"CARV": "CARV",
"CAS": "Cashaa",
"CASH": "Litecash",
"CASHCAT": "Cash Cat",
"CASHCOIN": "CashCoin",
"CASHIO": "Cashio Dollar",
"CASHLY": "Cashly",
@ -10790,7 +10791,7 @@
"MARS": "Marscoin",
"MARS4": "MARS4",
"MARSC": "MarsCoin",
"MARSCOIN": "Marscoin",
"MARSCOIN": "MarsCoin",
"MARSERC": "Mars",
"MARSH": "UnMarshal",
"MARSMI": "MarsMi",
@ -13919,6 +13920,7 @@
"PONKE": "Ponke",
"PONKEBNB": "Ponke BNB",
"PONKEI": "Chinese Ponkei the Original",
"PONS": "Pons",
"PONTEM": "Pontem Liquidswap",
"PONYO": "Ponyo Impact",
"PONZI": "PonziCoin",
@ -14660,6 +14662,7 @@
"RECORD": "Music Protocol",
"RECT": "ReflectionAI",
"RED": "RED",
"RED21707": "RedStone",
"REDC": "RedCab",
"REDCO": "Redcoin",
"REDDIT": "Reddit",
@ -16488,7 +16491,7 @@
"SQUAWK": "Squawk",
"SQUEEZER": "Squeezer",
"SQUIBONK": "SQUIBONK",
"SQUID": "Squid Game",
"SQUID": "SQUIDGames Token",
"SQUID2": "SquidDao",
"SQUIDGROW": "SquidGrow",
"SQUIDGROWV1": "SquidGrow v1",
@ -20250,7 +20253,7 @@
"ZPTC": "Zeptacoin",
"ZRC": "Zircuit",
"ZRCOIN": "ZrCoin",
"ZRO": "Carb0n.fi",
"ZRO": "LayerZero",
"ZRO26997": "LayerZero",
"ZRPY": "Zerpaay",
"ZRS": "Zaros",

20
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
@ -271,13 +277,19 @@ export class GfUserAccountSettingsComponent implements OnInit {
this.notificationService.confirm({
confirmFn: () => {
this.dataService
.deleteOwnUser({
accessToken: this.deleteOwnUserForm.controls.accessToken.value
})
.deleteOwnUser(
this.hasPermissionToUpdateOwnAccessToken
? {
accessToken: this.deleteOwnUserForm.controls.accessToken.value
}
: {}
)
.pipe(
catchError(() => {
this.notificationService.alert({
title: $localize`Oops! Incorrect Security Token.`
title: this.hasPermissionToUpdateOwnAccessToken
? $localize`Oops! Incorrect Security Token.`
: $localize`Oops! Something went wrong.`
});
return EMPTY;

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

@ -318,7 +318,10 @@
<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 (
hasPermissionToDeleteOwnUser &&
hasPermissionToUpdateOwnAccessToken
) {
<form
class="w-100"
[formGroup]="deleteOwnUserForm"
@ -363,6 +366,15 @@
Close Account
</button>
</form>
} @else if (hasPermissionToDeleteOwnUser && !hasImpersonationId) {
<button
color="warn"
i18n
mat-flat-button
(click)="onCloseAccount()"
>
Close Account
</button>
} @else if (
hasPermissionToRequestOwnUserDeletion &&
(user?.accounts?.length > 0 || user?.activitiesCount > 0)

2
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 = [
{

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

4
package-lock.json

@ -1,12 +1,12 @@
{
"name": "ghostfolio",
"version": "3.81.0",
"version": "3.82.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
"version": "3.81.0",
"version": "3.82.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {

2
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "3.81.0",
"version": "3.82.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",

2
prisma/migrations/20261005010000_removed_index_for_data_source_from_market_data/migration.sql

@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX "MarketData_dataSource_idx";

1
prisma/schema.prisma

@ -174,7 +174,6 @@ model MarketData {
symbol String
@@unique([dataSource, symbol, date])
@@index([dataSource])
@@index([date])
@@index([state])
@@index([symbol])

Loading…
Cancel
Save