Browse Source

Merge remote-tracking branch 'origin/main' into feature/extend-holdings-endpoint-for-cash

pull/5650/head
KenTandrian 1 month ago
parent
commit
4d4a9f5e2a
  1. 15
      CHANGELOG.md
  2. 4
      apps/api/src/app/admin/admin.controller.ts
  3. 6
      apps/api/src/app/admin/admin.service.ts
  4. 17
      apps/api/src/app/auth/auth.controller.ts
  5. 37
      apps/api/src/app/auth/auth.service.ts
  6. 3
      apps/api/src/app/auth/google.strategy.ts
  7. 140
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts
  8. 10
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts
  9. 2
      apps/api/src/services/i18n/i18n.service.ts
  10. 1
      apps/api/src/services/interfaces/interfaces.ts
  11. 55
      apps/api/src/services/market-data/market-data.service.ts
  12. 14
      apps/api/src/services/queues/data-gathering/data-gathering.processor.ts
  13. 8
      apps/api/src/services/queues/data-gathering/data-gathering.service.ts
  14. 56
      apps/client/project.json
  15. 8
      apps/client/src/app/components/admin-users/admin-users.component.ts
  16. 18
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts
  17. 11
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html
  18. 4
      apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts
  19. 2
      apps/client/src/app/core/module-preload.service.ts
  20. 14
      apps/client/src/app/pages/portfolio/fire/fire-page.scss
  21. 12
      apps/client/src/app/pages/register/register-page.component.ts
  22. 14
      apps/client/src/app/pages/register/register-page.html
  23. 4
      apps/client/src/app/services/admin.service.ts
  24. 56
      apps/client/src/app/services/internet-identity.service.ts
  25. 28
      apps/client/src/assets/icons/internet-computer.svg
  26. 61
      apps/client/src/styles/theme.scss
  27. 4
      apps/client/src/styles/variables.scss
  28. 5
      eslint.config.cjs
  29. 2
      libs/common/src/lib/helper.ts
  30. 4
      libs/common/src/lib/interfaces/index.ts
  31. 2
      libs/common/src/lib/interfaces/responses/admin-users-response.interface.ts
  32. 2
      libs/ui/src/lib/fire-calculator/fire-calculator.component.ts
  33. 3
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  34. 3
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
  35. 240
      package-lock.json
  36. 13
      package.json
  37. 8
      test/import/not-ok/invalid-data-source.json
  38. 8
      test/import/not-ok/invalid-date-before-min.json
  39. 8
      test/import/not-ok/invalid-date.json
  40. 8
      test/import/not-ok/invalid-symbol.json
  41. 8
      test/import/not-ok/invalid-type.json
  42. 8
      test/import/not-ok/unavailable-exchange-rate.json
  43. 3
      test/import/ok/500-activities.json
  44. 3
      test/import/ok/btceur.json
  45. 3
      test/import/ok/btcusd-short.json
  46. 3
      test/import/ok/btcusd.json
  47. 3
      test/import/ok/derived-currency.json
  48. 3
      test/import/ok/novn-buy-and-sell-partially.json
  49. 3
      test/import/ok/novn-buy-and-sell.json
  50. 3
      test/import/ok/penthouse-apartment.json
  51. 3
      test/import/ok/sample.json
  52. 3
      test/import/ok/vti-buy-long-history.json
  53. 3
      test/import/ok/without-accounts.json

15
CHANGELOG.md

@ -13,8 +13,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Changed the build executor of the client from `@nx/angular:webpack-browser` to `@nx/angular:browser-esbuild`
### Fixed
- Fixed the style of the safe withdrawal rate selector in the _FIRE_ section (experimental)
## 2.214.0 - 2025-11-01
### Changed
- Improved the icon of the _View Holding_ menu item in the activities table - Improved the icon of the _View Holding_ menu item in the activities table
- Ensured atomic data replacement during historical market data gathering
- Removed _Internet Identity_ as a social login provider
- Refreshed the cryptocurrencies list - Refreshed the cryptocurrencies list
- Upgraded `countries-list` from version `3.1.1` to `3.2.0`
- Upgraded `ng-extract-i18n-merge` from version `3.0.0` to `3.1.0`
- Upgraded `twitter-api-v2` from version `1.23.0` to `1.27.0`
## 2.213.0 - 2025-10-30 ## 2.213.0 - 2025-10-30

4
apps/api/src/app/admin/admin.controller.ts

