Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
ba926ffcf2 | |||
5ea455b98b | |||
39f315aba0 | |||
df2dfc20a1 | |||
81e83d4cea | |||
5d4156ecec | |||
4693a8baa2 | |||
773444b1e2 | |||
3c46bde8d5 | |||
63ee33b685 | |||
bc87c0a3e1 | |||
caa9fc3efa | |||
9ed82ac82b | |||
9c9ca4ab1e |
34
CHANGELOG.md
34
CHANGELOG.md
@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 1.52.0 - 11.09.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added the annualized performance to the portfolio summary tab on the home page
|
||||
- Added the Ghostfolio Slack channel to the about page
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `3.0.0` to `4.1.0`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the sign in with fingerprint for some android devices
|
||||
|
||||
## 1.51.0 - 11.09.2021
|
||||
|
||||
### Changed
|
||||
|
||||
- Provided the name in the portfolio position endpoint
|
||||
|
||||
## 1.50.0 - 11.09.2021
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the _Fear & Greed Index_ (market mood)
|
||||
- Fixed the overlap of the home button with tabs on iOS (_Add to Home Screen_)
|
||||
|
||||
## 1.49.0 - 08.09.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added labels to the allocation chart by symbol on desktop
|
||||
|
||||
## 1.48.0 - 07.09.2021
|
||||
|
||||
### Added
|
||||
|
@ -12,7 +12,7 @@
|
||||
<strong>Open Source Wealth Management Software made for Humans</strong>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://ghostfol.io"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/en/blog/2021/07/hello-ghostfolio"><strong>Blog</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
|
||||
<a href="https://ghostfol.io"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/en/blog/2021/07/hello-ghostfolio"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="#contributing">
|
||||
@ -128,7 +128,7 @@ Run `yarn test`
|
||||
|
||||
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
|
||||
|
||||
Not sure what to work on? We have got some ideas. Please tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_) or send an e-mail to hi@ghostfol.io. We would love to hear from you.
|
||||
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack channel](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg), tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_) or send an e-mail to hi@ghostfol.io. We would love to hear from you.
|
||||
|
||||
## License
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Access } from '@ghostfolio/common/interfaces';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
hasPermission,
|
||||
permissions
|
||||
} from '@ghostfolio/common/permissions';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
hasPermission,
|
||||
permissions
|
||||
} from '@ghostfolio/common/permissions';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
hasPermission,
|
||||
permissions
|
||||
} from '@ghostfolio/common/permissions';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Controller,
|
||||
Delete,
|
||||
|
@ -62,10 +62,10 @@ export class AuthController {
|
||||
}
|
||||
}
|
||||
|
||||
@Get('webauthn/generate-attestation-options')
|
||||
@Get('webauthn/generate-registration-options')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async generateAttestationOptions() {
|
||||
return this.webAuthService.generateAttestationOptions();
|
||||
public async generateRegistrationOptions() {
|
||||
return this.webAuthService.generateRegistrationOptions();
|
||||
}
|
||||
|
||||
@Post('webauthn/verify-attestation')
|
||||
|
@ -2,7 +2,7 @@ import { AuthDeviceDto } from '@ghostfolio/api/app/auth-device/auth-device.dto';
|
||||
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
|
||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
@ -11,16 +11,16 @@ import {
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import {
|
||||
GenerateAssertionOptionsOpts,
|
||||
GenerateAttestationOptionsOpts,
|
||||
VerifiedAssertion,
|
||||
VerifiedAttestation,
|
||||
VerifyAssertionResponseOpts,
|
||||
VerifyAttestationResponseOpts,
|
||||
generateAssertionOptions,
|
||||
generateAttestationOptions,
|
||||
verifyAssertionResponse,
|
||||
verifyAttestationResponse
|
||||
GenerateAuthenticationOptionsOpts,
|
||||
GenerateRegistrationOptionsOpts,
|
||||
VerifiedAuthenticationResponse,
|
||||
VerifiedRegistrationResponse,
|
||||
VerifyAuthenticationResponseOpts,
|
||||
VerifyRegistrationResponseOpts,
|
||||
generateAuthenticationOptions,
|
||||
generateRegistrationOptions,
|
||||
verifyAuthenticationResponse,
|
||||
verifyRegistrationResponse
|
||||
} from '@simplewebauthn/server';
|
||||
|
||||
import {
|
||||
@ -46,10 +46,10 @@ export class WebAuthService {
|
||||
return this.configurationService.get('ROOT_URL');
|
||||
}
|
||||
|
||||
public async generateAttestationOptions() {
|
||||
public async generateRegistrationOptions() {
|
||||
const user = this.request.user;
|
||||
|
||||
const opts: GenerateAttestationOptionsOpts = {
|
||||
const opts: GenerateRegistrationOptionsOpts = {
|
||||
rpName: 'Ghostfolio',
|
||||
rpID: this.rpID,
|
||||
userID: user.id,
|
||||
@ -63,7 +63,7 @@ export class WebAuthService {
|
||||
}
|
||||
};
|
||||
|
||||
const options = generateAttestationOptions(opts);
|
||||
const options = generateRegistrationOptions(opts);
|
||||
|
||||
await this.userService.updateUser({
|
||||
data: {
|
||||
@ -84,27 +84,27 @@ export class WebAuthService {
|
||||
const user = this.request.user;
|
||||
const expectedChallenge = user.authChallenge;
|
||||
|
||||
let verification: VerifiedAttestation;
|
||||
let verification: VerifiedRegistrationResponse;
|
||||
try {
|
||||
const opts: VerifyAttestationResponseOpts = {
|
||||
const opts: VerifyRegistrationResponseOpts = {
|
||||
credential,
|
||||
expectedChallenge,
|
||||
expectedOrigin: this.expectedOrigin,
|
||||
expectedRPID: this.rpID
|
||||
};
|
||||
verification = await verifyAttestationResponse(opts);
|
||||
verification = await verifyRegistrationResponse(opts);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new InternalServerErrorException(error.message);
|
||||
}
|
||||
|
||||
const { verified, attestationInfo } = verification;
|
||||
const { registrationInfo, verified } = verification;
|
||||
|
||||
const devices = await this.deviceService.authDevices({
|
||||
where: { userId: user.id }
|
||||
});
|
||||
if (verified && attestationInfo) {
|
||||
const { credentialPublicKey, credentialID, counter } = attestationInfo;
|
||||
if (registrationInfo && verified) {
|
||||
const { counter, credentialID, credentialPublicKey } = registrationInfo;
|
||||
|
||||
let existingDevice = devices.find(
|
||||
(device) => device.credentialId === credentialID
|
||||
@ -115,9 +115,9 @@ export class WebAuthService {
|
||||
* Add the returned device to the user's list of devices
|
||||
*/
|
||||
existingDevice = await this.deviceService.createAuthDevice({
|
||||
counter,
|
||||
credentialPublicKey,
|
||||
credentialId: credentialID,
|
||||
counter,
|
||||
User: { connect: { id: user.id } }
|
||||
});
|
||||
}
|
||||
@ -138,20 +138,20 @@ export class WebAuthService {
|
||||
throw new Error('Device not found');
|
||||
}
|
||||
|
||||
const opts: GenerateAssertionOptionsOpts = {
|
||||
timeout: 60000,
|
||||
const opts: GenerateAuthenticationOptionsOpts = {
|
||||
allowCredentials: [
|
||||
{
|
||||
id: device.credentialId,
|
||||
type: 'public-key',
|
||||
transports: ['internal']
|
||||
transports: ['internal'],
|
||||
type: 'public-key'
|
||||
}
|
||||
],
|
||||
userVerification: 'preferred',
|
||||
rpID: this.rpID
|
||||
rpID: this.rpID,
|
||||
timeout: 60000,
|
||||
userVerification: 'preferred'
|
||||
};
|
||||
|
||||
const options = generateAssertionOptions(opts);
|
||||
const options = generateAuthenticationOptions(opts);
|
||||
|
||||
await this.userService.updateUser({
|
||||
data: {
|
||||
@ -177,29 +177,29 @@ export class WebAuthService {
|
||||
|
||||
const user = await this.userService.user({ id: device.userId });
|
||||
|
||||
let verification: VerifiedAssertion;
|
||||
let verification: VerifiedAuthenticationResponse;
|
||||
try {
|
||||
const opts: VerifyAssertionResponseOpts = {
|
||||
const opts: VerifyAuthenticationResponseOpts = {
|
||||
credential,
|
||||
expectedChallenge: `${user.authChallenge}`,
|
||||
expectedOrigin: this.expectedOrigin,
|
||||
expectedRPID: this.rpID,
|
||||
authenticator: {
|
||||
credentialID: device.credentialId,
|
||||
credentialPublicKey: device.credentialPublicKey,
|
||||
counter: device.counter
|
||||
}
|
||||
},
|
||||
expectedChallenge: `${user.authChallenge}`,
|
||||
expectedOrigin: this.expectedOrigin,
|
||||
expectedRPID: this.rpID
|
||||
};
|
||||
verification = verifyAssertionResponse(opts);
|
||||
verification = verifyAuthenticationResponse(opts);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new InternalServerErrorException({ error: error.message });
|
||||
}
|
||||
|
||||
const { verified, assertionInfo } = verification;
|
||||
const { verified, authenticationInfo } = verification;
|
||||
|
||||
if (verified) {
|
||||
device.counter = assertionInfo.newCounter;
|
||||
device.counter = authenticationInfo.newCounter;
|
||||
|
||||
await this.deviceService.updateAuthDevice({
|
||||
data: device,
|
||||
|
2
apps/api/src/app/cache/cache.controller.ts
vendored
2
apps/api/src/app/cache/cache.controller.ts
vendored
@ -1,6 +1,6 @@
|
||||
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import { Controller, Inject, Post, UseGuards } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { baseCurrency, benchmarks } from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import { isApiTokenAuthorized } from '@ghostfolio/common/permissions';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Export } from '@ghostfolio/common/interfaces';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
hasPermission,
|
||||
permissions
|
||||
} from '@ghostfolio/common/permissions';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
|
@ -11,6 +11,7 @@ export interface PortfolioPositionDetail {
|
||||
marketPrice: number;
|
||||
maxPrice: number;
|
||||
minPrice: number;
|
||||
name: string;
|
||||
netPerformance: number;
|
||||
netPerformancePercent: number;
|
||||
quantity: number;
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
PortfolioSummary
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
|
62
apps/api/src/app/portfolio/portfolio.service.spec.ts
Normal file
62
apps/api/src/app/portfolio/portfolio.service.spec.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { PortfolioService } from './portfolio.service';
|
||||
|
||||
describe('PortfolioService', () => {
|
||||
let portfolioService: PortfolioService;
|
||||
|
||||
beforeAll(async () => {
|
||||
portfolioService = new PortfolioService(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it('Get annualized performance', async () => {
|
||||
expect(
|
||||
portfolioService.getAnnualizedPerformancePercent({
|
||||
daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day
|
||||
netPerformancePercent: 0
|
||||
})
|
||||
).toEqual(0);
|
||||
|
||||
expect(
|
||||
portfolioService.getAnnualizedPerformancePercent({
|
||||
daysInMarket: 0,
|
||||
netPerformancePercent: 0
|
||||
})
|
||||
).toEqual(0);
|
||||
|
||||
/**
|
||||
* Source: https://www.readyratios.com/reference/analysis/annualized_rate.html
|
||||
*/
|
||||
expect(
|
||||
portfolioService.getAnnualizedPerformancePercent({
|
||||
daysInMarket: 65, // < 1 year
|
||||
netPerformancePercent: 0.1025
|
||||
})
|
||||
).toBeCloseTo(0.729705);
|
||||
|
||||
expect(
|
||||
portfolioService.getAnnualizedPerformancePercent({
|
||||
daysInMarket: 365, // 1 year
|
||||
netPerformancePercent: 0.05
|
||||
})
|
||||
).toBeCloseTo(0.05);
|
||||
|
||||
/**
|
||||
* Source: https://www.investopedia.com/terms/a/annualized-total-return.asp#annualized-return-formula-and-calculation
|
||||
*/
|
||||
expect(
|
||||
portfolioService.getAnnualizedPerformancePercent({
|
||||
daysInMarket: 575, // > 1 year
|
||||
netPerformancePercent: 0.2374
|
||||
})
|
||||
).toBeCloseTo(0.145);
|
||||
});
|
||||
});
|
@ -32,7 +32,7 @@ import {
|
||||
TimelinePosition
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import {
|
||||
import type {
|
||||
DateRange,
|
||||
OrderWithAccount,
|
||||
RequestWithUser
|
||||
@ -47,6 +47,7 @@ import {
|
||||
} from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
import {
|
||||
differenceInDays,
|
||||
endOfToday,
|
||||
format,
|
||||
isAfter,
|
||||
@ -58,7 +59,7 @@ import {
|
||||
subDays,
|
||||
subYears
|
||||
} from 'date-fns';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { isEmpty, isNumber } from 'lodash';
|
||||
|
||||
import {
|
||||
HistoricalDataItem,
|
||||
@ -80,6 +81,21 @@ export class PortfolioService {
|
||||
private readonly symbolProfileService: SymbolProfileService
|
||||
) {}
|
||||
|
||||
public getAnnualizedPerformancePercent({
|
||||
daysInMarket,
|
||||
netPerformancePercent
|
||||
}: {
|
||||
daysInMarket: number;
|
||||
netPerformancePercent: number;
|
||||
}) {
|
||||
if (isNumber(daysInMarket) && daysInMarket > 0) {
|
||||
const exponent = new Big(365).div(daysInMarket).toNumber();
|
||||
return Math.pow(1 + netPerformancePercent, exponent) - 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public async getInvestments(
|
||||
aImpersonationId: string
|
||||
): Promise<InvestmentItem[]> {
|
||||
@ -282,6 +298,7 @@ export class PortfolioService {
|
||||
marketPrice: undefined,
|
||||
maxPrice: undefined,
|
||||
minPrice: undefined,
|
||||
name: undefined,
|
||||
netPerformance: undefined,
|
||||
netPerformancePercent: undefined,
|
||||
quantity: undefined,
|
||||
@ -291,6 +308,7 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
const positionCurrency = orders[0].currency;
|
||||
const name = orders[0].SymbolProfile?.name ?? '';
|
||||
|
||||
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
||||
currency: order.currency,
|
||||
@ -407,6 +425,7 @@ export class PortfolioService {
|
||||
marketPrice,
|
||||
maxPrice,
|
||||
minPrice,
|
||||
name,
|
||||
netPerformance,
|
||||
transactionCount,
|
||||
averagePrice: averagePrice.toNumber(),
|
||||
@ -455,6 +474,7 @@ export class PortfolioService {
|
||||
marketPrice,
|
||||
maxPrice,
|
||||
minPrice,
|
||||
name,
|
||||
averagePrice: 0,
|
||||
currency: currentData[aSymbol]?.currency,
|
||||
firstBuyDate: undefined,
|
||||
@ -711,6 +731,12 @@ export class PortfolioService {
|
||||
const fees = this.getFees(orders);
|
||||
const firstOrderDate = orders[0]?.date;
|
||||
|
||||
const annualizedPerformancePercent = this.getAnnualizedPerformancePercent({
|
||||
daysInMarket: differenceInDays(new Date(), firstOrderDate),
|
||||
netPerformancePercent:
|
||||
performanceInformation.performance.currentNetPerformancePercent
|
||||
});
|
||||
|
||||
const totalBuy = this.getTotalByType(orders, currency, TypeOfOrder.BUY);
|
||||
const totalSell = this.getTotalByType(orders, currency, TypeOfOrder.SELL);
|
||||
|
||||
@ -722,6 +748,7 @@ export class PortfolioService {
|
||||
|
||||
return {
|
||||
...performanceInformation.performance,
|
||||
annualizedPerformancePercent,
|
||||
fees,
|
||||
firstOrderDate,
|
||||
netWorth,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
|
@ -17,7 +17,7 @@ export class SymbolService {
|
||||
const response = await this.dataProviderService.get([aSymbol]);
|
||||
const { currency, dataSource, marketPrice } = response[aSymbol] ?? {};
|
||||
|
||||
if (currency && dataSource && marketPrice) {
|
||||
if (dataSource && marketPrice) {
|
||||
return {
|
||||
dataSource,
|
||||
marketPrice,
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
hasPermission,
|
||||
permissions
|
||||
} from '@ghostfolio/common/permissions';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { benchmarks, currencyPairs } from '@ghostfolio/common/config';
|
||||
import {
|
||||
benchmarks,
|
||||
currencyPairs,
|
||||
ghostfolioFearAndGreedIndexSymbol
|
||||
} from '@ghostfolio/common/config';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
getUtc,
|
||||
@ -295,7 +299,7 @@ export class DataGatheringService {
|
||||
benchmarksToGather.push({
|
||||
dataSource: DataSource.RAKUTEN,
|
||||
date: startDate,
|
||||
symbol: 'GF.FEAR_AND_GREED_INDEX'
|
||||
symbol: ghostfolioFearAndGreedIndexSymbol
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
getToday,
|
||||
@ -47,11 +48,11 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
||||
try {
|
||||
const symbol = aSymbols[0];
|
||||
|
||||
if (symbol === 'GF.FEAR_AND_GREED_INDEX') {
|
||||
if (symbol === ghostfolioFearAndGreedIndexSymbol) {
|
||||
const fgi = await this.getFearAndGreedIndex();
|
||||
|
||||
return {
|
||||
'GF.FEAR_AND_GREED_INDEX': {
|
||||
[ghostfolioFearAndGreedIndexSymbol]: {
|
||||
currency: undefined,
|
||||
dataSource: DataSource.RAKUTEN,
|
||||
marketPrice: fgi.now.value,
|
||||
@ -82,7 +83,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
||||
try {
|
||||
const symbol = aSymbols[0];
|
||||
|
||||
if (symbol === 'GF.FEAR_AND_GREED_INDEX') {
|
||||
if (symbol === ghostfolioFearAndGreedIndexSymbol) {
|
||||
const fgi = await this.getFearAndGreedIndex();
|
||||
|
||||
try {
|
||||
@ -118,7 +119,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
'GF.FEAR_AND_GREED_INDEX': {
|
||||
[ghostfolioFearAndGreedIndexSymbol]: {
|
||||
[format(getYesterday(), DATE_FORMAT)]: {
|
||||
marketPrice: fgi.previousClose.value
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import { LinearScale } from 'chart.js';
|
||||
import { ArcElement } from 'chart.js';
|
||||
import { DoughnutController } from 'chart.js';
|
||||
import { Chart } from 'chart.js';
|
||||
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||
import * as Color from 'color';
|
||||
|
||||
@Component({
|
||||
@ -32,6 +33,7 @@ export class PortfolioProportionChartComponent
|
||||
@Input() keys: string[];
|
||||
@Input() locale: string;
|
||||
@Input() maxItems?: number;
|
||||
@Input() showLabels = false;
|
||||
@Input() positions: {
|
||||
[symbol: string]: Pick<PortfolioPosition, 'type'> & { value: number };
|
||||
};
|
||||
@ -48,7 +50,13 @@ export class PortfolioProportionChartComponent
|
||||
};
|
||||
|
||||
public constructor() {
|
||||
Chart.register(ArcElement, DoughnutController, LinearScale, Tooltip);
|
||||
Chart.register(
|
||||
ArcElement,
|
||||
ChartDataLabels,
|
||||
DoughnutController,
|
||||
LinearScale,
|
||||
Tooltip
|
||||
);
|
||||
}
|
||||
|
||||
public ngOnInit() {}
|
||||
@ -235,7 +243,30 @@ export class PortfolioProportionChartComponent
|
||||
data,
|
||||
options: {
|
||||
cutout: '70%',
|
||||
layout: {
|
||||
padding: this.showLabels === true ? 100 : 0
|
||||
},
|
||||
plugins: {
|
||||
datalabels: {
|
||||
color: (context) => {
|
||||
return this.getColorPalette()[
|
||||
context.dataIndex % this.getColorPalette().length
|
||||
];
|
||||
},
|
||||
display: this.showLabels === true ? 'auto' : false,
|
||||
labels: {
|
||||
index: {
|
||||
align: 'end',
|
||||
anchor: 'end',
|
||||
formatter: (value, context) => {
|
||||
return value > 0
|
||||
? context.chart.data.labels[context.dataIndex]
|
||||
: '';
|
||||
},
|
||||
offset: 8
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
|
@ -146,7 +146,7 @@
|
||||
<div class="col"><hr /></div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>Net Worth</div>
|
||||
<div class="d-flex flex-grow-1 font-weight-bold" i18n>Net Worth</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
@ -156,4 +156,17 @@
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1 ml-3" i18n>Annualized Performance</div>
|
||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : summary?.annualizedPerformancePercent"
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,5 +3,4 @@ export interface PositionDetailDialogParams {
|
||||
deviceType: string;
|
||||
locale: string;
|
||||
symbol: string;
|
||||
title: string;
|
||||
}
|
||||
|
@ -34,9 +34,11 @@ export class PositionDetailDialog implements OnDestroy {
|
||||
public marketPrice: number;
|
||||
public maxPrice: number;
|
||||
public minPrice: number;
|
||||
public name: string;
|
||||
public netPerformance: number;
|
||||
public netPerformancePercent: number;
|
||||
public quantity: number;
|
||||
public symbol: string;
|
||||
public transactionCount: number;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
@ -62,9 +64,11 @@ export class PositionDetailDialog implements OnDestroy {
|
||||
marketPrice,
|
||||
maxPrice,
|
||||
minPrice,
|
||||
name,
|
||||
netPerformance,
|
||||
netPerformancePercent,
|
||||
quantity,
|
||||
symbol,
|
||||
transactionCount
|
||||
}) => {
|
||||
this.averagePrice = averagePrice;
|
||||
@ -90,9 +94,11 @@ export class PositionDetailDialog implements OnDestroy {
|
||||
this.marketPrice = marketPrice;
|
||||
this.maxPrice = maxPrice;
|
||||
this.minPrice = minPrice;
|
||||
this.name = name;
|
||||
this.netPerformance = netPerformance;
|
||||
this.netPerformancePercent = netPerformancePercent;
|
||||
this.quantity = quantity;
|
||||
this.symbol = symbol;
|
||||
this.transactionCount = transactionCount;
|
||||
|
||||
if (isToday(parseISO(this.firstBuyDate))) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
<gf-dialog-header
|
||||
mat-dialog-title
|
||||
[deviceType]="data.deviceType"
|
||||
[title]="data.title ?? data.symbol"
|
||||
[title]="name ?? symbol"
|
||||
(closeButtonClicked)="onClose()"
|
||||
></gf-dialog-header>
|
||||
|
||||
|
@ -64,8 +64,7 @@ export class PositionComponent implements OnDestroy, OnInit {
|
||||
baseCurrency: this.baseCurrency,
|
||||
deviceType: this.deviceType,
|
||||
locale: this.locale,
|
||||
symbol: this.position?.symbol,
|
||||
title: this.position?.name
|
||||
symbol: this.position?.symbol
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
|
@ -87,7 +87,7 @@
|
||||
}"
|
||||
(click)="
|
||||
!this.ignoreAssetClasses.includes(row.assetClass) &&
|
||||
onOpenPositionDialog({ symbol: row.symbol, title: row.name })
|
||||
onOpenPositionDialog({ symbol: row.symbol })
|
||||
"
|
||||
></tr>
|
||||
</table>
|
||||
|
@ -57,14 +57,9 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
this.routeQueryParams = route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (
|
||||
params['positionDetailDialog'] &&
|
||||
params['symbol'] &&
|
||||
params['title']
|
||||
) {
|
||||
if (params['positionDetailDialog'] && params['symbol']) {
|
||||
this.openPositionDialog({
|
||||
symbol: params['symbol'],
|
||||
title: params['title']
|
||||
symbol: params['symbol']
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -96,15 +91,9 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||
}*/
|
||||
|
||||
public onOpenPositionDialog({
|
||||
symbol,
|
||||
title
|
||||
}: {
|
||||
symbol: string;
|
||||
title: string;
|
||||
}): void {
|
||||
public onOpenPositionDialog({ symbol }: { symbol: string }): void {
|
||||
this.router.navigate([], {
|
||||
queryParams: { positionDetailDialog: true, symbol, title }
|
||||
queryParams: { positionDetailDialog: true, symbol }
|
||||
});
|
||||
}
|
||||
|
||||
@ -116,18 +105,11 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public openPositionDialog({
|
||||
symbol,
|
||||
title
|
||||
}: {
|
||||
symbol: string;
|
||||
title: string;
|
||||
}): void {
|
||||
public openPositionDialog({ symbol }: { symbol: string }): void {
|
||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
||||
autoFocus: false,
|
||||
data: {
|
||||
symbol,
|
||||
title,
|
||||
baseCurrency: this.baseCurrency,
|
||||
deviceType: this.deviceType,
|
||||
locale: this.locale
|
||||
|
@ -255,8 +255,7 @@
|
||||
mat-row
|
||||
(click)="
|
||||
onOpenPositionDialog({
|
||||
symbol: row.symbol,
|
||||
title: row.SymbolProfile?.name
|
||||
symbol: row.symbol
|
||||
})
|
||||
"
|
||||
></tr>
|
||||
|
@ -86,8 +86,7 @@ export class TransactionsTableComponent
|
||||
.subscribe((params) => {
|
||||
if (params['positionDetailDialog'] && params['symbol']) {
|
||||
this.openPositionDialog({
|
||||
symbol: params['symbol'],
|
||||
title: params['title']
|
||||
symbol: params['symbol']
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -196,15 +195,9 @@ export class TransactionsTableComponent
|
||||
this.import.emit();
|
||||
}
|
||||
|
||||
public onOpenPositionDialog({
|
||||
symbol,
|
||||
title
|
||||
}: {
|
||||
symbol: string;
|
||||
title: string;
|
||||
}): void {
|
||||
public onOpenPositionDialog({ symbol }: { symbol: string }): void {
|
||||
this.router.navigate([], {
|
||||
queryParams: { positionDetailDialog: true, symbol, title }
|
||||
queryParams: { positionDetailDialog: true, symbol }
|
||||
});
|
||||
}
|
||||
|
||||
@ -216,18 +209,11 @@ export class TransactionsTableComponent
|
||||
this.transactionToClone.emit(aTransaction);
|
||||
}
|
||||
|
||||
public openPositionDialog({
|
||||
symbol,
|
||||
title
|
||||
}: {
|
||||
symbol: string;
|
||||
title: string;
|
||||
}): void {
|
||||
public openPositionDialog({ symbol }: { symbol: string }): void {
|
||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
||||
autoFocus: false,
|
||||
data: {
|
||||
symbol,
|
||||
title,
|
||||
baseCurrency: this.baseCurrency,
|
||||
deviceType: this.deviceType,
|
||||
locale: this.locale
|
||||
|
@ -32,7 +32,12 @@
|
||||
</p>
|
||||
<p>
|
||||
If you encounter a bug or would like to suggest an improvement or a
|
||||
new feature, please tweet to
|
||||
new feature, please join the Ghostfolio
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
title="Join the Ghostfolio Slack channel"
|
||||
>Slack channel</a
|
||||
>, tweet to
|
||||
<a
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
title="Tweet to Ghostfolio on Twitter"
|
||||
@ -65,6 +70,14 @@
|
||||
>
|
||||
<ion-icon name="mail" size="large"></ion-icon>
|
||||
</a>
|
||||
<a
|
||||
class="mx-2"
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
mat-icon-button
|
||||
title="Join the Ghostfolio Slack channel"
|
||||
>
|
||||
<ion-icon name="logo-slack" size="large"></ion-icon>
|
||||
</a>
|
||||
<a
|
||||
class="mx-2"
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
SettingsStorageService
|
||||
} from '@ghostfolio/client/services/settings-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
|
||||
import {
|
||||
PortfolioPerformance,
|
||||
PortfolioSummary,
|
||||
@ -111,7 +112,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
|
||||
if (this.hasPermissionToAccessFearAndGreedIndex) {
|
||||
this.dataService
|
||||
.fetchSymbolItem('GF.FEAR_AND_GREED_INDEX')
|
||||
.fetchSymbolItem(ghostfolioFearAndGreedIndexSymbol)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ marketPrice }) => {
|
||||
this.fearAndGreedIndex = marketPrice;
|
||||
|
@ -16,6 +16,9 @@
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
||||
margin-bottom: env(safe-area-inset-bottom);
|
||||
margin-bottom: constant(safe-area-inset-bottom);
|
||||
|
||||
::ng-deep {
|
||||
.mat-tab-body-wrapper {
|
||||
height: 100%;
|
||||
|
@ -94,6 +94,7 @@
|
||||
[keys]="['name']"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="symbols"
|
||||
[showLabels]="deviceType !== 'mobile'"
|
||||
></gf-portfolio-proportion-chart>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
@ -1,7 +1,7 @@
|
||||
:host {
|
||||
.allocations-by-symbol {
|
||||
gf-portfolio-proportion-chart {
|
||||
max-width: 67vh;
|
||||
max-width: 80vh;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,8 +2,8 @@ import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||
import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component';
|
||||
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||
|
||||
import { WebauthnPageRoutingModule } from './webauthn-page-routing.module';
|
||||
|
||||
|
@ -12,6 +12,9 @@
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
||||
margin-bottom: env(safe-area-inset-bottom);
|
||||
margin-bottom: constant(safe-area-inset-bottom);
|
||||
|
||||
::ng-deep {
|
||||
.mat-tab-body-wrapper {
|
||||
height: 100%;
|
||||
|
@ -6,7 +6,10 @@ import {
|
||||
PublicKeyCredentialRequestOptionsJSON
|
||||
} from '@ghostfolio/api/app/auth/interfaces/simplewebauthn';
|
||||
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
|
||||
import { startAssertion, startAttestation } from '@simplewebauthn/browser';
|
||||
import {
|
||||
startAuthentication,
|
||||
startRegistration
|
||||
} from '@simplewebauthn/browser';
|
||||
import { of } from 'rxjs';
|
||||
import { catchError, switchMap, tap } from 'rxjs/operators';
|
||||
|
||||
@ -32,7 +35,7 @@ export class WebAuthnService {
|
||||
public register() {
|
||||
return this.http
|
||||
.get<PublicKeyCredentialCreationOptionsJSON>(
|
||||
`/api/auth/webauthn/generate-attestation-options`,
|
||||
`/api/auth/webauthn/generate-registration-options`,
|
||||
{}
|
||||
)
|
||||
.pipe(
|
||||
@ -41,7 +44,7 @@ export class WebAuthnService {
|
||||
return of(null);
|
||||
}),
|
||||
switchMap((attOps) => {
|
||||
return startAttestation(attOps);
|
||||
return startRegistration(attOps);
|
||||
}),
|
||||
switchMap((attResp) => {
|
||||
return this.http.post<AuthDeviceDto>(
|
||||
@ -83,7 +86,7 @@ export class WebAuthnService {
|
||||
{ deviceId }
|
||||
)
|
||||
.pipe(
|
||||
switchMap(startAssertion),
|
||||
switchMap(startAuthentication),
|
||||
switchMap((assertionResponse) => {
|
||||
return this.http.post<{ authToken: string }>(
|
||||
`/api/auth/webauthn/verify-assertion`,
|
||||
|
@ -27,7 +27,10 @@
|
||||
name="twitter:title"
|
||||
content="Ghostfolio – Open Source Wealth Management Software"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="initial-scale=1, viewport-fit=cover, width=device-width"
|
||||
/>
|
||||
<meta property="og:description" content="" />
|
||||
<meta
|
||||
property="og:title"
|
||||
|
@ -28,6 +28,7 @@ export const currencyPairs: Partial<
|
||||
|
||||
export const ghostfolioScraperApiSymbolPrefix = '_GF_';
|
||||
export const ghostfolioCashSymbol = `${ghostfolioScraperApiSymbolPrefix}CASH`;
|
||||
export const ghostfolioFearAndGreedIndexSymbol = `${ghostfolioScraperApiSymbolPrefix}FEAR_AND_GREED_INDEX`;
|
||||
|
||||
export const locale = 'de-CH';
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { PortfolioPerformance } from './portfolio-performance.interface';
|
||||
|
||||
export interface PortfolioSummary extends PortfolioPerformance {
|
||||
annualizedPerformancePercent: number;
|
||||
cash: number;
|
||||
committedFunds: number;
|
||||
fees: number;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { AccessWithGranteeUser } from './access-with-grantee-user.type';
|
||||
import { DateRange } from './date-range.type';
|
||||
import { Granularity } from './granularity.type';
|
||||
import { OrderWithAccount } from './order-with-account.type';
|
||||
import { RequestWithUser } from './request-with-user.type';
|
||||
import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
|
||||
import type { DateRange } from './date-range.type';
|
||||
import type { Granularity } from './granularity.type';
|
||||
import type { OrderWithAccount } from './order-with-account.type';
|
||||
import type { RequestWithUser } from './request-with-user.type';
|
||||
|
||||
export {
|
||||
export type {
|
||||
AccessWithGranteeUser,
|
||||
DateRange,
|
||||
Granularity,
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "1.48.0",
|
||||
"version": "1.52.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -68,9 +68,9 @@
|
||||
"@nestjs/serve-static": "2.1.4",
|
||||
"@nrwl/angular": "12.8.0",
|
||||
"@prisma/client": "2.30.2",
|
||||
"@simplewebauthn/browser": "3.0.0",
|
||||
"@simplewebauthn/server": "3.0.0",
|
||||
"@simplewebauthn/typescript-types": "3.0.0",
|
||||
"@simplewebauthn/browser": "4.1.0",
|
||||
"@simplewebauthn/server": "4.1.0",
|
||||
"@simplewebauthn/typescript-types": "4.0.0",
|
||||
"@stripe/stripe-js": "1.15.0",
|
||||
"alphavantage": "2.2.0",
|
||||
"angular-material-css-vars": "2.1.2",
|
||||
@ -81,6 +81,7 @@
|
||||
"cache-manager-redis-store": "2.0.0",
|
||||
"chart.js": "3.5.0",
|
||||
"chartjs-adapter-date-fns": "2.0.0",
|
||||
"chartjs-plugin-datalabels": "2.0.0",
|
||||
"cheerio": "1.0.0-rc.6",
|
||||
"class-transformer": "0.3.2",
|
||||
"class-validator": "0.13.1",
|
||||
|
47
yarn.lock
47
yarn.lock
@ -2519,7 +2519,7 @@
|
||||
consola "^2.15.0"
|
||||
node-fetch "^2.6.1"
|
||||
|
||||
"@peculiar/asn1-android@^2.0.26":
|
||||
"@peculiar/asn1-android@^2.0.38":
|
||||
version "2.0.38"
|
||||
resolved "https://registry.yarnpkg.com/@peculiar/asn1-android/-/asn1-android-2.0.38.tgz#193281f5a232e323d6f2c069c7a8e8e8f4a994bd"
|
||||
integrity sha512-krWyggV6FgYf3fEPKVNjHVecLcQWlAu3/YhOyN+/L43dNKcsmqiEvuhqplh3aiXF62Ds0pqzqttWmdvoVqmSVQ==
|
||||
@ -2528,7 +2528,7 @@
|
||||
asn1js "^2.1.1"
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@peculiar/asn1-schema@^2.0.26", "@peculiar/asn1-schema@^2.0.38":
|
||||
"@peculiar/asn1-schema@^2.0.38":
|
||||
version "2.0.38"
|
||||
resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.38.tgz#98b6f12daad275ecd6774dfe31fb62f362900412"
|
||||
integrity sha512-zZ64UpCTm9me15nuCpPgJghSdbEm8atcDQPCyK+bKXjZAQ1735NCZXCSCfbckbQ4MH36Rm9403n/qMq77LFDzQ==
|
||||
@ -2538,7 +2538,7 @@
|
||||
pvtsutils "^1.2.0"
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@peculiar/asn1-x509@^2.0.26":
|
||||
"@peculiar/asn1-x509@^2.0.38":
|
||||
version "2.0.38"
|
||||
resolved "https://registry.yarnpkg.com/@peculiar/asn1-x509/-/asn1-x509-2.0.38.tgz#7ff3b5478d9c3784f0eb2fbe7693509da9de0a43"
|
||||
integrity sha512-10aK9fSxlc1DK9nEcwh+WPFNhAheXSE9RbI5MyS7FdBhgq+Mz4Z9JqFfaBZm1Qp+5mPtUMOP6cXVo7aaYlgq7A==
|
||||
@ -2613,32 +2613,32 @@
|
||||
"@angular-devkit/schematics" "12.1.4"
|
||||
jsonc-parser "3.0.0"
|
||||
|
||||
"@simplewebauthn/browser@3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@simplewebauthn/browser/-/browser-3.0.0.tgz#3d76b199c9f474408a7ed75d86004423dd6ae38a"
|
||||
integrity sha512-P661gZX/QW0Rg2NRAMtW84Q3u4nhXkPef9LLU4btLJFYoXO8RBFfxcmyqwyf2QEb4B7+lFdp5EWfZV5T7FvuHw==
|
||||
"@simplewebauthn/browser@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@simplewebauthn/browser/-/browser-4.1.0.tgz#3e7fd66729405d6a2a2a187c93577b90a8e41786"
|
||||
integrity sha512-tIsEfShC1rrqrsNb44tOFuSriAFCz4tkdDnCjHfn2rYxgz+t+yqEvuIRfJHQpFrWSnZPdsjrAHtasj6lzfGI6w==
|
||||
|
||||
"@simplewebauthn/server@3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@simplewebauthn/server/-/server-3.0.0.tgz#eb1a5bbe2ecdda54363b178f4bb3e134f25641f0"
|
||||
integrity sha512-ymGX2obBrhY9R3OxrpCYaNGAovFHmMlQrGoNdVOe2R2JUBXC1Rg5JEUl1lGyaRykN1SyZqLgz86wAjDVuRITTA==
|
||||
"@simplewebauthn/server@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@simplewebauthn/server/-/server-4.1.0.tgz#9ad2e32cffa83833ff8a633775b2ace5e6926fa0"
|
||||
integrity sha512-52X5/U+5Fo0XYG1TuBBGgG0ap9c0ffpeq0GZfFio/DZDW4He0Arb7Q/XkHw96JK0X1sfRKNmnfC+NImplvIimA==
|
||||
dependencies:
|
||||
"@peculiar/asn1-android" "^2.0.26"
|
||||
"@peculiar/asn1-schema" "^2.0.26"
|
||||
"@peculiar/asn1-x509" "^2.0.26"
|
||||
"@simplewebauthn/typescript-types" "^3.0.0"
|
||||
"@peculiar/asn1-android" "^2.0.38"
|
||||
"@peculiar/asn1-schema" "^2.0.38"
|
||||
"@peculiar/asn1-x509" "^2.0.38"
|
||||
"@simplewebauthn/typescript-types" "^4.0.0"
|
||||
base64url "^3.0.1"
|
||||
cbor "^5.1.0"
|
||||
elliptic "^6.5.3"
|
||||
jsrsasign "^10.2.0"
|
||||
jsrsasign "^10.4.0"
|
||||
jwk-to-pem "^2.0.4"
|
||||
node-fetch "^2.6.0"
|
||||
node-rsa "^1.1.1"
|
||||
|
||||
"@simplewebauthn/typescript-types@3.0.0", "@simplewebauthn/typescript-types@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@simplewebauthn/typescript-types/-/typescript-types-3.0.0.tgz#6712e9619d860f54f571cd27dbe167b2d9e5ab87"
|
||||
integrity sha512-bsk3EQWzPOZwP9C+ETVhcFDpZywY5sTqmNuGkNm3aNpc9Xh/mqZjy8nL0Sm7xwrlhY0zWAlOaIWQ3LvN5SoFhg==
|
||||
"@simplewebauthn/typescript-types@4.0.0", "@simplewebauthn/typescript-types@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@simplewebauthn/typescript-types/-/typescript-types-4.0.0.tgz#46ae4e69cb07305c57093a3ed99555437dfe0d49"
|
||||
integrity sha512-jqQ0bCeBO96CytB397vSrQ8ipozQzAmI57izA7izyglyu35JBV90I7+75fSX+ZGNHmMwDNnA3EGYtBLOIpkJEg==
|
||||
|
||||
"@sinonjs/commons@^1.7.0":
|
||||
version "1.8.3"
|
||||
@ -6112,6 +6112,11 @@ chartjs-adapter-date-fns@2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz#5e53b2f660b993698f936f509c86dddf9ed44c6b"
|
||||
integrity sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==
|
||||
|
||||
chartjs-plugin-datalabels@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.0.0.tgz#caacefb26803d968785071eab012dde8746c5939"
|
||||
integrity sha512-WBsWihphzM0Y8fmQVm89+iy99mmgejmj5/jcsYqwxSioLRL/zqJ4Scv/eXq5ZqvG3TpojlGzZLeaOaSvDm7fwA==
|
||||
|
||||
check-more-types@^2.24.0:
|
||||
version "2.24.0"
|
||||
resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
|
||||
@ -11338,7 +11343,7 @@ jsprim@^1.2.2:
|
||||
json-schema "0.2.3"
|
||||
verror "1.10.0"
|
||||
|
||||
jsrsasign@^10.2.0:
|
||||
jsrsasign@^10.4.0:
|
||||
version "10.4.0"
|
||||
resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-10.4.0.tgz#362cc787079c03a363a958c03eb68d8545ba92f7"
|
||||
integrity sha512-C8qLhiAssh/b74KJpGhWuFGG9cFhJqMCVuuHXRibb3Z5vPuAW0ue0jUirpoExCdpdhv4nD3sZ1DAwJURYJTm9g==
|
||||
|
Reference in New Issue
Block a user