Browse Source

Merge branch 'main' into feat/currency-selector

pull/2487/head
Thomas Kaul 2 years ago
committed by GitHub
parent
commit
2a869e31b7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      CHANGELOG.md
  2. 19
      apps/api/src/app/account/account.controller.ts
  3. 66
      apps/api/src/app/admin/admin.controller.ts
  4. 2
      apps/api/src/app/admin/admin.module.ts
  5. 35
      apps/api/src/app/admin/admin.service.ts
  6. 11
      apps/api/src/app/admin/update-bulk-market-data.dto.ts
  7. 6
      apps/api/src/app/admin/update-market-data.dto.ts
  8. 30
      apps/api/src/app/auth/web-auth.service.ts
  9. 1
      apps/api/src/app/portfolio/portfolio.service.ts
  10. 26
      apps/api/src/services/account-balance/account-balance.service.ts
  11. 9
      apps/api/src/services/api/api.service.ts
  12. 20
      apps/api/src/services/symbol-profile/symbol-profile.service.ts
  13. 3
      apps/client/project.json
  14. 2
      apps/client/src/app/components/accounts-table/accounts-table.component.html
  15. 2
      apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts
  16. 20
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  17. 34
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  18. 30
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  19. 3
      apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.scss
  20. 62
      apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.ts
  21. 38
      apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html
  22. 4
      apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.module.ts
  23. 3
      apps/client/src/app/components/header/header.component.html
  24. 2
      apps/client/src/app/components/user-account-access/user-account-access.module.ts
  25. 24
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  26. 22
      apps/client/src/app/components/user-account-settings/user-account-settings.html
  27. 4
      apps/client/src/app/components/user-account-settings/user-account-settings.module.ts
  28. 6
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  29. 2
      apps/client/src/app/pages/accounts/accounts-page.html
  30. 4
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html
  31. 15
      apps/client/src/app/services/admin.service.ts
  32. 4
      apps/client/src/app/services/web-authn.service.ts
  33. 9
      apps/client/src/styles.scss
  34. 2
      libs/common/src/lib/interfaces/admin-market-data.interface.ts
  35. 2
      libs/common/src/lib/interfaces/index.ts
  36. 6
      libs/common/src/lib/interfaces/position.interface.ts
  37. 5
      libs/common/src/lib/interfaces/responses/account-balances-response.interface.ts
  38. 32
      libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts
  39. 20
      libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.html
  40. 3
      libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.module.ts
  41. 83
      libs/ui/src/lib/assistant/assistant.component.ts
  42. 26
      libs/ui/src/lib/assistant/assistant.html
  43. 11
      libs/ui/src/lib/assistant/interfaces/interfaces.ts
  44. 8
      package.json
  45. 243
      yarn.lock

36
CHANGELOG.md

@ -5,12 +5,33 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ## 2.12.0 - 2023-10-17
### Added
- Added the endpoint `GET api/v1/account/:id/balances` which provides historical cash balances
- Added support to search for an asset profile by `isin`, `name` and `symbol` as an administrator (experimental)
- Added support for creating asset profiles with `MANUAL` data source
### Changed
- Changed the checkboxes to slide toggles in the user settings of the user account page
- Extended the `copy-assets` `Nx` target to copy the locales to the server’s assets
- Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `5.2.1` to `8.3`
### Fixed
- Displayed the transfer cash balance button based on a permission
- Fixed the biometric authentication
- Fixed the query to get asset profiles that match both the `dataSource` and `symbol` values
## 2.11.0 - 2023-10-14
### Added ### Added
- Added support to transfer a part of the cash balance from one to another account - Added support to transfer a part of the cash balance from one to another account
- Extended the markets overview by benchmarks (date of last all time high) - Extended the markets overview by benchmarks (date of last all time high)
- Added support to import historical market data in the admin control panel
### Changed ### Changed
@ -21,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fixed `FEE` and `INTEREST` types in the activities import of `csv` files - Fixed `FEE` and `INTEREST` types in the activities import of `csv` files
- Fixed the displayed currency of the cash balance in the create or update account dialog
## 2.10.0 - 2023-10-09 ## 2.10.0 - 2023-10-09
@ -124,13 +146,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the preselected currency based on the account's currency in the create or edit activity dialog - Improved the preselected currency based on the accounts currency in the create or edit activity dialog
- Unlocked the experimental features setting for all users - Unlocked the experimental features setting for all users
- Upgraded `prisma` from version `5.2.0` to `5.3.1` - Upgraded `prisma` from version `5.2.0` to `5.3.1`
### Fixed ### Fixed
- Fixed a memory leak related to the server's timezone (behind UTC) in the data gathering - Fixed a memory leak related to the servers timezone (behind UTC) in the data gathering
## 2.3.0 - 2023-09-17 ## 2.3.0 - 2023-09-17
@ -281,7 +303,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Optimized the activities import by allowing a different currency than the asset's official one - Optimized the activities import by allowing a different currency than the assets official one
- Added a timeout to the _EOD Historical Data_ requests - Added a timeout to the _EOD Historical Data_ requests
- Migrated the requests from `bent` to `got` in the _EOD Historical Data_ service - Migrated the requests from `bent` to `got` in the _EOD Historical Data_ service
@ -788,7 +810,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Persisted today's market data continuously - Persisted todays market data continuously
### Fixed ### Fixed
@ -1022,7 +1044,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Filtered activities with type `ITEM` from search results - Filtered activities with type `ITEM` from search results
- Considered the user's language in the _Stripe_ checkout - Considered the users language in the _Stripe_ checkout
- Upgraded the _Stripe_ dependencies - Upgraded the _Stripe_ dependencies
- Upgraded `twitter-api-v2` from version `1.10.3` to `1.14.2` - Upgraded `twitter-api-v2` from version `1.10.3` to `1.14.2`
@ -2696,7 +2718,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Moved the countries and sectors charts in the position detail dialog - Moved the countries and sectors charts in the position detail dialog
- Distinguished today's data point of historical data in the admin control panel - Distinguished todays data point of historical data in the admin control panel
- Restructured the server modules - Restructured the server modules
### Fixed ### Fixed

19
apps/api/src/app/account/account.controller.ts