@ -17,7 +17,7 @@ import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { import {
AdminData, AdminData,
AdminMarketData, AdminMarketData,
AdminUsers, AdminUsersResponse,
EnhancedSymbolProfile, EnhancedSymbolProfile,
ScraperConfiguration ScraperConfiguration
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -315,7 +315,7 @@ export class AdminController {
public async getUsers( public async getUsers(
@Query('skip') skip?: number, @Query('skip') skip?: number,
@Query('take') take?: number @Query('take') take?: number
): Promise<AdminUsers> { ): Promise<AdminUsersResponse> {
return this.adminService.getUsers({ return this.adminService.getUsers({
skip: isNaN(skip) ? undefined : skip, skip: isNaN(skip) ? undefined : skip,
take: isNaN(take) ? undefined : take take: isNaN(take) ? undefined : take

6
apps/api/src/app/admin/admin.service.ts

@ -23,7 +23,7 @@ import {
AdminMarketData, AdminMarketData,
AdminMarketDataDetails, AdminMarketDataDetails,
AdminMarketDataItem, AdminMarketDataItem,
AdminUsers, AdminUsersResponse,
AssetProfileIdentifier, AssetProfileIdentifier,
EnhancedSymbolProfile, EnhancedSymbolProfile,
Filter Filter
@ -513,7 +513,7 @@ export class AdminService {
}: { }: {
skip?: number; skip?: number;
take?: number; take?: number;
}): Promise<AdminUsers> { }): Promise<AdminUsersResponse> {
const [count, users] = await Promise.all([ const [count, users] = await Promise.all([
this.countUsersWithAnalytics(), this.countUsersWithAnalytics(),
this.getUsersWithAnalytics({ skip, take }) this.getUsersWithAnalytics({ skip, take })
@ -818,7 +818,7 @@ export class AdminService {
}: { }: {
skip?: number; skip?: number;
take?: number; take?: number;
}): Promise<AdminUsers['users']> { }): Promise<AdminUsersResponse['users']> {
let orderBy: Prisma.Enumerable<Prisma.UserOrderByWithRelationInput> = [ let orderBy: Prisma.Enumerable<Prisma.UserOrderByWithRelationInput> = [
{ createdAt: 'desc' } { createdAt: 'desc' }
]; ];

17
apps/api/src/app/auth/auth.controller.ts

@ -102,23 +102,6 @@ export class AuthController {
} }
} }
@Post('internet-identity')
public async internetIdentityLogin(
@Body() body: { principalId: string }
): Promise<OAuthResponse> {
try {
const authToken = await this.authService.validateInternetIdentityLogin(
body.principalId
);
return { authToken };
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
}
@Get('webauthn/generate-registration-options') @Get('webauthn/generate-registration-options')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async generateRegistrationOptions() { public async generateRegistrationOptions() {

37
apps/api/src/app/auth/auth.service.ts

@ -4,7 +4,6 @@ import { PropertyService } from '@ghostfolio/api/services/property/property.serv
import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { Provider } from '@prisma/client';
import { ValidateOAuthLoginParams } from './interfaces/interfaces'; import { ValidateOAuthLoginParams } from './interfaces/interfaces';
@ -44,42 +43,6 @@ export class AuthService {
}); });
} }
public async validateInternetIdentityLogin(principalId: string) {
try {
const provider: Provider = 'INTERNET_IDENTITY';
let [user] = await this.userService.users({
where: { provider, thirdPartyId: principalId }
});
if (!user) {
const isUserSignupEnabled =
await this.propertyService.isUserSignupEnabled();
if (!isUserSignupEnabled || true) {
throw new Error('Sign up forbidden');
}
// Create new user if not found
user = await this.userService.createUser({
data: {
provider,
thirdPartyId: principalId
}
});
}
return this.jwtService.sign({
id: user.id
});
} catch (error) {
throw new InternalServerErrorException(
'validateInternetIdentityLogin',
error.message
);
}
}
public async validateOAuthLogin({ public async validateOAuthLogin({
provider, provider,
thirdPartyId thirdPartyId

3
apps/api/src/app/auth/google.strategy.ts

@ -3,6 +3,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { Provider } from '@prisma/client'; import { Provider } from '@prisma/client';
import { DoneCallback } from 'passport';
import { Profile, Strategy } from 'passport-google-oauth20'; import { Profile, Strategy } from 'passport-google-oauth20';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
@ -29,7 +30,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
_token: string, _token: string,
_refreshToken: string, _refreshToken: string,
profile: Profile, profile: Profile,
done: Function done: DoneCallback
) { ) {
try { try {
const jwt = await this.authService.validateOAuthLogin({ const jwt = await this.authService.validateOAuthLogin({

140
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts

@ -0,0 +1,140 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
loadExportFile,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js';
import { join } from 'node:path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
() => {
return {
ExchangeRateDataService: jest.fn().mockImplementation(() => {
return ExchangeRateDataServiceMock;
})
};
}
);
jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
};
}
);
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => {
let exportResponse: ExportResponse;
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService;
beforeAll(() => {
exportResponse = loadExportFile(
join(__dirname, '../../../../../../../test/import/ok/btceur.json')
);
});
beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
portfolioSnapshotService = new PortfolioSnapshotService(null);
redisCacheService = new RedisCacheService(null, null);
portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
portfolioSnapshotService,
redisCacheService
);
});
describe('get current positions', () => {
it.only('with BTCUSD buy (in EUR)', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime());
const activities: Activity[] = exportResponse.activities.map(
(activity) => ({
...activityDummyData,
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: 4.46,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
dataSource: activity.dataSource,
name: 'Bitcoin',
symbol: activity.symbol
},
unitPriceInAssetProfileCurrency: 44558.42
})
);
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: 'EUR',
userId: userDummyData.id
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
expect(portfolioSnapshot.positions[0].fee).toEqual(new Big(4.46));
expect(
portfolioSnapshot.positions[0].feeInBaseCurrency.toNumber()
).toBeCloseTo(3.94, 1);
});
});
});

10
apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts

@ -17,11 +17,21 @@ export const ExchangeRateDataServiceMock = {
'2023-07-10': 0.8854 '2023-07-10': 0.8854
} }
}); });
} else if (targetCurrency === 'EUR') {
return Promise.resolve({
EUREUR: {
'2021-12-12': 1
},
USDEUR: {
'2021-12-12': 0.8855
}
});
} else if (targetCurrency === 'USD') { } else if (targetCurrency === 'USD') {
return Promise.resolve({ return Promise.resolve({
USDUSD: { USDUSD: {
'2018-01-01': 1, '2018-01-01': 1,
'2021-11-16': 1, '2021-11-16': 1,
'2021-12-12': 1,
'2023-07-10': 1 '2023-07-10': 1
} }
}); });

2
apps/api/src/services/i18n/i18n.service.ts

@ -65,7 +65,7 @@ export class I18nService {
} }
private parseLanguageCode(aFileName: string) { private parseLanguageCode(aFileName: string) {
const match = aFileName.match(/\.([a-zA-Z]+)\.xlf$/); const match = /\.([a-zA-Z]+)\.xlf$/.exec(aFileName);
return match ? match[1] : DEFAULT_LANGUAGE_CODE; return match ? match[1] : DEFAULT_LANGUAGE_CODE;
} }

1
apps/api/src/services/interfaces/interfaces.ts

@ -20,4 +20,5 @@ export interface DataProviderResponse {
export interface DataGatheringItem extends AssetProfileIdentifier { export interface DataGatheringItem extends AssetProfileIdentifier {
date?: Date; date?: Date;
force?: boolean;
} }

55
apps/api/src/services/market-data/market-data.service.ts

@ -132,6 +132,61 @@ export class MarketDataService {
}); });
} }
/**
* Atomically replace market data for a symbol within a date range.
* Deletes existing data in the range and inserts new data within a single
* transaction to prevent data loss if the operation fails.
*/
public async replaceForSymbol({
data,
dataSource,
symbol
}: AssetProfileIdentifier & { data: Prisma.MarketDataUpdateInput[] }) {
await this.prismaService.$transaction(async (prisma) => {
if (data.length > 0) {
let minTime = Infinity;
let maxTime = -Infinity;
for (const { date } of data) {
const time = (date as Date).getTime();
if (time < minTime) {
minTime = time;
}
if (time > maxTime) {
maxTime = time;
}
}
const minDate = new Date(minTime);
const maxDate = new Date(maxTime);
await prisma.marketData.deleteMany({
where: {
dataSource,
symbol,
date: {
gte: minDate,
lte: maxDate
}
}
});
await prisma.marketData.createMany({
data: data.map(({ date, marketPrice, state }) => ({
dataSource,
symbol,
date: date as Date,
marketPrice: marketPrice as number,
state: state as MarketDataState
})),
skipDuplicates: true
});
}
});
}
public async updateAssetProfileIdentifier( public async updateAssetProfileIdentifier(
oldAssetProfileIdentifier: AssetProfileIdentifier, oldAssetProfileIdentifier: AssetProfileIdentifier,
newAssetProfileIdentifier: AssetProfileIdentifier newAssetProfileIdentifier: AssetProfileIdentifier

14
apps/api/src/services/queues/data-gathering/data-gathering.processor.ts

@ -100,7 +100,7 @@ export class DataGatheringProcessor {
name: GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME name: GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME
}) })
public async gatherHistoricalMarketData(job: Job<DataGatheringItem>) { public async gatherHistoricalMarketData(job: Job<DataGatheringItem>) {
const { dataSource, date, symbol } = job.data; const { dataSource, date, force, symbol } = job.data;
try { try {
let currentDate = parseISO(date as unknown as string); let currentDate = parseISO(date as unknown as string);
@ -109,7 +109,7 @@ export class DataGatheringProcessor {
`Historical market data gathering has been started for ${symbol} (${dataSource}) at ${format( `Historical market data gathering has been started for ${symbol} (${dataSource}) at ${format(
currentDate, currentDate,
DATE_FORMAT DATE_FORMAT
)}`, )}${force ? ' (forced update)' : ''}`,
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME})` `DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME})`
); );
@ -157,7 +157,15 @@ export class DataGatheringProcessor {
currentDate = addDays(currentDate, 1); currentDate = addDays(currentDate, 1);
} }
await this.marketDataService.updateMany({ data }); if (force) {
await this.marketDataService.replaceForSymbol({
data,
dataSource,
symbol
});
} else {
await this.marketDataService.updateMany({ data });
}
Logger.log( Logger.log(
`Historical market data gathering has been completed for ${symbol} (${dataSource}) at ${format( `Historical market data gathering has been completed for ${symbol} (${dataSource}) at ${format(

8
apps/api/src/services/queues/data-gathering/data-gathering.service.ts

@ -2,7 +2,6 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
@ -41,7 +40,6 @@ export class DataGatheringService {
private readonly dataGatheringQueue: Queue, private readonly dataGatheringQueue: Queue,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService, private readonly propertyService: PropertyService,
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
@ -95,8 +93,6 @@ export class DataGatheringService {
} }
public async gatherSymbol({ dataSource, date, symbol }: DataGatheringItem) { public async gatherSymbol({ dataSource, date, symbol }: DataGatheringItem) {
await this.marketDataService.deleteMany({ dataSource, symbol });
const dataGatheringItems = (await this.getSymbolsMax()) const dataGatheringItems = (await this.getSymbolsMax())
.filter((dataGatheringItem) => { .filter((dataGatheringItem) => {
return ( return (
@ -111,6 +107,7 @@ export class DataGatheringService {
await this.gatherSymbols({ await this.gatherSymbols({
dataGatheringItems, dataGatheringItems,
force: true,
priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH
}); });
} }
@ -274,9 +271,11 @@ export class DataGatheringService {
public async gatherSymbols({ public async gatherSymbols({
dataGatheringItems, dataGatheringItems,
force = false,
priority priority
}: { }: {
dataGatheringItems: DataGatheringItem[]; dataGatheringItems: DataGatheringItem[];
force?: boolean;
priority: number; priority: number;
}) { }) {
await this.addJobsToQueue( await this.addJobsToQueue(
@ -285,6 +284,7 @@ export class DataGatheringService {
data: { data: {
dataSource, dataSource,
date, date,
force,
symbol symbol
}, },
name: GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME, name: GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME,

56
apps/client/project.json

@ -61,30 +61,30 @@
}, },
"targets": { "targets": {
"build": { "build": {
"executor": "@nx/angular:webpack-browser", "executor": "@nx/angular:browser-esbuild",
"options": { "options": {
"deleteOutputPath": false,
"localize": true,
"outputPath": "dist/apps/client",
"index": "apps/client/src/index.html", "index": "apps/client/src/index.html",
"main": "apps/client/src/main.ts", "main": "apps/client/src/main.ts",
"polyfills": "apps/client/src/polyfills.ts", "outputPath": "dist/apps/client",
"tsConfig": "apps/client/tsconfig.app.json", "tsConfig": "apps/client/tsconfig.app.json",
"buildOptimizer": false,
"deleteOutputPath": false,
"extractLicenses": false,
"localize": true,
"namedChunks": true,
"ngswConfigPath": "apps/client/ngsw-config.json",
"optimization": false,
"polyfills": "apps/client/src/polyfills.ts",
"scripts": ["node_modules/marked/marked.min.js"],
"serviceWorker": true,
"sourceMap": true,
"styles": [ "styles": [
"apps/client/src/assets/fonts/inter.css", "apps/client/src/assets/fonts/inter.css",
"apps/client/src/styles/theme.scss", "apps/client/src/styles/theme.scss",
"apps/client/src/styles.scss", "apps/client/src/styles.scss",
"node_modules/open-color/open-color.css" "node_modules/open-color/open-color.css"
], ],
"scripts": ["node_modules/marked/marked.min.js"], "vendorChunk": true
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true,
"serviceWorker": true,
"ngswConfigPath": "apps/client/ngsw-config.json"
}, },
"configurations": { "configurations": {
"development-ca": { "development-ca": {
@ -136,19 +136,6 @@
"localize": ["zh"] "localize": ["zh"]
}, },
"production": { "production": {
"fileReplacements": [
{
"replace": "apps/client/src/environments/environment.ts",
"with": "apps/client/src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
@ -160,7 +147,20 @@
"maximumWarning": "6kb", "maximumWarning": "6kb",
"maximumError": "10kb" "maximumError": "10kb"
} }
] ],
"buildOptimizer": true,
"extractLicenses": true,
"fileReplacements": [
{
"replace": "apps/client/src/environments/environment.ts",
"with": "apps/client/src/environments/environment.prod.ts"
}
],
"namedChunks": false,
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"vendorChunk": false
} }
}, },
"outputs": ["{options.outputPath}"], "outputs": ["{options.outputPath}"],

8
apps/client/src/app/components/admin-users/admin-users.component.ts

@ -5,7 +5,11 @@ import {
getDateFormatString, getDateFormatString,
getEmojiFlag getEmojiFlag
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { AdminUsers, InfoItem, User } from '@ghostfolio/common/interfaces'; import {
AdminUsersResponse,
InfoItem,
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfValueComponent } from '@ghostfolio/ui/value';
@ -75,7 +79,7 @@ import { GfUserDetailDialogComponent } from '../user-detail-dialog/user-detail-d
export class GfAdminUsersComponent implements OnDestroy, OnInit { export class GfAdminUsersComponent implements OnDestroy, OnInit {
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
public dataSource = new MatTableDataSource<AdminUsers['users'][0]>(); public dataSource = new MatTableDataSource<AdminUsersResponse['users'][0]>();
public defaultDateFormat: string; public defaultDateFormat: string;
public deviceType: string; public deviceType: string;
public displayedColumns: string[] = []; public displayedColumns: string[] = [];

18
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts

@ -1,10 +1,8 @@
import { GfDialogHeaderComponent } from '@ghostfolio/client/components/dialog-header/dialog-header.component'; import { GfDialogHeaderComponent } from '@ghostfolio/client/components/dialog-header/dialog-header.component';
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service';
import { import {
KEY_STAY_SIGNED_IN, KEY_STAY_SIGNED_IN,
SettingsStorageService SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service'; } from '@ghostfolio/client/services/settings-storage.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
@ -21,7 +19,6 @@ import {
} from '@angular/material/dialog'; } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { Router } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { eyeOffOutline, eyeOutline } from 'ionicons/icons'; import { eyeOffOutline, eyeOutline } from 'ionicons/icons';
@ -55,10 +52,7 @@ export class GfLoginWithAccessTokenDialogComponent {
public constructor( public constructor(
@Inject(MAT_DIALOG_DATA) public data: LoginWithAccessTokenDialogParams, @Inject(MAT_DIALOG_DATA) public data: LoginWithAccessTokenDialogParams,
public dialogRef: MatDialogRef<GfLoginWithAccessTokenDialogComponent>, public dialogRef: MatDialogRef<GfLoginWithAccessTokenDialogComponent>,
private internetIdentityService: InternetIdentityService, private settingsStorageService: SettingsStorageService
private router: Router,
private settingsStorageService: SettingsStorageService,
private tokenStorageService: TokenStorageService
) { ) {
addIcons({ eyeOffOutline, eyeOutline }); addIcons({ eyeOffOutline, eyeOutline });
} }
@ -81,14 +75,4 @@ export class GfLoginWithAccessTokenDialogComponent {
}); });
} }
} }
public async onLoginWithInternetIdentity() {
try {
const { authToken } = await this.internetIdentityService.login();
this.tokenStorageService.saveToken(authToken);
this.dialogRef.close();
this.router.navigate(['/']);
} catch {}
}
} }

11
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html

@ -26,17 +26,6 @@
@if (data.hasPermissionToUseSocialLogin) { @if (data.hasPermissionToUseSocialLogin) {
<div class="my-3 text-center text-muted" i18n>or</div> <div class="my-3 text-center text-muted" i18n>or</div>
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<button
class="mb-2 px-4 rounded-pill"
mat-stroked-button
(click)="onLoginWithInternetIdentity()"
>
<img
class="mr-2"
src="../assets/icons/internet-computer.svg"
style="height: 0.75rem"
/><span i18n>Sign in with Internet Identity</span>
</button>
<a <a
class="px-4 rounded-pill" class="px-4 rounded-pill"
href="../api/v1/auth/google" href="../api/v1/auth/google"

4
apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts

@ -1,8 +1,8 @@
import { AdminUsers } from '@ghostfolio/common/interfaces'; import { AdminUsersResponse } from '@ghostfolio/common/interfaces';
export interface UserDetailDialogParams { export interface UserDetailDialogParams {
deviceType: string; deviceType: string;
hasPermissionForSubscription: boolean; hasPermissionForSubscription: boolean;
locale: string; locale: string;
userData: AdminUsers['users'][0]; userData: AdminUsersResponse['users'][0];
} }

2
apps/client/src/app/core/module-preload.service.ts

@ -7,7 +7,7 @@ export class ModulePreloadService implements PreloadingStrategy {
/** /**
* Preloads all lazy loading modules with the attribute 'preload' set to true * Preloads all lazy loading modules with the attribute 'preload' set to true
*/ */
preload(route: Route, load: Function): Observable<any> { preload<T>(route: Route, load: () => Observable<T>): Observable<T | null> {
return route.data?.preload ? load() : of(null); return route.data?.preload ? load() : of(null);
} }
} }

14
apps/client/src/app/pages/portfolio/fire/fire-page.scss

@ -1,9 +1,21 @@
@use '../../../../styles/variables.scss' as variables;
@mixin select-arrow($color) {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' fill='#{$color}' height='6' width='10'><polygon points='0,0 10,0 5,6'/></svg>");
}
:host { :host {
display: block; display: block;
.safe-withdrawal-rate-select { .safe-withdrawal-rate-select {
@include select-arrow(variables.$dark-primary-text);
appearance: none;
background-color: transparent; background-color: transparent;
background-position: right 0 center;
background-repeat: no-repeat;
color: rgb(var(--dark-primary-text)); color: rgb(var(--dark-primary-text));
padding: 0 0.75rem 0 0.25rem;
&:focus { &:focus {
box-shadow: none; box-shadow: none;
@ -14,6 +26,8 @@
:host-context(.theme-dark) { :host-context(.theme-dark) {
.safe-withdrawal-rate-select { .safe-withdrawal-rate-select {
@include select-arrow(variables.$light-primary-text);
color: rgb(var(--light-primary-text)); color: rgb(var(--light-primary-text));
} }
} }

12
apps/client/src/app/pages/register/register-page.component.ts

@ -1,5 +1,4 @@
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { InfoItem, LineChartItem } from '@ghostfolio/common/interfaces'; import { InfoItem, LineChartItem } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -43,7 +42,6 @@ export class GfRegisterPageComponent implements OnDestroy, OnInit {
private dataService: DataService, private dataService: DataService,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private internetIdentityService: InternetIdentityService,
private router: Router, private router: Router,
private tokenStorageService: TokenStorageService private tokenStorageService: TokenStorageService
) { ) {
@ -73,16 +71,6 @@ export class GfRegisterPageComponent implements OnDestroy, OnInit {
); );
} }
public async onLoginWithInternetIdentity() {
try {
const { authToken } = await this.internetIdentityService.login();
this.tokenStorageService.saveToken(authToken);
await this.router.navigate(['/']);
} catch {}
}
public openShowAccessTokenDialog() { public openShowAccessTokenDialog() {
const dialogRef = this.dialog.open< const dialogRef = this.dialog.open<
GfUserAccountRegistrationDialogComponent, GfUserAccountRegistrationDialogComponent,

14
apps/client/src/app/pages/register/register-page.html

@ -28,20 +28,6 @@
</button> </button>
@if (hasPermissionForSocialLogin) { @if (hasPermissionForSocialLogin) {
<div class="my-3 text-muted" i18n>or</div> <div class="my-3 text-muted" i18n>or</div>
@if (false) {
<button
class="d-block mb-2 px-4 rounded-pill"
mat-stroked-button
(click)="onLoginWithInternetIdentity()"
>
<img
class="mr-2"
src="../assets/icons/internet-computer.svg"
style="height: 0.75rem"
/>
<span i18n>Continue with Internet Identity</span>
</button>
}
<a <a
class="px-4 rounded-pill w-100" class="px-4 rounded-pill w-100"
href="../api/v1/auth/google" href="../api/v1/auth/google"

4
apps/client/src/app/services/admin.service.ts

@ -12,7 +12,7 @@ import {
AdminData, AdminData,
AdminJobs, AdminJobs,
AdminMarketData, AdminMarketData,
AdminUsers, AdminUsersResponse,
DataProviderGhostfolioStatusResponse, DataProviderGhostfolioStatusResponse,
EnhancedSymbolProfile, EnhancedSymbolProfile,
Filter Filter
@ -154,7 +154,7 @@ export class AdminService {
params = params.append('skip', skip); params = params.append('skip', skip);
params = params.append('take', take); params = params.append('take', take);
return this.http.get<AdminUsers>('/api/v1/admin/user', { params }); return this.http.get<AdminUsersResponse>('/api/v1/admin/user', { params });
} }
public gather7Days() { public gather7Days() {

56
apps/client/src/app/services/internet-identity.service.ts

@ -1,56 +0,0 @@
import { OAuthResponse } from '@ghostfolio/common/interfaces';
import { HttpClient } from '@angular/common/http';
import { Injectable, OnDestroy } from '@angular/core';
import { AuthClient } from '@dfinity/auth-client';
import { EMPTY, Subject } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class InternetIdentityService implements OnDestroy {
private unsubscribeSubject = new Subject<void>();
public constructor(private http: HttpClient) {}
public async login(): Promise<OAuthResponse> {
const authClient = await AuthClient.create({
idleOptions: {
disableDefaultIdleCallback: true,
disableIdle: true
}
});
return new Promise((resolve, reject) => {
authClient.login({
onError: async () => {
return reject();
},
onSuccess: () => {
const principalId = authClient.getIdentity().getPrincipal();
this.http
.post<OAuthResponse>(`/api/v1/auth/internet-identity`, {
principalId: principalId.toText()
})
.pipe(
catchError(() => {
reject();
return EMPTY;
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe((response) => {
resolve(response);
});
}
});
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

28
apps/client/src/assets/icons/internet-computer.svg

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 358.8 179.8" style="enable-background:new 0 0 358.8 179.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:url(#SVGID_2_);}
.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#29ABE2;}
</style>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="224.7853" y1="257.7536" x2="348.0663" y2="133.4581" gradientTransform="matrix(1 0 0 -1 0 272)">
<stop offset="0.21" style="stop-color:#F15A24"/>
<stop offset="0.6841" style="stop-color:#FBB03B"/>
</linearGradient>
<path class="st0" d="M271.6,0c-20,0-41.9,10.9-65,32.4c-10.9,10.1-20.5,21.1-27.5,29.8c0,0,11.2,12.9,23.5,26.8
c6.7-8.4,16.2-19.8,27.3-30.1c20.5-19.2,33.9-23.1,41.6-23.1c28.8,0,52.2,24.2,52.2,54.1c0,29.6-23.4,53.8-52.2,54.1
c-1.4,0-3-0.2-5-0.6c8.4,3.9,17.5,6.7,26,6.7c52.8,0,63.2-36.5,63.8-39.1c1.5-6.7,2.4-13.7,2.4-20.9C358.6,40.4,319.6,0,271.6,0z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="133.9461" y1="106.4262" x2="10.6653" y2="230.7215" gradientTransform="matrix(1 0 0 -1 0 272)">
<stop offset="0.21" style="stop-color:#ED1E79"/>
<stop offset="0.8929" style="stop-color:#522785"/>
</linearGradient>
<path class="st1" d="M87.1,179.8c20,0,41.9-10.9,65-32.4c10.9-10.1,20.5-21.1,27.5-29.8c0,0-11.2-12.9-23.5-26.8
c-6.7,8.4-16.2,19.8-27.3,30.1c-20.5,19-34,23.1-41.6,23.1c-28.8,0-52.2-24.2-52.2-54.1c0-29.6,23.4-53.8,52.2-54.1
c1.4,0,3,0.2,5,0.6c-8.4-3.9-17.5-6.7-26-6.7C13.4,29.6,3,66.1,2.4,68.8C0.9,75.5,0,82.5,0,89.7C0,139.4,39,179.8,87.1,179.8z"/>
<path class="st2" d="M127.3,59.7c-5.8-5.6-34-28.5-61-29.3C18.1,29.2,4,64.2,2.7,68.7C12,29.5,46.4,0.2,87.2,0
c33.3,0,67,32.7,91.9,62.2c0,0,0.1-0.1,0.1-0.1c0,0,11.2,12.9,23.5,26.8c0,0,14,16.5,28.8,31c5.8,5.6,33.9,28.2,60.9,29
c49.5,1.4,63.2-35.6,63.9-38.4c-9.1,39.5-43.6,68.9-84.6,69.1c-33.3,0-67-32.7-92-62.2c0,0.1-0.1,0.1-0.1,0.2
c0,0-11.2-12.9-23.5-26.8C156.2,90.8,142.2,74.2,127.3,59.7z M2.7,69.1c0-0.1,0-0.2,0.1-0.3C2.7,68.9,2.7,69,2.7,69.1z"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

61
apps/client/src/styles/theme.scss

@ -1,9 +1,6 @@
@use '@angular/material' as mat; @use '@angular/material' as mat;
$dark-primary-text: rgba(black, 0.87); @use './variables.scss' as variables;
$light-primary-text: white;
$mat-css-dark-theme-selector: '.theme-dark';
$gf-primary: ( $gf-primary: (
50: var(--gf-theme-primary-50), 50: var(--gf-theme-primary-50),
@ -21,20 +18,20 @@ $gf-primary: (
A400: var(--gf-theme-primary-A400), A400: var(--gf-theme-primary-A400),
A700: var(--gf-theme-primary-A700), A700: var(--gf-theme-primary-A700),
contrast: ( contrast: (
50: $dark-primary-text, 50: variables.$dark-primary-text,
100: $dark-primary-text, 100: variables.$dark-primary-text,
200: $dark-primary-text, 200: variables.$dark-primary-text,
300: $light-primary-text, 300: variables.$light-primary-text,
400: $light-primary-text, 400: variables.$light-primary-text,
500: $light-primary-text, 500: variables.$light-primary-text,
600: $light-primary-text, 600: variables.$light-primary-text,
700: $light-primary-text, 700: variables.$light-primary-text,
800: $light-primary-text, 800: variables.$light-primary-text,
900: $light-primary-text, 900: variables.$light-primary-text,
A100: $dark-primary-text, A100: variables.$dark-primary-text,
A200: $light-primary-text, A200: variables.$light-primary-text,
A400: $light-primary-text, A400: variables.$light-primary-text,
A700: $light-primary-text A700: variables.$light-primary-text
) )
); );
@ -54,20 +51,20 @@ $gf-secondary: (
A400: var(--gf-theme-secondary-A400), A400: var(--gf-theme-secondary-A400),
A700: var(--gf-theme-secondary-A700), A700: var(--gf-theme-secondary-A700),
contrast: ( contrast: (
50: $dark-primary-text, 50: variables.$dark-primary-text,
100: $dark-primary-text, 100: variables.$dark-primary-text,
200: $dark-primary-text, 200: variables.$dark-primary-text,
300: $light-primary-text, 300: variables.$light-primary-text,
400: $light-primary-text, 400: variables.$light-primary-text,
500: $light-primary-text, 500: variables.$light-primary-text,
600: $light-primary-text, 600: variables.$light-primary-text,
700: $light-primary-text, 700: variables.$light-primary-text,
800: $light-primary-text, 800: variables.$light-primary-text,
900: $light-primary-text, 900: variables.$light-primary-text,
A100: $dark-primary-text, A100: variables.$dark-primary-text,
A200: $light-primary-text, A200: variables.$light-primary-text,
A400: $light-primary-text, A400: variables.$light-primary-text,
A700: $light-primary-text A700: variables.$light-primary-text
) )
); );

4
apps/client/src/styles/variables.scss

@ -0,0 +1,4 @@
$dark-primary-text: rgba(black, 0.87);
$light-primary-text: white;
$mat-css-dark-theme-selector: '.theme-dark';

5
eslint.config.cjs

@ -152,7 +152,6 @@ module.exports = [
// The following rules are part of eslint:recommended // The following rules are part of eslint:recommended
// and can be remove once solved // and can be remove once solved
'no-constant-binary-expression': 'warn',
'no-loss-of-precision': 'warn', 'no-loss-of-precision': 'warn',
// The following rules are part of @typescript-eslint/recommended-type-checked // The following rules are part of @typescript-eslint/recommended-type-checked
@ -170,7 +169,6 @@ module.exports = [
'@typescript-eslint/no-unsafe-argument': 'warn', '@typescript-eslint/no-unsafe-argument': 'warn',
'@typescript-eslint/no-unsafe-assignment': 'warn', '@typescript-eslint/no-unsafe-assignment': 'warn',
'@typescript-eslint/no-unsafe-enum-comparison': 'warn', '@typescript-eslint/no-unsafe-enum-comparison': 'warn',
'@typescript-eslint/no-unsafe-function-type': 'warn',
'@typescript-eslint/no-unsafe-member-access': 'warn', '@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-unsafe-return': 'warn', '@typescript-eslint/no-unsafe-return': 'warn',
'@typescript-eslint/no-unsafe-call': 'warn', '@typescript-eslint/no-unsafe-call': 'warn',
@ -189,8 +187,7 @@ module.exports = [
// The following rules are part of @typescript-eslint/stylistic-type-checked // The following rules are part of @typescript-eslint/stylistic-type-checked
// and can be remove once solved // and can be remove once solved
'@typescript-eslint/prefer-nullish-coalescing': 'warn', // TODO: Requires strictNullChecks: true '@typescript-eslint/prefer-nullish-coalescing': 'warn' // TODO: Requires strictNullChecks: true
'@typescript-eslint/prefer-regexp-exec': 'warn'
} }
})) }))
]; ];

2
libs/common/src/lib/helper.ts

@ -375,7 +375,7 @@ export function parseDate(date: string): Date {
// Transform 'yyyyMMdd' format to supported format by parse function // Transform 'yyyyMMdd' format to supported format by parse function
if (date?.length === 8) { if (date?.length === 8) {
const match = date.match(/^(\d{4})(\d{2})(\d{2})$/); const match = /^(\d{4})(\d{2})(\d{2})$/.exec(date);
if (match) { if (match) {
const [, year, month, day] = match; const [, year, month, day] = match;

4
libs/common/src/lib/interfaces/index.ts

@ -7,7 +7,6 @@ import type {
AdminMarketData, AdminMarketData,
AdminMarketDataItem AdminMarketDataItem
} from './admin-market-data.interface'; } from './admin-market-data.interface';
import type { AdminUsers } from './admin-users.interface';
import type { AssetClassSelectorOption } from './asset-class-selector-option.interface'; import type { AssetClassSelectorOption } from './asset-class-selector-option.interface';
import type { AssetProfileIdentifier } from './asset-profile-identifier.interface'; import type { AssetProfileIdentifier } from './asset-profile-identifier.interface';
import type { BenchmarkProperty } from './benchmark-property.interface'; import type { BenchmarkProperty } from './benchmark-property.interface';
@ -39,6 +38,7 @@ import type { AccountBalancesResponse } from './responses/account-balances-respo
import type { AccountsResponse } from './responses/accounts-response.interface'; import type { AccountsResponse } from './responses/accounts-response.interface';
import type { ActivitiesResponse } from './responses/activities-response.interface'; import type { ActivitiesResponse } from './responses/activities-response.interface';
import type { ActivityResponse } from './responses/activity-response.interface'; import type { ActivityResponse } from './responses/activity-response.interface';
import type { AdminUsersResponse } from './responses/admin-users-response.interface';
import type { AiPromptResponse } from './responses/ai-prompt-response.interface'; import type { AiPromptResponse } from './responses/ai-prompt-response.interface';
import type { ApiKeyResponse } from './responses/api-key-response.interface'; import type { ApiKeyResponse } from './responses/api-key-response.interface';
import type { AssetResponse } from './responses/asset-response.interface'; import type { AssetResponse } from './responses/asset-response.interface';
@ -92,7 +92,7 @@ export {
AdminMarketData, AdminMarketData,
AdminMarketDataDetails, AdminMarketDataDetails,
AdminMarketDataItem, AdminMarketDataItem,
AdminUsers, AdminUsersResponse,
AiPromptResponse, AiPromptResponse,
ApiKeyResponse, ApiKeyResponse,
AssetClassSelectorOption, AssetClassSelectorOption,

2
libs/common/src/lib/interfaces/admin-users.interface.ts → libs/common/src/lib/interfaces/responses/admin-users-response.interface.ts

@ -1,6 +1,6 @@
import { Role } from '@prisma/client'; import { Role } from '@prisma/client';
export interface AdminUsers { export interface AdminUsersResponse {
count: number; count: number;
users: { users: {
accountCount: number; accountCount: number;

2
libs/ui/src/lib/fire-calculator/fire-calculator.component.ts

@ -185,7 +185,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
'principalInvestmentAmount' 'principalInvestmentAmount'
).value, ).value,
projectedTotalAmount: projectedTotalAmount:
Number(this.getProjectedTotalAmount().toFixed(0)) ?? 0, Math.round(this.getProjectedTotalAmount()) || 0,
retirementDate: retirementDate:
this.getRetirementDate() ?? this.DEFAULT_RETIREMENT_DATE this.getRetirementDate() ?? this.DEFAULT_RETIREMENT_DATE
}, },

3
libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts

@ -31,6 +31,7 @@ import ChartDataLabels from 'chartjs-plugin-datalabels';
import { isUUID } from 'class-validator'; import { isUUID } from 'class-validator';
import Color from 'color'; import Color from 'color';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import OpenColor from 'open-color';
import { translate } from '../i18n'; import { translate } from '../i18n';
@ -47,7 +48,7 @@ const {
teal, teal,
violet, violet,
yellow yellow
} = require('open-color'); } = OpenColor;
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

3
libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

@ -33,10 +33,11 @@ import { isUUID } from 'class-validator';
import { differenceInDays, max } from 'date-fns'; import { differenceInDays, max } from 'date-fns';
import { orderBy } from 'lodash'; import { orderBy } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import OpenColor from 'open-color';
import { GetColorParams } from './interfaces/interfaces'; import { GetColorParams } from './interfaces/interfaces';
const { gray, green, red } = require('open-color'); const { gray, green, red } = OpenColor;
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

240
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.213.0", "version": "2.214.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.213.0", "version": "2.214.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
@ -23,11 +23,6 @@
"@angular/service-worker": "20.2.4", "@angular/service-worker": "20.2.4",
"@codewithdan/observable-store": "2.2.15", "@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.0", "@date-fns/utc": "2.1.0",
"@dfinity/agent": "0.15.7",
"@dfinity/auth-client": "0.15.7",
"@dfinity/candid": "0.15.7",
"@dfinity/identity": "0.15.7",
"@dfinity/principal": "0.15.7",
"@internationalized/number": "3.6.3", "@internationalized/number": "3.6.3",
"@ionic/angular": "8.7.3", "@ionic/angular": "8.7.3",
"@keyv/redis": "4.4.0", "@keyv/redis": "4.4.0",
@ -62,7 +57,7 @@
"class-validator": "0.14.2", "class-validator": "0.14.2",
"color": "5.0.0", "color": "5.0.0",
"countries-and-timezones": "3.8.0", "countries-and-timezones": "3.8.0",
"countries-list": "3.1.1", "countries-list": "3.2.0",
"countup.js": "2.9.0", "countup.js": "2.9.0",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"dotenv": "17.2.3", "dotenv": "17.2.3",
@ -77,7 +72,7 @@
"lodash": "4.17.21", "lodash": "4.17.21",
"marked": "15.0.4", "marked": "15.0.4",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"ng-extract-i18n-merge": "3.0.0", "ng-extract-i18n-merge": "3.1.0",
"ngx-device-detector": "10.1.0", "ngx-device-detector": "10.1.0",
"ngx-markdown": "20.0.0", "ngx-markdown": "20.0.0",
"ngx-skeleton-loader": "11.3.0", "ngx-skeleton-loader": "11.3.0",
@ -93,7 +88,7 @@
"stripe": "18.5.0", "stripe": "18.5.0",
"svgmap": "2.12.2", "svgmap": "2.12.2",
"tablemark": "4.1.0", "tablemark": "4.1.0",
"twitter-api-v2": "1.23.0", "twitter-api-v2": "1.27.0",
"uuid": "11.1.0", "uuid": "11.1.0",
"yahoo-finance2": "3.10.0", "yahoo-finance2": "3.10.0",
"zone.js": "0.15.1" "zone.js": "0.15.1"
@ -4713,6 +4708,7 @@
"version": "0.8.1", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/trace-mapping": "0.3.9" "@jridgewell/trace-mapping": "0.3.9"
@ -4725,6 +4721,7 @@
"version": "0.3.9", "version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/resolve-uri": "^3.0.3",
@ -4953,73 +4950,6 @@
"node": "^16.13.0 || >=18.0.0" "node": "^16.13.0 || >=18.0.0"
} }
}, },
"node_modules/@dfinity/agent": {
"version": "0.15.7",
"resolved": "https://registry.npmjs.org/@dfinity/agent/-/agent-0.15.7.tgz",
"integrity": "sha512-w34yvlUTpPBG8nLOD0t/ao3k2xonOFq4QGvfJ1HiS/nIggdza/3xC3nLBszGrjVYWj1jqu8BLFvQXCAeWin75A==",
"license": "Apache-2.0",
"dependencies": {
"base64-arraybuffer": "^0.2.0",
"bignumber.js": "^9.0.0",
"borc": "^2.1.1",
"js-sha256": "0.9.0",
"simple-cbor": "^0.4.1",
"ts-node": "^10.8.2"
},
"peerDependencies": {
"@dfinity/candid": "^0.15.7",
"@dfinity/principal": "^0.15.7"
}
},
"node_modules/@dfinity/auth-client": {
"version": "0.15.7",
"resolved": "https://registry.npmjs.org/@dfinity/auth-client/-/auth-client-0.15.7.tgz",
"integrity": "sha512-f6cRqXayCf+7+9gNcDnAZZwJrgBYKIzfxjxeRLlpsueQeo+E/BX2yVSANxzTkCNc4U3p+ttHI1RNtasLunYTcA==",
"license": "Apache-2.0",
"dependencies": {
"idb": "^7.0.2"
},
"peerDependencies": {
"@dfinity/agent": "^0.15.7",
"@dfinity/identity": "^0.15.7",
"@dfinity/principal": "^0.15.7"
}
},
"node_modules/@dfinity/candid": {
"version": "0.15.7",
"resolved": "https://registry.npmjs.org/@dfinity/candid/-/candid-0.15.7.tgz",
"integrity": "sha512-lTcjK/xrSyT7wvUQ2pApG+yklQAwxaofQ04D1IWv0/8gKbY0eUbh8G2w6+CypJ15Hb1CH24ijUj8nWdeX/z3jg==",
"license": "Apache-2.0",
"dependencies": {
"ts-node": "^10.8.2"
}
},
"node_modules/@dfinity/identity": {
"version": "0.15.7",
"resolved": "https://registry.npmjs.org/@dfinity/identity/-/identity-0.15.7.tgz",
"integrity": "sha512-kBAkx9wq78jSQf6T5aayLyWm8YgtOZw8bW6+OuzX6tR3hkAEa85A9TcKA7BjkmMWSIskjEDVQub4fFfKWS2vOQ==",
"license": "Apache-2.0",
"dependencies": {
"borc": "^2.1.1",
"js-sha256": "^0.9.0",
"tweetnacl": "^1.0.1"
},
"peerDependencies": {
"@dfinity/agent": "^0.15.7",
"@dfinity/principal": "^0.15.7",
"@peculiar/webcrypto": "^1.4.0"
}
},
"node_modules/@dfinity/principal": {
"version": "0.15.7",
"resolved": "https://registry.npmjs.org/@dfinity/principal/-/principal-0.15.7.tgz",
"integrity": "sha512-6/AkYzpGEH6Jw/0+B/EeeQn+5u2GDDvRLt1kQPhIG4txQYFnOy04H3VvyrymmfAj6/CXUgrOrux6OxgYSLYVJg==",
"license": "Apache-2.0",
"dependencies": {
"js-sha256": "^0.9.0",
"ts-node": "^10.8.2"
}
},
"node_modules/@discoveryjs/json-ext": { "node_modules/@discoveryjs/json-ext": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz",
@ -11908,36 +11838,6 @@
"tslib": "^2.8.1" "tslib": "^2.8.1"
} }
}, },
"node_modules/@peculiar/json-schema": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz",
"integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@peculiar/webcrypto": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.5.0.tgz",
"integrity": "sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/json-schema": "^1.1.12",
"pvtsutils": "^1.3.5",
"tslib": "^2.6.2",
"webcrypto-core": "^1.8.0"
},
"engines": {
"node": ">=10.12.0"
}
},
"node_modules/@phenomnomnominal/tsquery": { "node_modules/@phenomnomnominal/tsquery": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/@phenomnomnominal/tsquery/-/tsquery-5.0.1.tgz", "resolved": "https://registry.npmjs.org/@phenomnomnominal/tsquery/-/tsquery-5.0.1.tgz",
@ -13754,24 +13654,28 @@
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@tsconfig/node12": { "node_modules/@tsconfig/node12": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@tsconfig/node14": { "node_modules/@tsconfig/node14": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@tsconfig/node16": { "node_modules/@tsconfig/node16": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@tufjs/canonical-json": { "node_modules/@tufjs/canonical-json": {
@ -15686,6 +15590,7 @@
"version": "8.15.0", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@ -15732,6 +15637,7 @@
"version": "8.3.4", "version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"acorn": "^8.11.0" "acorn": "^8.11.0"
@ -16057,6 +15963,7 @@
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/argparse": { "node_modules/argparse": {
@ -16709,14 +16616,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-arraybuffer": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz",
"integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -17040,30 +16939,6 @@
"popper.js": "^1.16.1" "popper.js": "^1.16.1"
} }
}, },
"node_modules/borc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/borc/-/borc-2.1.2.tgz",
"integrity": "sha512-Sy9eoUi4OiKzq7VovMn246iTo17kzuyHJKomCfpWMlI6RpfN1gk95w7d7gH264nApVLg0HZfcpz62/g4VH1Y4w==",
"license": "MIT",
"dependencies": {
"bignumber.js": "^9.0.0",
"buffer": "^5.5.0",
"commander": "^2.15.0",
"ieee754": "^1.1.13",
"iso-url": "~0.4.7",
"json-text-sequence": "~0.1.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/borc/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT"
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.12", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@ -17160,6 +17035,7 @@
"version": "5.7.1", "version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -18647,9 +18523,9 @@
} }
}, },
"node_modules/countries-list": { "node_modules/countries-list": {
"version": "3.1.1", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.1.1.tgz", "resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.2.0.tgz",
"integrity": "sha512-nPklKJ5qtmY5MdBKw1NiBAoyx5Sa7p2yPpljZyQ7gyCN1m+eMFs9I6CT37Mxt8zvR5L3VzD3DJBE4WQzX3WF4A==", "integrity": "sha512-HYHAo2fwEsG3TmbsNdVmIQPHizRlqeYMTtLEAl0IANG/3jRYX7p3NR6VapDqKP0n60TmsRy1dyRjVN5JbywDbA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/countup.js": { "node_modules/countup.js": {
@ -19325,6 +19201,7 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cron": { "node_modules/cron": {
@ -20808,12 +20685,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/delimit-stream": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/delimit-stream/-/delimit-stream-0.1.0.tgz",
"integrity": "sha512-a02fiQ7poS5CnjiJBAsjGLPp5EwVoGHNeu9sziBd9huppRfsAFIpv5zNLv0V1gbop53ilngAf5Kf331AwcoRBQ==",
"license": "BSD-2-Clause"
},
"node_modules/denque": { "node_modules/denque": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@ -20908,6 +20779,7 @@
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.3.1" "node": ">=0.3.1"
@ -24840,12 +24712,6 @@
"postcss": "^8.1.0" "postcss": "^8.1.0"
} }
}, },
"node_modules/idb": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
"integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
"license": "ISC"
},
"node_modules/identity-obj-proxy": { "node_modules/identity-obj-proxy": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz",
@ -25837,15 +25703,6 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/iso-url": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/iso-url/-/iso-url-0.4.7.tgz",
"integrity": "sha512-27fFRDnPAMnHGLq36bWTpKET+eiXct3ENlCcdcMdk+mjXrb2kw3mhBUg1B7ewAC0kVzlOPhADzQgz1SE6Tglog==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/isobject": { "node_modules/isobject": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
@ -30098,12 +29955,6 @@
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
"node_modules/js-sha256": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
"integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==",
"license": "MIT"
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -30316,15 +30167,6 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/json-text-sequence": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/json-text-sequence/-/json-text-sequence-0.1.1.tgz",
"integrity": "sha512-L3mEegEWHRekSHjc7+sc8eJhba9Clq1PZ8kMkzf8OxElhXc8O4TS5MwcVlj9aEbm5dr81N90WHC5nAz3UO971w==",
"license": "MIT",
"dependencies": {
"delimit-stream": "0.1.0"
}
},
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@ -32083,6 +31925,7 @@
"version": "1.3.6", "version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/make-fetch-happen": { "node_modules/make-fetch-happen": {
@ -32780,9 +32623,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/ng-extract-i18n-merge": { "node_modules/ng-extract-i18n-merge": {
"version": "3.0.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/ng-extract-i18n-merge/-/ng-extract-i18n-merge-3.0.0.tgz", "resolved": "https://registry.npmjs.org/ng-extract-i18n-merge/-/ng-extract-i18n-merge-3.1.0.tgz",
"integrity": "sha512-vTWtAz6a/wVYxnUMFHp1ur6o4JSLm+LcxdSMV8o8Ml2p5oCsSB4iFd5E6h8Yb8X8D596qyBz0ELgiDmbn4YyRQ==", "integrity": "sha512-4rJRcpTcP54xf5cjoz3S1By0T04X2RoyQcMDxr4wLdRx3fVxkeP8jeuLzmj9F4G5n0yMQb+6jhUiFERxpkfs1w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@angular-devkit/architect": "^0.2000.0", "@angular-devkit/architect": "^0.2000.0",
@ -38081,12 +37924,6 @@
"node": "^18.17.0 || >=20.5.0" "node": "^18.17.0 || >=20.5.0"
} }
}, },
"node_modules/simple-cbor": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/simple-cbor/-/simple-cbor-0.4.1.tgz",
"integrity": "sha512-rijcxtwx2b4Bje3sqeIqw5EeW7UlOIC4YfOdwqIKacpvRQ/D78bWg/4/0m5e0U91oKvlGh7LlJuZCu07ISCC7w==",
"license": "ISC"
},
"node_modules/sirv": { "node_modules/sirv": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
@ -40042,6 +39879,7 @@
"version": "10.9.2", "version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@cspotcode/source-map-support": "^0.8.0", "@cspotcode/source-map-support": "^0.8.0",
@ -40219,16 +40057,10 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
"license": "Unlicense"
},
"node_modules/twitter-api-v2": { "node_modules/twitter-api-v2": {
"version": "1.23.0", "version": "1.27.0",
"resolved": "https://registry.npmjs.org/twitter-api-v2/-/twitter-api-v2-1.23.0.tgz", "resolved": "https://registry.npmjs.org/twitter-api-v2/-/twitter-api-v2-1.27.0.tgz",
"integrity": "sha512-5i1agETVpTuY68Zuk9i2B3N9wHzc4JIWw0WKyG4CEaFv9mRKmU87roa+U1oYYXTChWb0HMcqfkwoBJHYmLbeDA==", "integrity": "sha512-hbIFwzg0NeOcFOdmJqtKMCXjLjc0INff/7NwhnZ2zpnw65oku8i+0eMxo5M0iTc1hs+inD/IpDw3S0Xh2c45QQ==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/type-check": { "node_modules/type-check": {
@ -40795,6 +40627,7 @@
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/v8-to-istanbul": { "node_modules/v8-to-istanbul": {
@ -41110,20 +40943,6 @@
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
"node_modules/webcrypto-core": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.8.1.tgz",
"integrity": "sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@peculiar/asn1-schema": "^2.3.13",
"@peculiar/json-schema": "^1.1.12",
"asn1js": "^3.0.5",
"pvtsutils": "^1.3.5",
"tslib": "^2.7.0"
}
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@ -42424,6 +42243,7 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"

13
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.213.0", "version": "2.214.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio", "repository": "https://github.com/ghostfolio/ghostfolio",
@ -69,11 +69,6 @@
"@angular/service-worker": "20.2.4", "@angular/service-worker": "20.2.4",
"@codewithdan/observable-store": "2.2.15", "@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.0", "@date-fns/utc": "2.1.0",
"@dfinity/agent": "0.15.7",
"@dfinity/auth-client": "0.15.7",
"@dfinity/candid": "0.15.7",
"@dfinity/identity": "0.15.7",
"@dfinity/principal": "0.15.7",
"@internationalized/number": "3.6.3", "@internationalized/number": "3.6.3",
"@ionic/angular": "8.7.3", "@ionic/angular": "8.7.3",
"@keyv/redis": "4.4.0", "@keyv/redis": "4.4.0",
@ -108,7 +103,7 @@
"class-validator": "0.14.2", "class-validator": "0.14.2",
"color": "5.0.0", "color": "5.0.0",
"countries-and-timezones": "3.8.0", "countries-and-timezones": "3.8.0",
"countries-list": "3.1.1", "countries-list": "3.2.0",
"countup.js": "2.9.0", "countup.js": "2.9.0",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"dotenv": "17.2.3", "dotenv": "17.2.3",
@ -123,7 +118,7 @@
"lodash": "4.17.21", "lodash": "4.17.21",
"marked": "15.0.4", "marked": "15.0.4",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"ng-extract-i18n-merge": "3.0.0", "ng-extract-i18n-merge": "3.1.0",
"ngx-device-detector": "10.1.0", "ngx-device-detector": "10.1.0",
"ngx-markdown": "20.0.0", "ngx-markdown": "20.0.0",
"ngx-skeleton-loader": "11.3.0", "ngx-skeleton-loader": "11.3.0",
@ -139,7 +134,7 @@
"stripe": "18.5.0", "stripe": "18.5.0",
"svgmap": "2.12.2", "svgmap": "2.12.2",
"tablemark": "4.1.0", "tablemark": "4.1.0",
"twitter-api-v2": "1.23.0", "twitter-api-v2": "1.27.0",
"uuid": "11.1.0", "uuid": "11.1.0",
"yahoo-finance2": "3.10.0", "yahoo-finance2": "3.10.0",
"zone.js": "0.15.1" "zone.js": "0.15.1"

8
test/import/not-ok/invalid-data-source.json

@ -14,5 +14,11 @@
"type": "BUY", "type": "BUY",
"unitPrice": 100.0 "unitPrice": 100.0
} }
] ],
"user": {
"settings": {
"currency": "USD",
"performanceCalculationType": "ROAI"
}
}
} }

8
test/import/not-ok/invalid-date-before-min.json

@ -14,5 +14,11 @@
"type": "BUY", "type": "BUY",
"unitPrice": 100.0 "unitPrice": 100.0
} }
] ],
"user": {
"settings": {
"currency": "USD",
"performanceCalculationType": "ROAI"
}
}
} }

8
test/import/not-ok/invalid-date.json

@ -14,5 +14,11 @@
"type": "BUY", "type": "BUY",
"unitPrice": 100.0 "unitPrice": 100.0
} }
] ],
"user": {
"settings": {
"currency": "USD",
"performanceCalculationType": "ROAI"
}
}
} }

8
test/import/not-ok/invalid-symbol.json

@ -14,5 +14,11 @@
"type": "BUY", "type": "BUY",
"unitPrice": 100.0 "unitPrice": 100.0
} }
] ],
"user": {
"settings": {
"currency": "USD",
"performanceCalculationType": "ROAI"
}
}
} }

8
test/import/not-ok/invalid-type.json

@ -14,5 +14,11 @@
"type": "<invalid>", "type": "<invalid>",
"unitPrice": 100.0 "unitPrice": 100.0
} }
] ],
"user": {
"settings": {
"currency": "USD",
"performanceCalculationType": "ROAI"
}
}
} }

8
test/import/not-ok/unavailable-exchange-rate.json

@ -15,5 +15,11 @@
"date": "1990-01-01T00:00:00.000Z", "date": "1990-01-01T00:00:00.000Z",
"symbol": "MSFT" "symbol": "MSFT"
} }
] ],
"user": {
"settings": {
"currency": "USD",
"performanceCalculationType": "ROAI"
}
}
} }

3
test/import/ok/500-activities.json

@ -6019,7 +6019,8 @@
], ],
"user": { "user": {
"settings": { "settings": {
"currency": "USD" "currency": "USD",
"performanceCalculationType": "ROAI"
} }
} }
} }

3
test/import/ok/btceur.json

@ -23,7 +23,8 @@
], ],
"user": { "user": {
"settings": { "settings": {
"currency": "USD" "currency": "USD",
"performanceCalculationType": "ROAI"
} }
} }
} }

3
test/import/ok/btcusd-short.json

@ -36,7 +36,8 @@
], ],
"user": { "user": {
"settings": { "settings": {
"currency": "USD" "currency": "USD",
"performanceCalculationType": "ROAI"
} }
} }
} }

3
test/import/ok/btcusd.json

@ -23,7 +23,8 @@
], ],
"user": { "user": {
"settings": { "settings": {
"currency": "USD" "currency": "USD",
"performanceCalculationType": "ROAI"
} }
} }
} }

3
test/import/ok/derived-currency.json

@ -31,7 +31,8 @@
], ],
"user": { "user": {
"settings": { "settings": {
"currency": "USD" "currency": "USD",
"performanceCalculationType": "ROAI"
} }
} }
} }

3
test/import/ok/novn-buy-and-sell-partially.json

@ -27,7 +27,8 @@
], ],
"user": { "user": {
"settings": { "settings": {
"currency": "CHF" "currency": "CHF",
"performanceCalculationType": "ROAI"
} }
} }
} }

3
test/import/ok/novn-buy-and-sell.json

@ -27,7 +27,8 @@
], ],
"user": { "user": {
"settings": { "settings": {
"currency": "CHF" "currency": "CHF",
"performanceCalculationType": "ROAI"
} }
} }
} }

3
test/import/ok/penthouse-apartment.json

@ -47,7 +47,8 @@
], ],
"user": { "user": {
"settings": { "settings": {
"currency": "USD" "currency": "USD",
"performanceCalculationType": "ROAI"
} }
} }
} }

3
test/import/ok/sample.json

@ -147,7 +147,8 @@
], ],
"user": { "user": {
"settings": { "settings": {
"currency": "USD" "currency": "USD",
"performanceCalculationType": "ROAI"
} }
} }
} }

3
test/import/ok/vti-buy-long-history.json

@ -40,7 +40,8 @@
], ],
"user": { "user": {
"settings": { "settings": {
"currency": "USD" "currency": "USD",
"performanceCalculationType": "ROAI"
} }
} }
} }

3
test/import/ok/without-accounts.json

@ -47,7 +47,8 @@
], ],
"user": { "user": {
"settings": { "settings": {
"currency": "USD" "currency": "USD",
"performanceCalculationType": "ROAI"
} }
} }
} }

Loading…
Cancel
Save