@ -1,8 +1,12 @@
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { Accounts } from '@ghostfolio/common/interfaces'; import {
AccountBalancesResponse,
Accounts
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { import type {
AccountWithValue, AccountWithValue,
@ -35,6 +39,7 @@ import { UpdateAccountDto } from './update-account.dto';
@Controller('account') @Controller('account')
export class AccountController { export class AccountController {
public constructor( public constructor(
private readonly accountBalanceService: AccountBalanceService,
private readonly accountService: AccountService, private readonly accountService: AccountService,
private readonly impersonationService: ImpersonationService, private readonly impersonationService: ImpersonationService,
private readonly portfolioService: PortfolioService, private readonly portfolioService: PortfolioService,
@ -116,6 +121,18 @@ export class AccountController {
return accountsWithAggregations.accounts[0]; return accountsWithAggregations.accounts[0];
} }
@Get(':id/balances')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(RedactValuesInResponseInterceptor)
public async getAccountBalancesById(
@Param('id') id: string
): Promise<AccountBalancesResponse> {
return this.accountBalanceService.getAccountBalances({
accountId: id,
userId: this.request.user.id
});
}
@Post() @Post()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async createAccount( public async createAccount(

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

@ -1,9 +1,9 @@
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service'; import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto'; import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
import { import {
DEFAULT_PAGE_SIZE,
GATHER_ASSET_PROFILE_PROCESS, GATHER_ASSET_PROFILE_PROCESS,
GATHER_ASSET_PROFILE_PROCESS_OPTIONS GATHER_ASSET_PROFILE_PROCESS_OPTIONS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
@ -12,8 +12,7 @@ import {
AdminData, AdminData,
AdminMarketData, AdminMarketData,
AdminMarketDataDetails, AdminMarketDataDetails,
EnhancedSymbolProfile, EnhancedSymbolProfile
Filter
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { import type {
@ -43,12 +42,14 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AdminService } from './admin.service'; import { AdminService } from './admin.service';
import { UpdateAssetProfileDto } from './update-asset-profile.dto'; import { UpdateAssetProfileDto } from './update-asset-profile.dto';
import { UpdateBulkMarketDataDto } from './update-bulk-market-data.dto';
import { UpdateMarketDataDto } from './update-market-data.dto'; import { UpdateMarketDataDto } from './update-market-data.dto';
@Controller('admin') @Controller('admin')
export class AdminController { export class AdminController {
public constructor( public constructor(
private readonly adminService: AdminService, private readonly adminService: AdminService,
private readonly apiService: ApiService,
private readonly dataGatheringService: DataGatheringService, private readonly dataGatheringService: DataGatheringService,
private readonly marketDataService: MarketDataService, private readonly marketDataService: MarketDataService,
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser
@ -254,6 +255,7 @@ export class AdminController {
public async getMarketData( public async getMarketData(
@Query('assetSubClasses') filterByAssetSubClasses?: string, @Query('assetSubClasses') filterByAssetSubClasses?: string,
@Query('presetId') presetId?: MarketDataPreset, @Query('presetId') presetId?: MarketDataPreset,
@Query('query') filterBySearchQuery?: string,
@Query('skip') skip?: number, @Query('skip') skip?: number,
@Query('sortColumn') sortColumn?: string, @Query('sortColumn') sortColumn?: string,
@Query('sortDirection') sortDirection?: Prisma.SortOrder, @Query('sortDirection') sortDirection?: Prisma.SortOrder,
@ -271,16 +273,10 @@ export class AdminController {
); );
} }
const assetSubClasses = filterByAssetSubClasses?.split(',') ?? []; const filters = this.apiService.buildFiltersFromQueryParams({
filterByAssetSubClasses,
const filters: Filter[] = [ filterBySearchQuery
...assetSubClasses.map((assetSubClass) => { });
return <Filter>{
id: assetSubClass,
type: 'ASSET_SUB_CLASS'
};
})
];
return this.adminService.getMarketData({ return this.adminService.getMarketData({
filters, filters,
@ -313,6 +309,43 @@ export class AdminController {
return this.adminService.getMarketDataBySymbol({ dataSource, symbol }); return this.adminService.getMarketDataBySymbol({ dataSource, symbol });
} }
@Post('market-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
public async updateMarketData(
@Body() data: UpdateBulkMarketDataDto,
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
) {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const dataBulkUpdate: Prisma.MarketDataUpdateInput[] = data.marketData.map(
({ date, marketPrice }) => ({
dataSource,
date,
marketPrice,
symbol,
state: 'CLOSE'
})
);
return this.marketDataService.updateMany({
data: dataBulkUpdate
});
}
/**
* @deprecated
*/
@Put('market-data/:dataSource/:symbol/:dateString') @Put('market-data/:dataSource/:symbol/:dateString')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async update( public async update(
@ -365,8 +398,11 @@ export class AdminController {
StatusCodes.FORBIDDEN StatusCodes.FORBIDDEN
); );
} }
return this.adminService.addAssetProfile({
return this.adminService.addAssetProfile({ dataSource, symbol }); dataSource,
symbol,
currency: this.request.user.Settings.settings.baseCurrency
});
} }
@Delete('profile-data/:dataSource/:symbol') @Delete('profile-data/:dataSource/:symbol')

2
apps/api/src/app/admin/admin.module.ts

@ -1,4 +1,5 @@
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
@ -15,6 +16,7 @@ import { QueueModule } from './queue/queue.module';
@Module({ @Module({
imports: [ imports: [
ApiModule,
ConfigurationModule, ConfigurationModule,
DataGatheringModule, DataGatheringModule,
DataProviderModule, DataProviderModule,

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

@ -41,10 +41,19 @@ export class AdminService {
) {} ) {}
public async addAssetProfile({ public async addAssetProfile({
currency,
dataSource, dataSource,
symbol symbol
}: UniqueAsset): Promise<SymbolProfile | never> { }: UniqueAsset & { currency?: string }): Promise<SymbolProfile | never> {
try { try {
if (dataSource === 'MANUAL') {
return this.symbolProfileService.add({
currency,
dataSource,
symbol
});
}
const assetProfiles = await this.dataProviderService.getAssetProfiles([ const assetProfiles = await this.dataProviderService.getAssetProfiles([
{ dataSource, symbol } { dataSource, symbol }
]); ]);
@ -131,10 +140,14 @@ export class AdminService {
filters = [{ id: 'ETF', type: 'ASSET_SUB_CLASS' }]; filters = [{ id: 'ETF', type: 'ASSET_SUB_CLASS' }];
} }
const searchQuery = filters.find(({ type }) => {
return type === 'SEARCH_QUERY';
})?.id;
const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy( const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy(
filters, filters,
(filter) => { ({ type }) => {
return filter.type; return type;
} }
); );
@ -147,6 +160,14 @@ export class AdminService {
where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id]; where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id];
} }
if (searchQuery) {
where.OR = [
{ isin: { mode: 'insensitive', startsWith: searchQuery } },
{ name: { mode: 'insensitive', startsWith: searchQuery } },
{ symbol: { mode: 'insensitive', startsWith: searchQuery } }
];
}
if (sortColumn) { if (sortColumn) {
orderBy = [{ [sortColumn]: sortDirection }]; orderBy = [{ [sortColumn]: sortDirection }];
@ -173,7 +194,9 @@ export class AdminService {
assetSubClass: true, assetSubClass: true,
comment: true, comment: true,
countries: true, countries: true,
currency: true,
dataSource: true, dataSource: true,
name: true,
Order: { Order: {
orderBy: [{ date: 'asc' }], orderBy: [{ date: 'asc' }],
select: { date: true }, select: { date: true },
@ -194,7 +217,9 @@ export class AdminService {
assetSubClass, assetSubClass,
comment, comment,
countries, countries,
currency,
dataSource, dataSource,
name,
Order, Order,
sectors, sectors,
symbol symbol
@ -213,8 +238,10 @@ export class AdminService {
assetClass, assetClass,
assetSubClass, assetSubClass,
comment, comment,
currency,
countriesCount, countriesCount,
dataSource, dataSource,
name,
symbol, symbol,
marketDataItemCount, marketDataItemCount,
sectorsCount, sectorsCount,
@ -341,6 +368,8 @@ export class AdminService {
symbol, symbol,
assetClass: 'CASH', assetClass: 'CASH',
countriesCount: 0, countriesCount: 0,
currency: symbol.replace(DEFAULT_CURRENCY, ''),
name: symbol,
sectorsCount: 0 sectorsCount: 0
}; };
}); });

11
apps/api/src/app/admin/update-bulk-market-data.dto.ts

@ -0,0 +1,11 @@
import { Type } from 'class-transformer';
import { ArrayNotEmpty, IsArray, isNotEmptyObject } from 'class-validator';
import { UpdateMarketDataDto } from './update-market-data.dto';
export class UpdateBulkMarketDataDto {
@ArrayNotEmpty()
@IsArray()
@Type(() => UpdateMarketDataDto)
marketData: UpdateMarketDataDto[];
}

6
apps/api/src/app/admin/update-market-data.dto.ts

@ -1,6 +1,10 @@
import { IsNumber } from 'class-validator'; import { IsDate, IsNumber, IsOptional } from 'class-validator';
export class UpdateMarketDataDto { export class UpdateMarketDataDto {
@IsDate()
@IsOptional()
date?: Date;
@IsNumber() @IsNumber()
marketPrice: number; marketPrice: number;
} }

30
apps/api/src/app/auth/web-auth.service.ts

@ -64,7 +64,7 @@ export class WebAuthService {
} }
}; };
const options = generateRegistrationOptions(opts); const options = await generateRegistrationOptions(opts);
await this.userService.updateUser({ await this.userService.updateUser({
data: { data: {
@ -88,10 +88,16 @@ export class WebAuthService {
let verification: VerifiedRegistrationResponse; let verification: VerifiedRegistrationResponse;
try { try {
const opts: VerifyRegistrationResponseOpts = { const opts: VerifyRegistrationResponseOpts = {
credential,
expectedChallenge, expectedChallenge,
expectedOrigin: this.expectedOrigin, expectedOrigin: this.expectedOrigin,
expectedRPID: this.rpID expectedRPID: this.rpID,
response: {
clientExtensionResults: credential.clientExtensionResults,
id: credential.id,
rawId: credential.rawId,
response: credential.response,
type: 'public-key'
}
}; };
verification = await verifyRegistrationResponse(opts); verification = await verifyRegistrationResponse(opts);
} catch (error) { } catch (error) {
@ -117,8 +123,8 @@ export class WebAuthService {
*/ */
existingDevice = await this.deviceService.createAuthDevice({ existingDevice = await this.deviceService.createAuthDevice({
counter, counter,
credentialPublicKey, credentialId: Buffer.from(credentialID),
credentialId: credentialID, credentialPublicKey: Buffer.from(credentialPublicKey),
User: { connect: { id: user.id } } User: { connect: { id: user.id } }
}); });
} }
@ -152,7 +158,7 @@ export class WebAuthService {
userVerification: 'preferred' userVerification: 'preferred'
}; };
const options = generateAuthenticationOptions(opts); const options = await generateAuthenticationOptions(opts);
await this.userService.updateUser({ await this.userService.updateUser({
data: { data: {
@ -181,7 +187,6 @@ export class WebAuthService {
let verification: VerifiedAuthenticationResponse; let verification: VerifiedAuthenticationResponse;
try { try {
const opts: VerifyAuthenticationResponseOpts = { const opts: VerifyAuthenticationResponseOpts = {
credential,
authenticator: { authenticator: {
credentialID: device.credentialId, credentialID: device.credentialId,
credentialPublicKey: device.credentialPublicKey, credentialPublicKey: device.credentialPublicKey,
@ -189,9 +194,16 @@ export class WebAuthService {
}, },
expectedChallenge: `${user.authChallenge}`, expectedChallenge: `${user.authChallenge}`,
expectedOrigin: this.expectedOrigin, expectedOrigin: this.expectedOrigin,
expectedRPID: this.rpID expectedRPID: this.rpID,
response: {
clientExtensionResults: credential.clientExtensionResults,
id: credential.id,
rawId: credential.rawId,
response: credential.response,
type: 'public-key'
}
}; };
verification = verifyAuthenticationResponse(opts); verification = await verifyAuthenticationResponse(opts);
} catch (error) { } catch (error) {
Logger.error(error, 'WebAuthService'); Logger.error(error, 'WebAuthService');
throw new InternalServerErrorException({ error: error.message }); throw new InternalServerErrorException({ error: error.message });

1
apps/api/src/app/portfolio/portfolio.service.ts

@ -1088,6 +1088,7 @@ export class PortfolioService {
return { return {
...position, ...position,
assetClass: symbolProfileMap[position.symbol].assetClass, assetClass: symbolProfileMap[position.symbol].assetClass,
assetSubClass: symbolProfileMap[position.symbol].assetSubClass,
averagePrice: new Big(position.averagePrice).toNumber(), averagePrice: new Big(position.averagePrice).toNumber(),
grossPerformance: position.grossPerformance?.toNumber() ?? null, grossPerformance: position.grossPerformance?.toNumber() ?? null,
grossPerformancePercentage: grossPerformancePercentage:

26
apps/api/src/services/account-balance/account-balance.service.ts

@ -1,4 +1,5 @@
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { AccountBalancesResponse } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { AccountBalance, Prisma } from '@prisma/client'; import { AccountBalance, Prisma } from '@prisma/client';
@ -13,4 +14,29 @@ export class AccountBalanceService {
data data
}); });
} }
public async getAccountBalances({
accountId,
userId
}: {
accountId: string;
userId: string;
}): Promise<AccountBalancesResponse> {
const balances = await this.prismaService.accountBalance.findMany({
orderBy: {
date: 'asc'
},
select: {
date: true,
id: true,
value: true
},
where: {
accountId,
userId
}
});
return { balances };
}
} }

9
apps/api/src/services/api/api.service.ts

@ -8,16 +8,19 @@ export class ApiService {
public buildFiltersFromQueryParams({ public buildFiltersFromQueryParams({
filterByAccounts, filterByAccounts,
filterByAssetClasses, filterByAssetClasses,
filterByAssetSubClasses,
filterBySearchQuery, filterBySearchQuery,
filterByTags filterByTags
}: { }: {
filterByAccounts?: string; filterByAccounts?: string;
filterByAssetClasses?: string; filterByAssetClasses?: string;
filterByAssetSubClasses?: string;
filterBySearchQuery?: string; filterBySearchQuery?: string;
filterByTags?: string; filterByTags?: string;
}): Filter[] { }): Filter[] {
const accountIds = filterByAccounts?.split(',') ?? []; const accountIds = filterByAccounts?.split(',') ?? [];
const assetClasses = filterByAssetClasses?.split(',') ?? []; const assetClasses = filterByAssetClasses?.split(',') ?? [];
const assetSubClasses = filterByAssetSubClasses?.split(',') ?? [];
const searchQuery = filterBySearchQuery?.toLowerCase(); const searchQuery = filterBySearchQuery?.toLowerCase();
const tagIds = filterByTags?.split(',') ?? []; const tagIds = filterByTags?.split(',') ?? [];
@ -34,6 +37,12 @@ export class ApiService {
type: 'ASSET_CLASS' type: 'ASSET_CLASS'
}; };
}), }),
...assetSubClasses.map((assetClass) => {
return <Filter>{
id: assetClass,
type: 'ASSET_SUB_CLASS'
};
}),
{ {
id: searchQuery, id: searchQuery,
type: 'SEARCH_QUERY' type: 'SEARCH_QUERY'

20
apps/api/src/services/symbol-profile/symbol-profile.service.ts

@ -52,20 +52,12 @@ export class SymbolProfileService {
SymbolProfileOverrides: true SymbolProfileOverrides: true
}, },
where: { where: {
AND: [ OR: aUniqueAssets.map(({ dataSource, symbol }) => {
{ return {
dataSource: { dataSource,
in: aUniqueAssets.map(({ dataSource }) => { symbol
return dataSource; };
}) })
},
symbol: {
in: aUniqueAssets.map(({ symbol }) => {
return symbol;
})
}
}
]
} }
}) })
.then((symbolProfiles) => this.getSymbols(symbolProfiles)); .then((symbolProfiles) => this.getSymbols(symbolProfiles));

3
apps/client/project.json

@ -124,6 +124,9 @@
{ {
"command": "shx cp apps/client/src/assets/site.webmanifest dist/apps/client" "command": "shx cp apps/client/src/assets/site.webmanifest dist/apps/client"
}, },
{
"command": "shx cp -r apps/client/src/locales dist/apps/api/assets"
},
{ {
"command": "shx cp node_modules/ionicons/dist/index.js dist/apps/client" "command": "shx cp node_modules/ionicons/dist/index.js dist/apps/client"
}, },

2
apps/client/src/app/components/accounts-table/accounts-table.component.html

@ -1,4 +1,4 @@
<div class="d-flex justify-content-end"> <div *ngIf="showActions" class="d-flex justify-content-end">
<button <button
class="align-items-center d-flex" class="align-items-center d-flex"
mat-stroked-button mat-stroked-button

2
apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts

@ -177,7 +177,7 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ withRefresh }) => { .subscribe(({ withRefresh } = { withRefresh: false }) => {
this.marketDataChanged.next(withRefresh); this.marketDataChanged.next(withRefresh);
}); });
} }

20
apps/client/src/app/components/admin-market-data/admin-market-data.component.ts

@ -178,10 +178,20 @@ export class AdminMarketDataComponent
} }
public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) { public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) {
this.adminService const confirmation = confirm(
.deleteProfileData({ dataSource, symbol }) $localize`Do you really want to delete this asset profile?`
.pipe(takeUntil(this.unsubscribeSubject)) );
.subscribe(() => {});
if (confirmation) {
this.adminService
.deleteProfileData({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
setTimeout(() => {
window.location.reload();
}, 300);
});
}
} }
public onGather7Days() { public onGather7Days() {
@ -342,7 +352,7 @@ export class AdminMarketDataComponent
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ dataSource, symbol }) => { .subscribe(({ dataSource, symbol } = {}) => {
if (dataSource && symbol) { if (dataSource && symbol) {
this.adminService this.adminService
.addAssetProfile({ dataSource, symbol }) .addAssetProfile({ dataSource, symbol })

34
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts

@ -11,12 +11,15 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto'; import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { import {
AdminMarketDataDetails, AdminMarketDataDetails,
UniqueAsset UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { MarketData, SymbolProfile } from '@prisma/client'; import { MarketData, SymbolProfile } from '@prisma/client';
import { format, parseISO } from 'date-fns';
import { parse as csvToJson } from 'papaparse';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -42,12 +45,17 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
public countries: { public countries: {
[code: string]: { name: string; value: number }; [code: string]: { name: string; value: number };
}; };
public historicalDataAsCsvString: string;
public isBenchmark = false; public isBenchmark = false;
public marketDataDetails: MarketData[] = []; public marketDataDetails: MarketData[] = [];
public sectors: { public sectors: {
[name: string]: { name: string; value: number }; [name: string]: { name: string; value: number };
}; };
private static readonly HISTORICAL_DATA_TEMPLATE = `date;marketPrice\n${format(
new Date(),
DATE_FORMAT
)};123.45`;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
@ -66,6 +74,9 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
} }
public initialize() { public initialize() {
this.historicalDataAsCsvString =
AssetProfileDialog.HISTORICAL_DATA_TEMPLATE;
this.adminService this.adminService
.fetchAdminMarketDataBySymbol({ .fetchAdminMarketDataBySymbol({
dataSource: this.data.dataSource, dataSource: this.data.dataSource,
@ -134,6 +145,29 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
.subscribe(() => {}); .subscribe(() => {});
} }
public onImportHistoricalData() {
const marketData = csvToJson(this.historicalDataAsCsvString, {
dynamicTyping: true,
header: true,
skipEmptyLines: true
}).data;
this.adminService
.postMarketData({
dataSource: this.data.dataSource,
marketData: {
marketData: marketData.map(({ date, marketPrice }) => {
return { marketPrice, date: parseISO(date) };
})
},
symbol: this.data.symbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.initialize();
});
}
public onMarketDataChanged(withRefresh: boolean = false) { public onMarketDataChanged(withRefresh: boolean = false) {
if (withRefresh) { if (withRefresh) {
this.initialize(); this.initialize();

30
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -51,6 +51,36 @@
[symbol]="data.symbol" [symbol]="data.symbol"
(marketDataChanged)="onMarketDataChanged($event)" (marketDataChanged)="onMarketDataChanged($event)"
></gf-admin-market-data-detail> ></gf-admin-market-data-detail>
<div class="mt-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label>
<ng-container i18n>Historical Data</ng-container> (CSV)
</mat-label>
<textarea
cdkAutosizeMaxRows="5"
cdkTextareaAutosize
matInput
placeholder="e.g. 20230601;1.61"
type="text"
[ngModelOptions]="{standalone: true}"
[(ngModel)]="historicalDataAsCsvString"
(keyup.enter)="$event.stopPropagation()"
></textarea>
</mat-form-field>
</div>
<div class="d-flex justify-content-end mt-2">
<button
color="accent"
mat-flat-button
type="button"
(click)="onImportHistoricalData()"
>
<ng-container i18n>Import</ng-container>
</button>
</div>
<div class="row"> <div class="row">
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value i18n size="medium" [value]="assetProfile?.symbol" <gf-value i18n size="medium" [value]="assetProfile?.symbol"

3
apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.scss

@ -0,0 +1,3 @@
:host {
display: block;
}

62
apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.ts

@ -1,15 +1,15 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef,
Component, Component,
Inject,
OnDestroy, OnDestroy,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { import {
AbstractControl,
FormBuilder, FormBuilder,
FormControl, FormControl,
FormGroup, FormGroup,
ValidationErrors,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { MatDialogRef } from '@angular/material/dialog'; import { MatDialogRef } from '@angular/material/dialog';
@ -19,35 +19,75 @@ import { AdminService } from '@ghostfolio/client/services/admin.service';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'h-100' }, host: { class: 'h-100' },
selector: 'gf-create-asset-profile-dialog', selector: 'gf-create-asset-profile-dialog',
styleUrls: ['./create-asset-profile-dialog.component.scss'],
templateUrl: 'create-asset-profile-dialog.html' templateUrl: 'create-asset-profile-dialog.html'
}) })
export class CreateAssetProfileDialog implements OnInit, OnDestroy { export class CreateAssetProfileDialog implements OnInit, OnDestroy {
public createAssetProfileForm: FormGroup; public createAssetProfileForm: FormGroup;
public mode: 'auto' | 'manual';
public constructor( public constructor(
public readonly adminService: AdminService, public readonly adminService: AdminService,
public readonly changeDetectorRef: ChangeDetectorRef,
public readonly dialogRef: MatDialogRef<CreateAssetProfileDialog>, public readonly dialogRef: MatDialogRef<CreateAssetProfileDialog>,
public readonly formBuilder: FormBuilder public readonly formBuilder: FormBuilder
) {} ) {}
public ngOnInit() { public ngOnInit() {
this.createAssetProfileForm = this.formBuilder.group({ this.createAssetProfileForm = this.formBuilder.group(
searchSymbol: new FormControl(null, [Validators.required]) {
}); addSymbol: new FormControl(null, [Validators.required]),
searchSymbol: new FormControl(null, [Validators.required])
},
{
validators: this.atLeastOneValid
}
);
this.mode = 'auto';
} }
public onCancel() { public onCancel() {
this.dialogRef.close(); this.dialogRef.close();
} }
public onRadioChange(mode: 'auto' | 'manual') {
this.mode = mode;
}
public onSubmit() { public onSubmit() {
this.dialogRef.close({ this.mode === 'auto'
dataSource: ? this.dialogRef.close({
this.createAssetProfileForm.controls['searchSymbol'].value.dataSource, dataSource:
symbol: this.createAssetProfileForm.controls['searchSymbol'].value.symbol this.createAssetProfileForm.controls['searchSymbol'].value
}); .dataSource,
symbol:
this.createAssetProfileForm.controls['searchSymbol'].value.symbol
})
: this.dialogRef.close({
dataSource: 'MANUAL',
symbol: this.createAssetProfileForm.controls['addSymbol'].value
});
} }
public ngOnDestroy() {} public ngOnDestroy() {}
private atLeastOneValid(control: AbstractControl): ValidationErrors {
const addSymbolControl = control.get('addSymbol');
const searchSymbolControl = control.get('searchSymbol');
if (addSymbolControl.valid && searchSymbolControl.valid) {
return { atLeastOneValid: true };
}
if (
addSymbolControl.valid ||
!addSymbolControl ||
searchSymbolControl.valid ||
!searchSymbolControl
) {
return { atLeastOneValid: false };
}
return { atLeastOneValid: true };
}
} }

38
apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html

@ -6,13 +6,35 @@
> >
<h1 i18n mat-dialog-title>Add Asset Profile</h1> <h1 i18n mat-dialog-title>Add Asset Profile</h1>
<div class="flex-grow-1 py-3" mat-dialog-content> <div class="flex-grow-1 py-3" mat-dialog-content>
<mat-form-field appearance="outline" class="w-100"> <div class="mb-3">
<mat-label i18n>Name, symbol or ISIN</mat-label> <mat-radio-group
<gf-symbol-autocomplete color="primary"
formControlName="searchSymbol" [value]="mode"
[includeIndices]="true" (change)="onRadioChange($event.value)"
/> >
</mat-form-field> <mat-radio-button name="auto" value="auto"></mat-radio-button>
<label class="m-0" for="auto" i18n>Search</label>
<mat-radio-button class="ml-3" name="manual" value="manual">
</mat-radio-button>
<label class="m-0" for="manual" i18n>Add Manually</label>
</mat-radio-group>
</div>
<div *ngIf="mode === 'auto'">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Name, symbol or ISIN</mat-label>
<gf-symbol-autocomplete
formControlName="searchSymbol"
[includeIndices]="true"
/>
</mat-form-field>
</div>
<div *ngIf="mode === 'manual'">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Symbol</mat-label>
<input formControlName="addSymbol" matInput />
</mat-form-field>
</div>
</div> </div>
<div class="d-flex justify-content-end" mat-dialog-actions> <div class="d-flex justify-content-end" mat-dialog-actions>
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button> <button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
@ -20,7 +42,7 @@
color="primary" color="primary"
mat-flat-button mat-flat-button
type="submit" type="submit"
[disabled]="!createAssetProfileForm.valid" [disabled]="createAssetProfileForm.hasError('atLeastOneValid')"
> >
<ng-container i18n>Save</ng-container> <ng-container i18n>Save</ng-container>
</button> </button>

4
apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.module.ts

@ -4,6 +4,8 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } 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 { MatRadioModule } from '@angular/material/radio';
import { GfSymbolAutocompleteModule } from '@ghostfolio/ui/symbol-autocomplete'; import { GfSymbolAutocompleteModule } from '@ghostfolio/ui/symbol-autocomplete';
import { CreateAssetProfileDialog } from './create-asset-profile-dialog.component'; import { CreateAssetProfileDialog } from './create-asset-profile-dialog.component';
@ -17,6 +19,8 @@ import { CreateAssetProfileDialog } from './create-asset-profile-dialog.componen
MatDialogModule, MatDialogModule,
MatButtonModule, MatButtonModule,
MatFormFieldModule, MatFormFieldModule,
MatInputModule,
MatRadioModule,
ReactiveFormsModule ReactiveFormsModule
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]

3
apps/client/src/app/components/header/header.component.html

@ -131,6 +131,9 @@
<gf-assistant <gf-assistant
#assistant #assistant
[deviceType]="deviceType" [deviceType]="deviceType"
[hasPermissionToAccessAdminControl]="
hasPermissionToAccessAdminControl
"
(closed)="closeAssistant()" (closed)="closeAssistant()"
/> />
</mat-menu> </mat-menu>

2
apps/client/src/app/components/user-account-access/user-account-access.module.ts

@ -1,5 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module'; import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
@ -7,7 +8,6 @@ import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-dialog/create-or-update-access-dialog.module'; import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-dialog/create-or-update-access-dialog.module';
import { UserAccountAccessComponent } from './user-account-access.component'; import { UserAccountAccessComponent } from './user-account-access.component';
import { MatButtonModule } from '@angular/material/button';
@NgModule({ @NgModule({
declarations: [UserAccountAccessComponent], declarations: [UserAccountAccessComponent],

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

@ -3,10 +3,9 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
OnDestroy, OnDestroy,
OnInit, OnInit
ViewChild
} from '@angular/core'; } from '@angular/core';
import { MatCheckbox, MatCheckboxChange } from '@angular/material/checkbox'; import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { import {
STAY_SIGNED_IN, STAY_SIGNED_IN,
@ -29,14 +28,12 @@ import { catchError, takeUntil } from 'rxjs/operators';
templateUrl: './user-account-settings.html' templateUrl: './user-account-settings.html'
}) })
export class UserAccountSettingsComponent implements OnDestroy, OnInit { export class UserAccountSettingsComponent implements OnDestroy, OnInit {
@ViewChild('toggleSignInWithFingerprintEnabledElement')
signInWithFingerprintElement: MatCheckbox;
public appearancePlaceholder = $localize`Auto`; public appearancePlaceholder = $localize`Auto`;
public baseCurrency: string; public baseCurrency: string;
public currencies: string[] = []; public currencies: string[] = [];
public hasPermissionToUpdateViewMode: boolean; public hasPermissionToUpdateViewMode: boolean;
public hasPermissionToUpdateUserSettings: boolean; public hasPermissionToUpdateUserSettings: boolean;
public isWebAuthnEnabled: boolean;
public language = document.documentElement.lang; public language = document.documentElement.lang;
public locales = [ public locales = [
'de', 'de',
@ -120,7 +117,7 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
}); });
} }
public onExperimentalFeaturesChange(aEvent: MatCheckboxChange) { public onExperimentalFeaturesChange(aEvent: MatSlideToggleChange) {
this.dataService this.dataService
.putUserSetting({ isExperimentalFeatures: aEvent.checked }) .putUserSetting({ isExperimentalFeatures: aEvent.checked })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
@ -158,7 +155,7 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
}); });
} }
public onRestrictedViewChange(aEvent: MatCheckboxChange) { public onRestrictedViewChange(aEvent: MatSlideToggleChange) {
this.dataService this.dataService
.putUserSetting({ isRestrictedView: aEvent.checked }) .putUserSetting({ isRestrictedView: aEvent.checked })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
@ -176,7 +173,7 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
}); });
} }
public onSignInWithFingerprintChange(aEvent: MatCheckboxChange) { public onSignInWithFingerprintChange(aEvent: MatSlideToggleChange) {
if (aEvent.checked) { if (aEvent.checked) {
this.registerDevice(); this.registerDevice();
} else { } else {
@ -192,7 +189,7 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
} }
} }
public onViewModeChange(aEvent: MatCheckboxChange) { public onViewModeChange(aEvent: MatSlideToggleChange) {
this.dataService this.dataService
.putUserSetting({ viewMode: aEvent.checked === true ? 'ZEN' : 'DEFAULT' }) .putUserSetting({ viewMode: aEvent.checked === true ? 'ZEN' : 'DEFAULT' })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
@ -250,9 +247,8 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
} }
private update() { private update() {
if (this.signInWithFingerprintElement) { this.isWebAuthnEnabled = this.webAuthnService.isEnabled() ?? false;
this.signInWithFingerprintElement.checked =
this.webAuthnService.isEnabled() ?? false; this.changeDetectorRef.markForCheck();
}
} }
} }

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

@ -11,12 +11,13 @@
</div> </div>
</div> </div>
<div class="pl-1 w-50"> <div class="pl-1 w-50">
<mat-checkbox <mat-slide-toggle
color="primary" color="primary"
hideIcon="true"
[checked]="user.settings.isRestrictedView" [checked]="user.settings.isRestrictedView"
[disabled]="!hasPermissionToUpdateUserSettings" [disabled]="!hasPermissionToUpdateUserSettings"
(change)="onRestrictedViewChange($event)" (change)="onRestrictedViewChange($event)"
></mat-checkbox> ></mat-slide-toggle>
</div> </div>
</div> </div>
<div class="d-flex mt-4 py-1"> <div class="d-flex mt-4 py-1">
@ -139,12 +140,13 @@
</div> </div>
</div> </div>
<div class="pl-1 w-50"> <div class="pl-1 w-50">
<mat-checkbox <mat-slide-toggle
color="primary" color="primary"
hideIcon="true"
[checked]="user.settings.viewMode === 'ZEN'" [checked]="user.settings.viewMode === 'ZEN'"
[disabled]="!hasPermissionToUpdateViewMode" [disabled]="!hasPermissionToUpdateViewMode"
(change)="onViewModeChange($event)" (change)="onViewModeChange($event)"
></mat-checkbox> ></mat-slide-toggle>
</div> </div>
</div> </div>
<div class="align-items-center d-flex mt-4 py-1"> <div class="align-items-center d-flex mt-4 py-1">
@ -153,12 +155,13 @@
<div class="hint-text text-muted" i18n>Sign in with fingerprint</div> <div class="hint-text text-muted" i18n>Sign in with fingerprint</div>
</div> </div>
<div class="pl-1 w-50"> <div class="pl-1 w-50">
<mat-checkbox <mat-slide-toggle
#toggleSignInWithFingerprintEnabledElement
color="primary" color="primary"
hideIcon="true"
[checked]="isWebAuthnEnabled === true"
[disabled]="!hasPermissionToUpdateUserSettings" [disabled]="!hasPermissionToUpdateUserSettings"
(change)="onSignInWithFingerprintChange($event)" (change)="onSignInWithFingerprintChange($event)"
></mat-checkbox> ></mat-slide-toggle>
</div> </div>
</div> </div>
<div <div
@ -172,12 +175,13 @@
</div> </div>
</div> </div>
<div class="pl-1 w-50"> <div class="pl-1 w-50">
<mat-checkbox <mat-slide-toggle
color="primary" color="primary"
hideIcon="true"
[checked]="user.settings.isExperimentalFeatures" [checked]="user.settings.isExperimentalFeatures"
[disabled]="!hasPermissionToUpdateUserSettings" [disabled]="!hasPermissionToUpdateUserSettings"
(change)="onExperimentalFeaturesChange($event)" (change)="onExperimentalFeaturesChange($event)"
></mat-checkbox> ></mat-slide-toggle>
</div> </div>
</div> </div>
<div class="align-items-center d-flex mt-4 py-1"> <div class="align-items-center d-flex mt-4 py-1">

4
apps/client/src/app/components/user-account-settings/user-account-settings.module.ts

@ -3,9 +3,9 @@ import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
@ -20,9 +20,9 @@ import { UserAccountSettingsComponent } from './user-account-settings.component'
GfValueModule, GfValueModule,
MatButtonModule, MatButtonModule,
MatCardModule, MatCardModule,
MatCheckboxModule,
MatFormFieldModule, MatFormFieldModule,
MatSelectModule, MatSelectModule,
MatSlideToggleModule,
ReactiveFormsModule, ReactiveFormsModule,
RouterModule RouterModule
] ]

6
apps/client/src/app/pages/accounts/accounts-page.component.ts

@ -30,7 +30,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
public deviceType: string; public deviceType: string;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public hasPermissionToCreateAccount: boolean; public hasPermissionToCreateAccount: boolean;
public hasPermissionToDeleteAccount: boolean; public hasPermissionToUpdateAccount: boolean;
public routeQueryParams: Subscription; public routeQueryParams: Subscription;
public totalBalanceInBaseCurrency = 0; public totalBalanceInBaseCurrency = 0;
public totalValueInBaseCurrency = 0; public totalValueInBaseCurrency = 0;
@ -95,9 +95,9 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
this.user.permissions, this.user.permissions,
permissions.createAccount permissions.createAccount
); );
this.hasPermissionToDeleteAccount = hasPermission( this.hasPermissionToUpdateAccount = hasPermission(
this.user.permissions, this.user.permissions,
permissions.deleteAccount permissions.updateAccount
); );
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();

2
apps/client/src/app/pages/accounts/accounts-page.html

@ -8,7 +8,7 @@
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType" [deviceType]="deviceType"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccount && !user.settings.isRestrictedView" [showActions]="!hasImpersonationId && hasPermissionToUpdateAccount && !user.settings.isRestrictedView"
[totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency" [totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency"
[totalValueInBaseCurrency]="totalValueInBaseCurrency" [totalValueInBaseCurrency]="totalValueInBaseCurrency"
[transactionCount]="transactionCount" [transactionCount]="transactionCount"

4
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html

@ -35,7 +35,9 @@
type="number" type="number"
(keydown.enter)="$event.stopPropagation()" (keydown.enter)="$event.stopPropagation()"
/> />
<span class="ml-2" matTextSuffix>{{ data.account.currency }}</span> <span class="ml-2" matTextSuffix
>{{ accountForm.controls['currency'].value }}</span
>
</mat-form-field> </mat-form-field>
</div> </div>
<div [ngClass]="{ 'd-none': platforms?.length < 1 }"> <div [ngClass]="{ 'd-none': platforms?.length < 1 }">

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

@ -1,6 +1,7 @@
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto'; import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
import { UpdateBulkMarketDataDto } from '@ghostfolio/api/app/admin/update-bulk-market-data.dto';
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto'; import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto'; import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto';
import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto'; import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto';
@ -214,6 +215,20 @@ export class AdminService {
); );
} }
public postMarketData({
dataSource,
marketData,
symbol
}: {
dataSource: DataSource;
marketData: UpdateBulkMarketDataDto;
symbol: string;
}) {
const url = `/api/v1/admin/market-data/${dataSource}/${symbol}`;
return this.http.post<MarketData>(url, marketData);
}
public postPlatform(aPlatform: CreatePlatformDto) { public postPlatform(aPlatform: CreatePlatformDto) {
return this.http.post<Platform>(`/api/v1/platform`, aPlatform); return this.http.post<Platform>(`/api/v1/platform`, aPlatform);
} }

4
apps/client/src/app/services/web-authn.service.ts

@ -88,7 +88,9 @@ export class WebAuthnService {
{ deviceId } { deviceId }
) )
.pipe( .pipe(
switchMap(startAuthentication), switchMap((requestOptionsJSON) =>
startAuthentication(requestOptionsJSON, true)
),
switchMap((assertionResponse) => { switchMap((assertionResponse) => {
return this.http.post<{ authToken: string }>( return this.http.post<{ authToken: string }>(
`/api/v1/auth/webauthn/verify-assertion`, `/api/v1/auth/webauthn/verify-assertion`,

9
apps/client/src/styles.scss

@ -462,6 +462,15 @@ ngx-skeleton-loader {
} }
} }
/**
* Fix for https://github.com/angular/components/issues/26818
*/
.mat-mdc-slide-toggle {
.mdc-switch__track {
background-color: rgba(var(--palette-primary-500), 1);
}
}
.mat-stepper-vertical, .mat-stepper-vertical,
.mat-stepper-horizontal { .mat-stepper-horizontal {
background: transparent !important; background: transparent !important;

2
libs/common/src/lib/interfaces/admin-market-data.interface.ts

@ -9,9 +9,11 @@ export interface AdminMarketDataItem {
assetClass?: AssetClass; assetClass?: AssetClass;
assetSubClass?: AssetSubClass; assetSubClass?: AssetSubClass;
countriesCount: number; countriesCount: number;
currency: string;
dataSource: DataSource; dataSource: DataSource;
date?: Date; date?: Date;
marketDataItemCount: number; marketDataItemCount: number;
name: string;
sectorsCount: number; sectorsCount: number;
symbol: string; symbol: string;
} }

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

@ -33,6 +33,7 @@ import type { PortfolioReport } from './portfolio-report.interface';
import type { PortfolioSummary } from './portfolio-summary.interface'; import type { PortfolioSummary } from './portfolio-summary.interface';
import type { Position } from './position.interface'; import type { Position } from './position.interface';
import type { Product } from './product'; import type { Product } from './product';
import type { AccountBalancesResponse } from './responses/account-balances-response.interface';
import type { BenchmarkResponse } from './responses/benchmark-response.interface'; import type { BenchmarkResponse } from './responses/benchmark-response.interface';
import type { ResponseError } from './responses/errors.interface'; import type { ResponseError } from './responses/errors.interface';
import type { ImportResponse } from './responses/import-response.interface'; import type { ImportResponse } from './responses/import-response.interface';
@ -49,6 +50,7 @@ import type { User } from './user.interface';
export { export {
Access, Access,
AccountBalancesResponse,
Accounts, Accounts,
AdminData, AdminData,
AdminJobs, AdminJobs,

6
libs/common/src/lib/interfaces/position.interface.ts

@ -1,9 +1,9 @@
import { AssetClass, DataSource } from '@prisma/client'; import { MarketState } from '@ghostfolio/common/types';
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
import { MarketState } from '../types';
export interface Position { export interface Position {
assetClass: AssetClass; assetClass: AssetClass;
assetSubClass: AssetSubClass;
averagePrice: number; averagePrice: number;
currency: string; currency: string;
dataSource: DataSource; dataSource: DataSource;

5
libs/common/src/lib/interfaces/responses/account-balances-response.interface.ts

@ -0,0 +1,5 @@
import { AccountBalance } from '@prisma/client';
export interface AccountBalancesResponse {
balances: Pick<AccountBalance, 'date' | 'id' | 'value'>[];
}

32
libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts

@ -7,10 +7,12 @@ import {
EventEmitter, EventEmitter,
HostBinding, HostBinding,
Input, Input,
OnChanges,
Output, Output,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { Position } from '@ghostfolio/common/interfaces'; import { Params } from '@angular/router';
import { ISearchResultItem } from '@ghostfolio/ui/assistant/interfaces/interfaces';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -18,22 +20,46 @@ import { Position } from '@ghostfolio/common/interfaces';
templateUrl: './assistant-list-item.html', templateUrl: './assistant-list-item.html',
styleUrls: ['./assistant-list-item.scss'] styleUrls: ['./assistant-list-item.scss']
}) })
export class AssistantListItemComponent implements FocusableOption { export class AssistantListItemComponent implements FocusableOption, OnChanges {
@HostBinding('attr.tabindex') tabindex = -1; @HostBinding('attr.tabindex') tabindex = -1;
@HostBinding('class.has-focus') get getHasFocus() { @HostBinding('class.has-focus') get getHasFocus() {
return this.hasFocus; return this.hasFocus;
} }
@Input() holding: Position; @Input() item: ISearchResultItem;
@Input() mode: 'assetProfile' | 'holding';
@Output() clicked = new EventEmitter<void>(); @Output() clicked = new EventEmitter<void>();
@ViewChild('link') public linkElement: ElementRef; @ViewChild('link') public linkElement: ElementRef;
public hasFocus = false; public hasFocus = false;
public queryParams: Params;
public routerLink: string[];
public constructor(private changeDetectorRef: ChangeDetectorRef) {} public constructor(private changeDetectorRef: ChangeDetectorRef) {}
public ngOnChanges() {
const dataSource = this.item?.dataSource;
const symbol = this.item?.symbol;
if (this.mode === 'assetProfile') {
this.queryParams = {
dataSource,
symbol,
assetProfileDialog: true
};
this.routerLink = ['/admin', 'market-data'];
} else if (this.mode === 'holding') {
this.queryParams = {
dataSource,
symbol,
positionDetailDialog: true
};
this.routerLink = ['/portfolio', 'holdings'];
}
}
public focus() { public focus() {
this.hasFocus = true; this.hasFocus = true;

20
libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.html

@ -1,12 +1,16 @@
<a <a
#link #link
class="d-block px-2 py-1 text-truncate" class="d-block line-height-1 px-2 py-1 text-truncate"
[queryParams]="{ [queryParams]="queryParams"
dataSource: holding?.dataSource, [routerLink]="routerLink"
positionDetailDialog: true,
symbol: holding?.symbol
}"
[routerLink]="['/portfolio', 'holdings']"
(click)="onClick()" (click)="onClick()"
>{{ holding?.name }}</a ><span><b>{{ item?.name }}</b></span>
<br />
<small class="text-muted"
>{{ item?.symbol | gfSymbol }} · {{ item?.currency }}<ng-container
*ngIf="item?.assetSubClassString"
>
· {{ item?.assetSubClassString }}</ng-container
></small
></a
> >

3
libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.module.ts

@ -1,12 +1,13 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { AssistantListItemComponent } from './assistant-list-item.component'; import { AssistantListItemComponent } from './assistant-list-item.component';
@NgModule({ @NgModule({
declarations: [AssistantListItemComponent], declarations: [AssistantListItemComponent],
exports: [AssistantListItemComponent], exports: [AssistantListItemComponent],
imports: [CommonModule, RouterModule] imports: [CommonModule, GfSymbolModule, RouterModule]
}) })
export class GfAssistantListItemModule {} export class GfAssistantListItemModule {}

83
libs/ui/src/lib/assistant/assistant.component.ts

@ -16,9 +16,10 @@ import {
} from '@angular/core'; } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { MatMenuTrigger } from '@angular/material/menu'; import { MatMenuTrigger } from '@angular/material/menu';
import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { Position } from '@ghostfolio/common/interfaces'; import { translate } from '@ghostfolio/ui/i18n';
import { EMPTY, Subject, lastValueFrom } from 'rxjs'; import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs';
import { import {
catchError, catchError,
debounceTime, debounceTime,
@ -29,13 +30,13 @@ import {
} from 'rxjs/operators'; } from 'rxjs/operators';
import { AssistantListItemComponent } from './assistant-list-item/assistant-list-item.component'; import { AssistantListItemComponent } from './assistant-list-item/assistant-list-item.component';
import { ISearchResults } from './interfaces/interfaces'; import { ISearchResultItem, ISearchResults } from './interfaces/interfaces';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'gf-assistant', selector: 'gf-assistant',
templateUrl: './assistant.html', styleUrls: ['./assistant.scss'],
styleUrls: ['./assistant.scss'] templateUrl: './assistant.html'
}) })
export class AssistantComponent implements OnDestroy, OnInit { export class AssistantComponent implements OnDestroy, OnInit {
@HostListener('document:keydown', ['$event']) onKeydown( @HostListener('document:keydown', ['$event']) onKeydown(
@ -71,6 +72,7 @@ export class AssistantComponent implements OnDestroy, OnInit {
} }
@Input() deviceType: string; @Input() deviceType: string;
@Input() hasPermissionToAccessAdminControl: boolean;
@Output() closed = new EventEmitter<void>(); @Output() closed = new EventEmitter<void>();
@ -87,6 +89,7 @@ export class AssistantComponent implements OnDestroy, OnInit {
public placeholder = $localize`Find holding...`; public placeholder = $localize`Find holding...`;
public searchFormControl = new FormControl(''); public searchFormControl = new FormControl('');
public searchResults: ISearchResults = { public searchResults: ISearchResults = {
assetProfiles: [],
holdings: [] holdings: []
}; };
@ -94,6 +97,7 @@ export class AssistantComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService private dataService: DataService
) {} ) {}
@ -104,6 +108,7 @@ export class AssistantComponent implements OnDestroy, OnInit {
map((searchTerm) => { map((searchTerm) => {
this.isLoading = true; this.isLoading = true;
this.searchResults = { this.searchResults = {
assetProfiles: [],
holdings: [] holdings: []
}; };
@ -115,6 +120,7 @@ export class AssistantComponent implements OnDestroy, OnInit {
distinctUntilChanged(), distinctUntilChanged(),
mergeMap(async (searchTerm) => { mergeMap(async (searchTerm) => {
const result = <ISearchResults>{ const result = <ISearchResults>{
assetProfiles: [],
holdings: [] holdings: []
}; };
@ -140,6 +146,7 @@ export class AssistantComponent implements OnDestroy, OnInit {
this.isLoading = true; this.isLoading = true;
this.keyManager = new FocusKeyManager(this.assistantListItems).withWrap(); this.keyManager = new FocusKeyManager(this.assistantListItems).withWrap();
this.searchResults = { this.searchResults = {
assetProfiles: [],
holdings: [] holdings: []
}; };
@ -180,10 +187,23 @@ export class AssistantComponent implements OnDestroy, OnInit {
} }
private async getSearchResults(aSearchTerm: string) { private async getSearchResults(aSearchTerm: string) {
let holdings: Position[] = []; let assetProfiles: ISearchResultItem[] = [];
let holdings: ISearchResultItem[] = [];
if (this.hasPermissionToAccessAdminControl) {
try {
assetProfiles = await lastValueFrom(
this.searchAssetProfiles(aSearchTerm)
);
assetProfiles = assetProfiles.slice(
0,
AssistantComponent.SEARCH_RESULTS_DEFAULT_LIMIT
);
} catch {}
}
try { try {
holdings = await lastValueFrom(this.searchHolding(aSearchTerm)); holdings = await lastValueFrom(this.searchHoldings(aSearchTerm));
holdings = holdings.slice( holdings = holdings.slice(
0, 0,
AssistantComponent.SEARCH_RESULTS_DEFAULT_LIMIT AssistantComponent.SEARCH_RESULTS_DEFAULT_LIMIT
@ -191,11 +211,46 @@ export class AssistantComponent implements OnDestroy, OnInit {
} catch {} } catch {}
return { return {
assetProfiles,
holdings holdings
}; };
} }
private searchHolding(aSearchTerm: string) { private searchAssetProfiles(
aSearchTerm: string
): Observable<ISearchResultItem[]> {
return this.adminService
.fetchAdminMarketData({
filters: [
{
id: aSearchTerm,
type: 'SEARCH_QUERY'
}
],
take: AssistantComponent.SEARCH_RESULTS_DEFAULT_LIMIT
})
.pipe(
catchError(() => {
return EMPTY;
}),
map(({ marketData }) => {
return marketData.map(
({ assetSubClass, currency, dataSource, name, symbol }) => {
return {
currency,
dataSource,
name,
symbol,
assetSubClassString: translate(assetSubClass)
};
}
);
}),
takeUntil(this.unsubscribeSubject)
);
}
private searchHoldings(aSearchTerm: string): Observable<ISearchResultItem[]> {
return this.dataService return this.dataService
.fetchPositions({ .fetchPositions({
filters: [ filters: [
@ -211,7 +266,17 @@ export class AssistantComponent implements OnDestroy, OnInit {
return EMPTY; return EMPTY;
}), }),
map(({ positions }) => { map(({ positions }) => {
return positions; return positions.map(
({ assetSubClass, currency, dataSource, name, symbol }) => {
return {
currency,
dataSource,
name,
symbol,
assetSubClassString: translate(assetSubClass)
};
}
);
}), }),
takeUntil(this.unsubscribeSubject) takeUntil(this.unsubscribeSubject)
); );

26
libs/ui/src/lib/assistant/assistant.html

@ -45,8 +45,9 @@
<div> <div>
<div class="h6 mb-1 px-2" i18n>Holdings</div> <div class="h6 mb-1 px-2" i18n>Holdings</div>
<gf-assistant-list-item <gf-assistant-list-item
*ngFor="let holding of searchResults?.holdings" *ngFor="let searchResultItem of searchResults?.holdings"
[holding]="holding" mode="holding"
[item]="searchResultItem"
(clicked)="onCloseAssistant()" (clicked)="onCloseAssistant()"
/> />
<ng-container *ngIf="searchResults?.holdings?.length === 0"> <ng-container *ngIf="searchResults?.holdings?.length === 0">
@ -62,5 +63,26 @@
<div *ngIf="!isLoading" class="px-2 py-1" i18n>No entries...</div> <div *ngIf="!isLoading" class="px-2 py-1" i18n>No entries...</div>
</ng-container> </ng-container>
</div> </div>
<div *ngIf="hasPermissionToAccessAdminControl" class="mt-3">
<div class="h6 mb-1 px-2" i18n>Asset Profiles</div>
<gf-assistant-list-item
*ngFor="let searchResultItem of searchResults?.assetProfiles"
mode="assetProfile"
[item]="searchResultItem"
(clicked)="onCloseAssistant()"
/>
<ng-container *ngIf="searchResults?.assetProfiles?.length === 0">
<ngx-skeleton-loader
*ngIf="isLoading"
animation="pulse"
class="mx-2"
[theme]="{
height: '1.5rem',
width: '100%'
}"
></ngx-skeleton-loader>
<div *ngIf="!isLoading" class="px-2 py-1" i18n>No entries...</div>
</ng-container>
</div>
</div> </div>
</div> </div>

11
libs/ui/src/lib/assistant/interfaces/interfaces.ts

@ -1,5 +1,12 @@
import { Position } from '@ghostfolio/common/interfaces'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
export interface ISearchResultItem extends UniqueAsset {
assetSubClassString: string;
currency: string;
name: string;
}
export interface ISearchResults { export interface ISearchResults {
holdings: Position[]; assetProfiles: ISearchResultItem[];
holdings: ISearchResultItem[];
} }

8
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.10.0", "version": "2.12.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",
@ -82,8 +82,8 @@
"@nestjs/schedule": "3.0.2", "@nestjs/schedule": "3.0.2",
"@nestjs/serve-static": "4.0.0", "@nestjs/serve-static": "4.0.0",
"@prisma/client": "5.4.2", "@prisma/client": "5.4.2",
"@simplewebauthn/browser": "5.2.1", "@simplewebauthn/browser": "8.3.1",
"@simplewebauthn/server": "5.2.1", "@simplewebauthn/server": "8.3.2",
"@stripe/stripe-js": "1.47.0", "@stripe/stripe-js": "1.47.0",
"alphavantage": "2.2.0", "alphavantage": "2.2.0",
"big.js": "6.2.1", "big.js": "6.2.1",
@ -157,7 +157,7 @@
"@nx/web": "16.7.4", "@nx/web": "16.7.4",
"@nx/workspace": "16.7.4", "@nx/workspace": "16.7.4",
"@schematics/angular": "16.2.0", "@schematics/angular": "16.2.0",
"@simplewebauthn/typescript-types": "5.2.1", "@simplewebauthn/typescript-types": "8.0.0",
"@storybook/addon-essentials": "7.3.2", "@storybook/addon-essentials": "7.3.2",
"@storybook/angular": "7.3.2", "@storybook/angular": "7.3.2",
"@storybook/core-server": "7.3.2", "@storybook/core-server": "7.3.2",

243
yarn.lock

@ -1819,6 +1819,36 @@
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f" resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f"
integrity sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg== integrity sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==
"@cbor-extract/cbor-extract-darwin-arm64@2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.1.1.tgz#5721f6dd3feae0b96d23122853ce977e0671b7a6"
integrity sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA==
"@cbor-extract/cbor-extract-darwin-x64@2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.1.1.tgz#c25e7d0133950d87d101d7b3afafea8d50d83f5f"
integrity sha512-h6KFOzqk8jXTvkOftyRIWGrd7sKQzQv2jVdTL9nKSf3D2drCvQB/LHUxAOpPXo3pv2clDtKs3xnHalpEh3rDsw==
"@cbor-extract/cbor-extract-linux-arm64@2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.1.1.tgz#48f78e7d8f0fcc84ed074b6bfa6d15dd83187c63"
integrity sha512-SxAaRcYf8S0QHaMc7gvRSiTSr7nUYMqbUdErBEu+HYA4Q6UNydx1VwFE68hGcp1qvxcy9yT5U7gA+a5XikfwSQ==
"@cbor-extract/cbor-extract-linux-arm@2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.1.1.tgz#7507d346389cb682e44fab8fae9534edd52e2e41"
integrity sha512-ds0uikdcIGUjPyraV4oJqyVE5gl/qYBpa/Wnh6l6xLE2lj/hwnjT2XcZCChdXwW/YFZ1LUHs6waoYN8PmK0nKQ==
"@cbor-extract/cbor-extract-linux-x64@2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.1.1.tgz#b7c1d2be61c58ec18d58afbad52411ded63cd4cd"
integrity sha512-GVK+8fNIE9lJQHAlhOROYiI0Yd4bAZ4u++C2ZjlkS3YmO6hi+FUxe6Dqm+OKWTcMpL/l71N6CQAmaRcb4zyJuA==
"@cbor-extract/cbor-extract-win32-x64@2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.1.1.tgz#21b11a1a3f18c3e7d62fd5f87438b7ed2c64c1f7"
integrity sha512-2Niq1C41dCRIDeD8LddiH+mxGlO7HJ612Ll3D/E73ZWBmycued+8ghTr/Ho3CMOWPUEr08XtyBMVXAjqF+TcKw==
"@codewithdan/observable-store@2.2.15": "@codewithdan/observable-store@2.2.15":
version "2.2.15" version "2.2.15"
resolved "https://registry.yarnpkg.com/@codewithdan/observable-store/-/observable-store-2.2.15.tgz#6d27e0988e182853def59a714b712f4389e558d2" resolved "https://registry.yarnpkg.com/@codewithdan/observable-store/-/observable-store-2.2.15.tgz#6d27e0988e182853def59a714b712f4389e558d2"
@ -2540,6 +2570,11 @@
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.1.tgz#1a5b1959a528e374e8037c4396c3e825d6cf4a83" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.1.tgz#1a5b1959a528e374e8037c4396c3e825d6cf4a83"
integrity sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw== integrity sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==
"@hexagon/base64@^1.1.27":
version "1.1.28"
resolved "https://registry.yarnpkg.com/@hexagon/base64/-/base64-1.1.28.tgz#7d306a97f1423829be5b27c9d388fe50e3099d48"
integrity sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==
"@humanwhocodes/config-array@^0.11.8": "@humanwhocodes/config-array@^0.11.8":
version "0.11.10" version "0.11.10"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2"
@ -4252,7 +4287,7 @@
node-addon-api "^3.2.1" node-addon-api "^3.2.1"
node-gyp-build "^4.3.0" node-gyp-build "^4.3.0"
"@peculiar/asn1-android@^2.1.7": "@peculiar/asn1-android@^2.3.6":
version "2.3.6" version "2.3.6"
resolved "https://registry.yarnpkg.com/@peculiar/asn1-android/-/asn1-android-2.3.6.tgz#20363c23bc5b9a91f7ffd80d7c3842dccff8c20b" resolved "https://registry.yarnpkg.com/@peculiar/asn1-android/-/asn1-android-2.3.6.tgz#20363c23bc5b9a91f7ffd80d7c3842dccff8c20b"
integrity sha512-zkYh4DsiRhiNfg6tWaUuRc+huwlb9XJbmeZLrjTz9v76UK1Ehq3EnfJFED6P3sdznW/nqWe46LoM9JrqxcD58g== integrity sha512-zkYh4DsiRhiNfg6tWaUuRc+huwlb9XJbmeZLrjTz9v76UK1Ehq3EnfJFED6P3sdznW/nqWe46LoM9JrqxcD58g==
@ -4261,7 +4296,27 @@
asn1js "^3.0.5" asn1js "^3.0.5"
tslib "^2.4.0" tslib "^2.4.0"
"@peculiar/asn1-schema@^2.1.7", "@peculiar/asn1-schema@^2.3.6": "@peculiar/asn1-ecc@^2.3.6":
version "2.3.6"
resolved "https://registry.yarnpkg.com/@peculiar/asn1-ecc/-/asn1-ecc-2.3.6.tgz#f155f33f5c61df463d9b33b911d25578a19694b7"
integrity sha512-Hu1xzMJQWv8/GvzOiinaE6XiD1/kEhq2C/V89UEoWeZ2fLUcGNIvMxOr/pMyL0OmpRWj/mhCTXOZp4PP+a0aTg==
dependencies:
"@peculiar/asn1-schema" "^2.3.6"
"@peculiar/asn1-x509" "^2.3.6"
asn1js "^3.0.5"
tslib "^2.4.0"
"@peculiar/asn1-rsa@^2.3.6":
version "2.3.6"
resolved "https://registry.yarnpkg.com/@peculiar/asn1-rsa/-/asn1-rsa-2.3.6.tgz#e2af2c52a914c60f33853a86a48905ec555b29c2"
integrity sha512-DswjJyAXZnvESuImGNTvbNKvh1XApBVqU+r3UmrFFTAI23gv62byl0f5OFKWTNhCf66WQrd3sklpsCZc/4+jwA==
dependencies:
"@peculiar/asn1-schema" "^2.3.6"
"@peculiar/asn1-x509" "^2.3.6"
asn1js "^3.0.5"
tslib "^2.4.0"
"@peculiar/asn1-schema@^2.3.6":
version "2.3.6" version "2.3.6"
resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.3.6.tgz#3dd3c2ade7f702a9a94dfb395c192f5fa5d6b922" resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.3.6.tgz#3dd3c2ade7f702a9a94dfb395c192f5fa5d6b922"
integrity sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA== integrity sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==
@ -4270,7 +4325,7 @@
pvtsutils "^1.3.2" pvtsutils "^1.3.2"
tslib "^2.4.0" tslib "^2.4.0"
"@peculiar/asn1-x509@^2.1.7": "@peculiar/asn1-x509@^2.3.6":
version "2.3.6" version "2.3.6"
resolved "https://registry.yarnpkg.com/@peculiar/asn1-x509/-/asn1-x509-2.3.6.tgz#e50154a460cdf43da8a41b23ee807a53e0036af0" resolved "https://registry.yarnpkg.com/@peculiar/asn1-x509/-/asn1-x509-2.3.6.tgz#e50154a460cdf43da8a41b23ee807a53e0036af0"
integrity sha512-dRwX31R1lcbIdzbztiMvLNTDoGptxdV7HocNx87LfKU0fEWh7fTWJjx4oV+glETSy6heF/hJHB2J4RGB3vVSYg== integrity sha512-dRwX31R1lcbIdzbztiMvLNTDoGptxdV7HocNx87LfKU0fEWh7fTWJjx4oV+glETSy6heF/hJHB2J4RGB3vVSYg==
@ -4638,38 +4693,32 @@
"@sigstore/protobuf-specs" "^0.1.0" "@sigstore/protobuf-specs" "^0.1.0"
tuf-js "^1.1.7" tuf-js "^1.1.7"
"@simplewebauthn/browser@5.2.1": "@simplewebauthn/browser@8.3.1":
version "5.2.1" version "8.3.1"
resolved "https://registry.yarnpkg.com/@simplewebauthn/browser/-/browser-5.2.1.tgz#569252a9f235a99aae90c4d1cc6c441f42637b8e" resolved "https://registry.yarnpkg.com/@simplewebauthn/browser/-/browser-8.3.1.tgz#f5c1aed6313d61944a9e13f16ae4495750bddf93"
integrity sha512-TxL3OPHJf57hmnfQoF3zRIQWEdsJLxrA9NcGdRK0sB/h3jd13kpGQonBtMnj4YBQnWTtRDZ804wlpI9IEMaJ9g== integrity sha512-bMW7oOkxX4ydRAkkPtJ1do2k9yOoIGc/hZYebcuEOVdJoC6wwVpu97mYY7Mz8B9hLlcaR5WFgBsLl5tSJVzm8A==
dependencies:
"@simplewebauthn/server@5.2.1": "@simplewebauthn/typescript-types" "^8.0.0"
version "5.2.1"
resolved "https://registry.yarnpkg.com/@simplewebauthn/server/-/server-5.2.1.tgz#49038d2951ad2ac065bdf8342fdb13f78ee4df1c"
integrity sha512-+CQ8oJf9Io8y4ReYLagX5JG9ShntIkdeCPkMoyHLBSRPlNY0N/Yv3Iun4YPQ8d4LJUU9f8S1eD5bibIEMjWDRg==
dependencies:
"@peculiar/asn1-android" "^2.1.7"
"@peculiar/asn1-schema" "^2.1.7"
"@peculiar/asn1-x509" "^2.1.7"
"@simplewebauthn/typescript-types" "^5.2.1"
base64url "^3.0.1"
cbor "^5.1.0"
debug "^4.3.2"
elliptic "^6.5.3"
jsrsasign "^10.4.0"
jwk-to-pem "^2.0.4"
node-fetch "^2.6.0"
node-rsa "^1.1.1"
"@simplewebauthn/typescript-types@5.2.1": "@simplewebauthn/server@8.3.2":
version "5.2.1" version "8.3.2"
resolved "https://registry.yarnpkg.com/@simplewebauthn/typescript-types/-/typescript-types-5.2.1.tgz#a8229ce4f71be7edafe3bfdce062b332ef494f0d" resolved "https://registry.yarnpkg.com/@simplewebauthn/server/-/server-8.3.2.tgz#dfdbe7af4c1258e786c4a0b1c83c54743ba7568c"
integrity sha512-t/NzbjaD0zu4ivUmiof2cPA8X5LHhFX+DflBBl71/dzEhl15qepDI2rxWdjB+Hc0FfOT1fBQnb1uP19fPcDUiA== integrity sha512-ceo8t5gdO5W/JOePQWPDH+rAd8tO6QNalLU56rc9ItdzaTjk+qcYwQg/BKXDDg6117P3HKrRBkZwBrMJl4dOdA==
dependencies:
"@hexagon/base64" "^1.1.27"
"@peculiar/asn1-android" "^2.3.6"
"@peculiar/asn1-ecc" "^2.3.6"
"@peculiar/asn1-rsa" "^2.3.6"
"@peculiar/asn1-schema" "^2.3.6"
"@peculiar/asn1-x509" "^2.3.6"
"@simplewebauthn/typescript-types" "^8.0.0"
cbor-x "^1.5.2"
cross-fetch "^4.0.0"
"@simplewebauthn/typescript-types@^5.2.1": "@simplewebauthn/typescript-types@8.0.0", "@simplewebauthn/typescript-types@^8.0.0":
version "5.4.0" version "8.0.0"
resolved "https://registry.yarnpkg.com/@simplewebauthn/typescript-types/-/typescript-types-5.4.0.tgz#533b28e7cabcc092396ecd07bbb953b71e7696b6" resolved "https://registry.yarnpkg.com/@simplewebauthn/typescript-types/-/typescript-types-8.0.0.tgz#1698a7228aba880c5c1deba1f13a4f9fd8851cb3"
integrity sha512-LeJq6Jx+o7D6iIlCy8CH5jCjwVcUvAReEo66VcF3nysfc/yKW5yCAPLSRmPITF4CRZTfnVPxUBUcveUQL6aBMA== integrity sha512-d7Izb2H+LZJteXMkS8DmpAarD6mZdpIOu/av/yH4/u/3Pd6DKFLyBM3j8BMmUvUqpzvJvHARNrRfQYto58mtTQ==
"@sinclair/typebox@^0.27.8": "@sinclair/typebox@^0.27.8":
version "0.27.8" version "0.27.8"
@ -7122,17 +7171,7 @@ arrify@^2.0.0:
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
asn1.js@^5.3.0: asn1@~0.2.3:
version "5.4.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
dependencies:
bn.js "^4.0.0"
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
safer-buffer "^2.1.0"
asn1@^0.2.4, asn1@~0.2.3:
version "0.2.6" version "0.2.6"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==
@ -7461,7 +7500,7 @@ base64-js@^1.2.0, base64-js@^1.3.0, base64-js@^1.3.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
base64url@3.x.x, base64url@^3.0.1: base64url@3.x.x:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
@ -7527,7 +7566,7 @@ big.js@^5.2.2:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
bignumber.js@^9.0.0, bignumber.js@^9.0.1: bignumber.js@^9.0.0:
version "9.1.1" version "9.1.1"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.1.tgz#c4df7dc496bd849d4c9464344c1aa74228b4dac6" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.1.tgz#c4df7dc496bd849d4c9464344c1aa74228b4dac6"
integrity sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig== integrity sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==
@ -7556,11 +7595,6 @@ bluebird@^3.7.2:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
bn.js@^4.0.0, bn.js@^4.11.9:
version "4.12.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
body-parser@1.20.1: body-parser@1.20.1:
version "1.20.1" version "1.20.1"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
@ -7689,11 +7723,6 @@ braces@^3.0.2, braces@~3.0.2:
dependencies: dependencies:
fill-range "^7.0.1" fill-range "^7.0.1"
brorand@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==
browser-assert@^1.2.1: browser-assert@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/browser-assert/-/browser-assert-1.2.1.tgz#9aaa5a2a8c74685c2ae05bfe46efd606f068c200" resolved "https://registry.yarnpkg.com/browser-assert/-/browser-assert-1.2.1.tgz#9aaa5a2a8c74685c2ae05bfe46efd606f068c200"
@ -7953,13 +7982,26 @@ caseless@~0.12.0:
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==
cbor@^5.1.0: cbor-extract@^2.1.1:
version "5.2.0" version "2.1.1"
resolved "https://registry.yarnpkg.com/cbor/-/cbor-5.2.0.tgz#4cca67783ccd6de7b50ab4ed62636712f287a67c" resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.1.1.tgz#f154b31529fdb6b7c70fb3ca448f44eda96a1b42"
integrity sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A== integrity sha512-1UX977+L+zOJHsp0mWFG13GLwO6ucKgSmSW6JTl8B9GUvACvHeIVpFqhU92299Z6PfD09aTXDell5p+lp1rUFA==
dependencies: dependencies:
bignumber.js "^9.0.1" node-gyp-build-optional-packages "5.0.3"
nofilter "^1.0.4" optionalDependencies:
"@cbor-extract/cbor-extract-darwin-arm64" "2.1.1"
"@cbor-extract/cbor-extract-darwin-x64" "2.1.1"
"@cbor-extract/cbor-extract-linux-arm" "2.1.1"
"@cbor-extract/cbor-extract-linux-arm64" "2.1.1"
"@cbor-extract/cbor-extract-linux-x64" "2.1.1"
"@cbor-extract/cbor-extract-win32-x64" "2.1.1"
cbor-x@^1.5.2:
version "1.5.4"
resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.5.4.tgz#8f0754fa8589cbd7339b613b2b5717d133508e98"
integrity sha512-PVKILDn+Rf6MRhhcyzGXi5eizn1i0i3F8Fe6UMMxXBnWkalq9+C5+VTmlIjAYM4iF2IYF2N+zToqAfYOp+3rfw==
optionalDependencies:
cbor-extract "^2.1.1"
chalk@^1.0.0, chalk@^1.1.3: chalk@^1.0.0, chalk@^1.1.3:
version "1.1.3" version "1.1.3"
@ -8691,6 +8733,13 @@ cross-fetch@^3.0.5:
dependencies: dependencies:
node-fetch "^2.6.12" node-fetch "^2.6.12"
cross-fetch@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983"
integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==
dependencies:
node-fetch "^2.6.12"
cross-spawn@^6.0.0, cross-spawn@^6.0.5: cross-spawn@^6.0.0, cross-spawn@^6.0.5:
version "6.0.5" version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@ -9741,19 +9790,6 @@ elkjs@^0.8.2:
resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e" resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e"
integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ== integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==
elliptic@^6.5.3, elliptic@^6.5.4:
version "6.5.4"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
dependencies:
bn.js "^4.11.9"
brorand "^1.1.0"
hash.js "^1.0.0"
hmac-drbg "^1.0.1"
inherits "^2.0.4"
minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1"
emittery@^0.13.1: emittery@^0.13.1:
version "0.13.1" version "0.13.1"
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad"
@ -11673,14 +11709,6 @@ has@^1.0.3:
dependencies: dependencies:
function-bind "^1.1.1" function-bind "^1.1.1"
hash.js@^1.0.0, hash.js@^1.0.3:
version "1.1.7"
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
dependencies:
inherits "^2.0.3"
minimalistic-assert "^1.0.1"
hdr-histogram-js@^2.0.1: hdr-histogram-js@^2.0.1:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz#0b860534655722b6e3f3e7dca7b78867cf43dcb5" resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz#0b860534655722b6e3f3e7dca7b78867cf43dcb5"
@ -11710,15 +11738,6 @@ helmet@7.0.0:
resolved "https://registry.yarnpkg.com/helmet/-/helmet-7.0.0.tgz#ac3011ba82fa2467f58075afa58a49427ba6212d" resolved "https://registry.yarnpkg.com/helmet/-/helmet-7.0.0.tgz#ac3011ba82fa2467f58075afa58a49427ba6212d"
integrity sha512-MsIgYmdBh460ZZ8cJC81q4XJknjG567wzEmv46WOBblDb6TUd3z8/GhgmsM9pn8g2B80tAJ4m5/d3Bi1KrSUBQ== integrity sha512-MsIgYmdBh460ZZ8cJC81q4XJknjG567wzEmv46WOBblDb6TUd3z8/GhgmsM9pn8g2B80tAJ4m5/d3Bi1KrSUBQ==
hmac-drbg@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==
dependencies:
hash.js "^1.0.3"
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hosted-git-info@^2.1.4: hosted-git-info@^2.1.4:
version "2.8.9" version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
@ -13490,11 +13509,6 @@ jsprim@^2.0.2:
json-schema "0.4.0" json-schema "0.4.0"
verror "1.10.0" verror "1.10.0"
jsrsasign@^10.4.0:
version "10.8.6"
resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-10.8.6.tgz#ebf7f3c812c6517af84f0d8a10115e0dbfabe145"
integrity sha512-bQmbVtsfbgaKBTWCKiDCPlUPbdlRIK/FzSwT3BzIgZl/cU6TqXu6pZJsCI/dJVrZ9Gir5GC4woqw9shH/v7MBw==
jwa@^1.4.1: jwa@^1.4.1:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
@ -13513,15 +13527,6 @@ jwa@^2.0.0:
ecdsa-sig-formatter "1.0.11" ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
jwk-to-pem@^2.0.4:
version "2.0.5"
resolved "https://registry.yarnpkg.com/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz#151310bcfbcf731adc5ad9f379cbc8b395742906"
integrity sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==
dependencies:
asn1.js "^5.3.0"
elliptic "^6.5.4"
safe-buffer "^5.0.1"
jws@^3.2.2: jws@^3.2.2:
version "3.2.2" version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
@ -14226,16 +14231,11 @@ mini-css-extract-plugin@~2.4.7:
dependencies: dependencies:
schema-utils "^4.0.0" schema-utils "^4.0.0"
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: minimalistic-assert@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
minimalistic-crypto-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==
minimatch@3.0.5: minimatch@3.0.5:
version "3.0.5" version "3.0.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.5.tgz#4da8f1290ee0f0f8e83d60ca69f8f134068604a3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.5.tgz#4da8f1290ee0f0f8e83d60ca69f8f134068604a3"
@ -14600,7 +14600,7 @@ node-fetch-native@^1.0.2:
resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.2.0.tgz#13ec6df98f33168958dbfb6945f10aedf42e7ea8" resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.2.0.tgz#13ec6df98f33168958dbfb6945f10aedf42e7ea8"
integrity sha512-5IAMBTl9p6PaAjYCnMv5FmqIF6GcZnawAVnzaCG0rX2aYZJ4CxEkZNtVPuTRug7fL7wyM5BQYTlAzcyMPi6oTQ== integrity sha512-5IAMBTl9p6PaAjYCnMv5FmqIF6GcZnawAVnzaCG0rX2aYZJ4CxEkZNtVPuTRug7fL7wyM5BQYTlAzcyMPi6oTQ==
node-fetch@^2.0.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7: node-fetch@^2.0.0, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7:
version "2.6.12" version "2.6.12"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba"
integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g== integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==
@ -14612,6 +14612,11 @@ node-forge@^1, node-forge@^1.3.1:
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==
node-gyp-build-optional-packages@5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17"
integrity sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==
node-gyp-build-optional-packages@5.0.7: node-gyp-build-optional-packages@5.0.7:
version "5.0.7" version "5.0.7"
resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz#5d2632bbde0ab2f6e22f1bbac2199b07244ae0b3" resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz#5d2632bbde0ab2f6e22f1bbac2199b07244ae0b3"
@ -14654,18 +14659,6 @@ node-releases@^2.0.12:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
node-rsa@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/node-rsa/-/node-rsa-1.1.1.tgz#efd9ad382097782f506153398496f79e4464434d"
integrity sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==
dependencies:
asn1 "^0.2.4"
nofilter@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-1.0.4.tgz#78d6f4b6a613e7ced8b015cec534625f7667006e"
integrity sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==
non-layered-tidy-tree-layout@^2.0.2: non-layered-tidy-tree-layout@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz#57d35d13c356643fc296a55fb11ac15e74da7804" resolved "https://registry.yarnpkg.com/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz#57d35d13c356643fc296a55fb11ac15e74da7804"

Loading…
Cancel
Save