Compare commits

..

17 Commits

Author SHA1 Message Date
678f1f0051 Release 1.173.0 (#1095) 2022-07-23 20:38:53 +02:00
71c7e37b5a Bugfix/fix currency inconsistency with usx (#1094)
* Support USX

* Update changelog
2022-07-23 20:37:26 +02:00
80459371f3 Release 1.172.0 (#1093) 2022-07-23 12:09:38 +02:00
35f1f348a8 Feature/add blog post ghostfolio meets internet identity (#1092)
* Add blog post: Ghostfolio meets Internet Identity

* Update changelog
2022-07-23 12:06:49 +02:00
0bb0b12991 Release 1.171.0 (#1091) 2022-07-22 19:57:04 +02:00
d887de50d2 Feature/setup internet identity (#1080)
* Setup Internet Identity as social login provider

* Update changelog
2022-07-22 19:55:33 +02:00
2571e5b8c0 Feature/improve empty states (#1090)
* Improve empty states

* Update changelog
2022-07-22 19:33:06 +02:00
e444d717e5 Add tests for investments by month (#1089)
* Fix investments by month

* Add tests for investments and investments by month

* Update changelog
2022-07-22 19:00:36 +02:00
1866e26c1d Fix distorted tooltip (#1088)
* Fix distorted tooltip

* Update changelog
2022-07-21 20:36:16 +02:00
9923074e04 Add script to open prisma studio with prod env variables (#1087) 2022-07-20 09:16:02 +02:00
c367e61b85 Release 1.170.0 (#1086) 2022-07-19 20:30:59 +02:00
364f1ad9b9 Feature/add support for ust usd (#1085)
* Add UST

* Update changelog
2022-07-19 20:29:42 +02:00
2394cbd6fe Feature/support tags in create and update order dto (#1084)
* Support tags in create or update order

* Update changelog
2022-07-19 20:24:01 +02:00
a74d5cce20 Feature/remove activities import limit for premium users (#1082)
* Remove activities import limit for premium users

* Update changelog
2022-07-17 20:44:28 +02:00
95bcc3f32d Feature/remove alias from user interface (#1083)
* Remove alias from user interface

* Update changelog
2022-07-17 11:05:23 +02:00
e9dbd4a55d Add blog post to resources (#1081) 2022-07-17 10:09:11 +02:00
d440b09dc9 Improve quotes (#1078) 2022-07-15 09:04:44 +02:00
70 changed files with 982 additions and 170 deletions

3
.gitignore vendored
View File

@ -24,15 +24,16 @@
# misc # misc
/.angular/cache /.angular/cache
.env.prod
/.sass-cache /.sass-cache
/connect.lock /connect.lock
/coverage /coverage
/dist /dist
/libpeerconnection.log /libpeerconnection.log
npm-debug.log npm-debug.log
yarn-error.log
testem.log testem.log
/typings /typings
yarn-error.log
# System Files # System Files
.DS_Store .DS_Store

View File

@ -5,6 +5,56 @@ 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).
## 1.173.0 - 23.07.2022
### Fixed
- Fixed an issue with the currency inconsistency in the _Yahoo Finance_ service (convert from `USX` to `USD`)
## 1.172.0 - 23.07.2022
### Added
- Added a blog post: _Ghostfolio meets Internet Identity_
## 1.171.0 - 22.07.2022
### Added
- Added _Internet Identity_ as a new social login provider
### Changed
- Improved the empty state of the
- _Analysis_ section
- _Holdings_ section
- performance chart on the home page
### Fixed
- Fixed the distorted tooltip in the performance chart on the home page
- Fixed a calculation issue of the current month in the investment timeline grouped by month
### Todo
- Apply data migration (`yarn database:migrate`)
## 1.170.0 - 19.07.2022
### Added
- Added support for the tags in the create or edit transaction dialog
- Added support for the cryptocurrency _TerraUSD_ (`UST-USD`)
### Changed
- Removed the alias from the user interface as a preparation to remove it from the `User` database schema
- Removed the activities import limit for users with a subscription
### Todo
- Rename the environment variable from `MAX_ORDERS_TO_IMPORT` to `MAX_ACTIVITIES_TO_IMPORT`
## 1.169.0 - 14.07.2022 ## 1.169.0 - 14.07.2022
### Added ### Added

View File

@ -1,6 +1,5 @@
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
@ -24,7 +23,6 @@ export class AdminService {
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly dataGatheringService: DataGatheringService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly marketDataService: MarketDataService, private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
@ -174,7 +172,6 @@ export class AdminService {
_count: { _count: {
select: { Account: true, Order: true } select: { Account: true, Order: true }
}, },
alias: true,
Analytics: { Analytics: {
select: { select: {
activityCount: true, activityCount: true,
@ -194,7 +191,7 @@ export class AdminService {
}); });
return usersWithAnalytics.map( return usersWithAnalytics.map(
({ _count, alias, Analytics, createdAt, id, Subscription }) => { ({ _count, Analytics, createdAt, id, Subscription }) => {
const daysSinceRegistration = const daysSinceRegistration =
differenceInDays(new Date(), createdAt) + 1; differenceInDays(new Date(), createdAt) + 1;
const engagement = Analytics.activityCount / daysSinceRegistration; const engagement = Analytics.activityCount / daysSinceRegistration;
@ -206,7 +203,6 @@ export class AdminService {
: undefined; : undefined;
return { return {
alias,
createdAt, createdAt,
engagement, engagement,
id, id,

View File

@ -1,5 +1,6 @@
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service'; import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { OAuthResponse } from '@ghostfolio/common/interfaces';
import { import {
Body, Body,
Controller, Controller,
@ -31,7 +32,9 @@ export class AuthController {
) {} ) {}
@Get('anonymous/:accessToken') @Get('anonymous/:accessToken')
public async accessTokenLogin(@Param('accessToken') accessToken: string) { public async accessTokenLogin(
@Param('accessToken') accessToken: string
): Promise<OAuthResponse> {
try { try {
const authToken = await this.authService.validateAnonymousLogin( const authToken = await this.authService.validateAnonymousLogin(
accessToken accessToken
@ -65,6 +68,23 @@ export class AuthController {
} }
} }
@Get('internet-identity/:principalId')
public async internetIdentityLogin(
@Param('principalId') principalId: string
): Promise<OAuthResponse> {
try {
const authToken = await this.authService.validateInternetIdentityLogin(
principalId
);
return { authToken };
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
}
@Get('webauthn/generate-registration-options') @Get('webauthn/generate-registration-options')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async generateRegistrationOptions() { public async generateRegistrationOptions() {

View File

@ -2,6 +2,7 @@ import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { Provider } from '@prisma/client';
import { ValidateOAuthLoginParams } from './interfaces/interfaces'; import { ValidateOAuthLoginParams } from './interfaces/interfaces';
@ -13,7 +14,7 @@ export class AuthService {
private readonly userService: UserService private readonly userService: UserService
) {} ) {}
public async validateAnonymousLogin(accessToken: string) { public async validateAnonymousLogin(accessToken: string): Promise<string> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
const hashedAccessToken = this.userService.createAccessToken( const hashedAccessToken = this.userService.createAccessToken(
@ -26,7 +27,7 @@ export class AuthService {
}); });
if (user) { if (user) {
const jwt: string = this.jwtService.sign({ const jwt = this.jwtService.sign({
id: user.id id: user.id
}); });
@ -40,6 +41,33 @@ export class AuthService {
}); });
} }
public async validateInternetIdentityLogin(principalId: string) {
try {
const provider: Provider = 'INTERNET_IDENTITY';
let [user] = await this.userService.users({
where: { provider, thirdPartyId: principalId }
});
if (!user) {
// Create new user if not found
user = await this.userService.createUser({
provider,
thirdPartyId: principalId
});
}
return this.jwtService.sign({
id: user.id
});
} catch (error) {
throw new InternalServerErrorException(
'validateInternetIdentityLogin',
error.message
);
}
}
public async validateOAuthLogin({ public async validateOAuthLogin({
provider, provider,
thirdPartyId thirdPartyId
@ -57,13 +85,14 @@ export class AuthService {
}); });
} }
const jwt: string = this.jwtService.sign({ return this.jwtService.sign({
id: user.id id: user.id
}); });
} catch (error) {
return jwt; throw new InternalServerErrorException(
} catch (err) { 'validateOAuthLogin',
throw new InternalServerErrorException('validateOAuthLogin', err.message); error.message
);
} }
} }
} }

View File

@ -34,8 +34,20 @@ export class ImportController {
); );
} }
let maxActivitiesToImport = this.configurationService.get(
'MAX_ACTIVITIES_TO_IMPORT'
);
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Premium'
) {
maxActivitiesToImport = Number.MAX_SAFE_INTEGER;
}
try { try {
return await this.importService.import({ return await this.importService.import({
maxActivitiesToImport,
activities: importData.activities, activities: importData.activities,
userId: this.request.user.id userId: this.request.user.id
}); });

View File

@ -17,9 +17,11 @@ export class ImportService {
public async import({ public async import({
activities, activities,
maxActivitiesToImport,
userId userId
}: { }: {
activities: Partial<CreateOrderDto>[]; activities: Partial<CreateOrderDto>[];
maxActivitiesToImport: number;
userId: string; userId: string;
}): Promise<void> { }): Promise<void> {
for (const activity of activities) { for (const activity of activities) {
@ -32,7 +34,11 @@ export class ImportService {
} }
} }
await this.validateActivities({ activities, userId }); await this.validateActivities({
activities,
maxActivitiesToImport,
userId
});
const accountIds = (await this.accountService.getAccounts(userId)).map( const accountIds = (await this.accountService.getAccounts(userId)).map(
(account) => { (account) => {
@ -81,19 +87,15 @@ export class ImportService {
private async validateActivities({ private async validateActivities({
activities, activities,
maxActivitiesToImport,
userId userId
}: { }: {
activities: Partial<CreateOrderDto>[]; activities: Partial<CreateOrderDto>[];
maxActivitiesToImport: number;
userId: string; userId: string;
}) { }) {
if ( if (activities?.length > maxActivitiesToImport) {
activities?.length > this.configurationService.get('MAX_ORDERS_TO_IMPORT') throw new Error(`Too many activities (${maxActivitiesToImport} at most)`);
) {
throw new Error(
`Too many activities (${this.configurationService.get(
'MAX_ORDERS_TO_IMPORT'
)} at most)`
);
} }
const existingActivities = await this.orderService.orders({ const existingActivities = await this.orderService.orders({

View File

@ -1,5 +1,12 @@
import { AssetClass, AssetSubClass, DataSource, Type } from '@prisma/client';
import { import {
AssetClass,
AssetSubClass,
DataSource,
Tag,
Type
} from '@prisma/client';
import {
IsArray,
IsEnum, IsEnum,
IsISO8601, IsISO8601,
IsNumber, IsNumber,
@ -39,6 +46,10 @@ export class CreateOrderDto {
@IsString() @IsString()
symbol: string; symbol: string;
@IsArray()
@IsOptional()
tags?: Tag[];
@IsEnum(Type, { each: true }) @IsEnum(Type, { each: true })
type: Type; type: Type;

View File

@ -16,6 +16,7 @@ import {
DataSource, DataSource,
Order, Order,
Prisma, Prisma,
Tag,
Type as TypeOfOrder Type as TypeOfOrder
} from '@prisma/client'; } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
@ -71,6 +72,7 @@ export class OrderService {
currency?: string; currency?: string;
dataSource?: DataSource; dataSource?: DataSource;
symbol?: string; symbol?: string;
tags?: Tag[];
userId: string; userId: string;
} }
): Promise<Order> { ): Promise<Order> {
@ -80,6 +82,8 @@ export class OrderService {
return account.isDefault === true; return account.isDefault === true;
}); });
const tags = data.tags ?? [];
let Account = { let Account = {
connect: { connect: {
id_userId: { id_userId: {
@ -142,6 +146,7 @@ export class OrderService {
delete data.currency; delete data.currency;
delete data.dataSource; delete data.dataSource;
delete data.symbol; delete data.symbol;
delete data.tags;
delete data.userId; delete data.userId;
const orderData: Prisma.OrderCreateInput = data; const orderData: Prisma.OrderCreateInput = data;
@ -150,7 +155,12 @@ export class OrderService {
data: { data: {
...orderData, ...orderData,
Account, Account,
isDraft isDraft,
tags: {
connect: tags.map(({ id }) => {
return { id };
})
}
} }
}); });
} }
@ -298,6 +308,7 @@ export class OrderService {
currency?: string; currency?: string;
dataSource?: DataSource; dataSource?: DataSource;
symbol?: string; symbol?: string;
tags?: Tag[];
}; };
where: Prisma.OrderWhereUniqueInput; where: Prisma.OrderWhereUniqueInput;
}): Promise<Order> { }): Promise<Order> {
@ -305,6 +316,8 @@ export class OrderService {
delete data.Account; delete data.Account;
} }
const tags = data.tags ?? [];
let isDraft = false; let isDraft = false;
if (data.type === 'ITEM') { if (data.type === 'ITEM') {
@ -331,11 +344,17 @@ export class OrderService {
delete data.currency; delete data.currency;
delete data.dataSource; delete data.dataSource;
delete data.symbol; delete data.symbol;
delete data.tags;
return this.prismaService.order.update({ return this.prismaService.order.update({
data: { data: {
...data, ...data,
isDraft isDraft,
tags: {
connect: tags.map(({ id }) => {
return { id };
})
}
}, },
where where
}); });

View File

@ -1,5 +1,12 @@
import { AssetClass, AssetSubClass, DataSource, Type } from '@prisma/client';
import { import {
AssetClass,
AssetSubClass,
DataSource,
Tag,
Type
} from '@prisma/client';
import {
IsArray,
IsEnum, IsEnum,
IsISO8601, IsISO8601,
IsNumber, IsNumber,
@ -41,6 +48,10 @@ export class UpdateOrderDto {
@IsString() @IsString()
symbol: string; symbol: string;
@IsArray()
@IsOptional()
tags?: Tag[];
@IsString() @IsString()
type: Type; type: Type;

View File

@ -62,6 +62,10 @@ describe('PortfolioCalculator', () => {
parseDate('2021-11-22') parseDate('2021-11-22')
); );
const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth();
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual({
@ -91,6 +95,15 @@ describe('PortfolioCalculator', () => {
], ],
totalInvestment: new Big('0') totalInvestment: new Big('0')
}); });
expect(investments).toEqual([
{ date: '2021-11-22', investment: new Big('285.8') },
{ date: '2021-11-30', investment: new Big('0') }
]);
expect(investmentsByMonth).toEqual([
{ date: '2021-11-01', investment: new Big('12.6') }
]);
}); });
}); });
}); });

View File

@ -51,6 +51,10 @@ describe('PortfolioCalculator', () => {
parseDate('2021-11-30') parseDate('2021-11-30')
); );
const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth();
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual({
@ -80,6 +84,14 @@ describe('PortfolioCalculator', () => {
], ],
totalInvestment: new Big('273.2') totalInvestment: new Big('273.2')
}); });
expect(investments).toEqual([
{ date: '2021-11-30', investment: new Big('273.2') }
]);
expect(investmentsByMonth).toEqual([
{ date: '2021-11-01', investment: new Big('273.2') }
]);
}); });
}); });
}); });

View File

@ -39,6 +39,10 @@ describe('PortfolioCalculator', () => {
new Date() new Date()
); );
const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth();
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual({
@ -51,6 +55,10 @@ describe('PortfolioCalculator', () => {
positions: [], positions: [],
totalInvestment: new Big(0) totalInvestment: new Big(0)
}); });
expect(investments).toEqual([]);
expect(investmentsByMonth).toEqual([]);
}); });
}); });
}); });

View File

@ -62,6 +62,10 @@ describe('PortfolioCalculator', () => {
parseDate('2022-03-07') parseDate('2022-03-07')
); );
const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth();
spy.mockRestore(); spy.mockRestore();
expect(currentPositions).toEqual({ expect(currentPositions).toEqual({
@ -91,6 +95,16 @@ describe('PortfolioCalculator', () => {
], ],
totalInvestment: new Big('75.80') totalInvestment: new Big('75.80')
}); });
expect(investments).toEqual([
{ date: '2022-03-07', investment: new Big('151.6') },
{ date: '2022-04-08', investment: new Big('75.8') }
]);
expect(investmentsByMonth).toEqual([
{ date: '2022-03-01', investment: new Big('151.6') },
{ date: '2022-04-01', investment: new Big('-85.73') }
]);
}); });
}); });
}); });

View File

@ -332,7 +332,7 @@ export class PortfolioCalculator {
} }
const investments = []; const investments = [];
let currentDate = parseDate(this.orders[0].date); let currentDate: Date;
let investmentByMonth = new Big(0); let investmentByMonth = new Big(0);
for (const [index, order] of this.orders.entries()) { for (const [index, order] of this.orders.entries()) {
@ -340,27 +340,34 @@ export class PortfolioCalculator {
isSameMonth(parseDate(order.date), currentDate) && isSameMonth(parseDate(order.date), currentDate) &&
isSameYear(parseDate(order.date), currentDate) isSameYear(parseDate(order.date), currentDate)
) { ) {
// Same month: Add up investments
investmentByMonth = investmentByMonth.plus( investmentByMonth = investmentByMonth.plus(
order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type)) order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
); );
} else {
// New month: Store previous month and reset
if (index === this.orders.length - 1) { if (currentDate) {
investments.push({ investments.push({
date: format(set(currentDate, { date: 1 }), DATE_FORMAT), date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
investment: investmentByMonth investment: investmentByMonth
}); });
} }
} else {
investments.push({
date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
investment: investmentByMonth
});
currentDate = parseDate(order.date); currentDate = parseDate(order.date);
investmentByMonth = order.quantity investmentByMonth = order.quantity
.mul(order.unitPrice) .mul(order.unitPrice)
.mul(this.getFactor(order.type)); .mul(this.getFactor(order.type));
} }
if (index === this.orders.length - 1) {
// Store current month (latest order)
investments.push({
date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
investment: investmentByMonth
});
}
} }
return investments; return investments;

View File

@ -36,14 +36,7 @@ export class UserService {
} }
public async getUser( public async getUser(
{ { Account, id, permissions, Settings, subscription }: UserWithSettings,
Account,
alias,
id,
permissions,
Settings,
subscription
}: UserWithSettings,
aLocale = locale aLocale = locale
): Promise<IUser> { ): Promise<IUser> {
const access = await this.prismaService.access.findMany({ const access = await this.prismaService.access.findMany({
@ -63,7 +56,6 @@ export class UserService {
} }
return { return {
alias,
id, id,
permissions, permissions,
subscription, subscription,

View File

@ -2,5 +2,6 @@
"LUNA1": "Terra", "LUNA1": "Terra",
"LUNA2": "Terra", "LUNA2": "Terra",
"SGB1": "Songbird", "SGB1": "Songbird",
"UNI1": "Uniswap" "UNI1": "Uniswap",
"UST": "TerraUSD"
} }

View File

@ -33,8 +33,8 @@ export class ConfigurationService {
GOOGLE_SHEETS_PRIVATE_KEY: str({ default: '' }), GOOGLE_SHEETS_PRIVATE_KEY: str({ default: '' }),
HOST: host({ default: '0.0.0.0' }), HOST: host({ default: '0.0.0.0' }),
JWT_SECRET_KEY: str({}), JWT_SECRET_KEY: str({}),
MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }),
MAX_ITEM_IN_CACHE: num({ default: 9999 }), MAX_ITEM_IN_CACHE: num({ default: 9999 }),
MAX_ORDERS_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }),
PORT: port({ default: 3333 }), PORT: port({ default: 3333 }),
RAKUTEN_RAPID_API_KEY: str({ default: '' }), RAKUTEN_RAPID_API_KEY: str({ default: '' }),
REDIS_HOST: host({ default: 'localhost' }), REDIS_HOST: host({ default: 'localhost' }),

View File

@ -266,6 +266,16 @@ export class YahooFinanceService implements DataProviderInterface {
} }
} }
if (yahooFinanceSymbols.includes('USDUSX=X')) {
// Convert USD to USX (cent)
response['USDUSX'] = {
currency: 'USX',
dataSource: this.getName(),
marketPrice: new Big(1).mul(100).toNumber(),
marketState: 'open'
};
}
return response; return response;
} catch (error) { } catch (error) {
Logger.error(error, 'YahooFinanceService'); Logger.error(error, 'YahooFinanceService');

View File

@ -122,15 +122,6 @@ export class ExchangeRateDataService {
return 0; return 0;
} }
const hasNaN = Object.values(this.exchangeRates).some((exchangeRate) => {
return isNaN(exchangeRate);
});
if (hasNaN) {
// Reinitialize if data is not loaded correctly
this.initialize();
}
let factor = 1; let factor = 1;
if (aFromCurrency !== aToCurrency) { if (aFromCurrency !== aToCurrency) {

View File

@ -23,8 +23,8 @@ export interface Environment extends CleanedEnvAccessors {
GOOGLE_SHEETS_ID: string; GOOGLE_SHEETS_ID: string;
GOOGLE_SHEETS_PRIVATE_KEY: string; GOOGLE_SHEETS_PRIVATE_KEY: string;
JWT_SECRET_KEY: string; JWT_SECRET_KEY: string;
MAX_ACTIVITIES_TO_IMPORT: number;
MAX_ITEM_IN_CACHE: number; MAX_ITEM_IN_CACHE: number;
MAX_ORDERS_TO_IMPORT: number;
PORT: number; PORT: number;
RAKUTEN_RAPID_API_KEY: string; RAKUTEN_RAPID_API_KEY: string;
REDIS_HOST: string; REDIS_HOST: string;

View File

@ -78,6 +78,13 @@ const routes: Routes = [
'./pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module' './pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module'
).then((m) => m.FirstMonthsInOpenSourcePageModule) ).then((m) => m.FirstMonthsInOpenSourcePageModule)
}, },
{
path: 'en/blog/2022/07/ghostfolio-meets-internet-identity',
loadChildren: () =>
import(
'./pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.module'
).then((m) => m.GhostfolioMeetsInternetIdentityPageModule)
},
{ {
path: 'en/blog/2022/07/how-do-i-get-my-finances-in-order', path: 'en/blog/2022/07/how-do-i-get-my-finances-in-order',
loadChildren: () => loadChildren: () =>

View File

@ -29,11 +29,10 @@
<td class="mat-cell px-1 py-2"> <td class="mat-cell px-1 py-2">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<span class="d-none d-sm-inline-block" <span class="d-none d-sm-inline-block"
>{{ userItem.alias || userItem.id }}</span >{{ userItem.id }}</span
> >
<span class="d-inline-block d-sm-none" <span class="d-inline-block d-sm-none"
>{{ userItem.alias || (userItem.id | slice:0:5) + >{{ (userItem.id | slice:0:5) + '...' }}</span
'...' }}</span
> >
<gf-premium-indicator <gf-premium-indicator
*ngIf="userItem?.subscription?.type === 'Premium'" *ngIf="userItem?.subscription?.type === 'Premium'"

View File

@ -124,13 +124,11 @@
: 'radio-button-on-outline' : 'radio-button-on-outline'
" "
></ion-icon> ></ion-icon>
<span *ngIf="user?.alias">{{ user.alias }}</span> <span i18n>Me</span>
<span *ngIf="!user?.alias" i18n><span></span>Me</span>
</button> </button>
<button <button
*ngFor="let accessItem of user?.access" *ngFor="let accessItem of user?.access"
class="align-items-center d-flex" class="align-items-center d-flex"
disabled="false"
mat-menu-item mat-menu-item
(click)="impersonateAccount(accessItem.id)" (click)="impersonateAccount(accessItem.id)"
> >

View File

@ -2,26 +2,29 @@
class="align-items-center container d-flex flex-column h-100 justify-content-center overview p-0 position-relative" class="align-items-center container d-flex flex-column h-100 justify-content-center overview p-0 position-relative"
> >
<div class="row w-100"> <div class="row w-100">
<div class="chart-container col"> <div class="col p-0">
<gf-line-chart <div class="chart-container mx-auto position-relative">
class="position-absolute" <div
symbol="Performance" *ngIf="hasPermissionToCreateOrder && historicalDataItems?.length === 0"
[currency]="user?.settings?.baseCurrency" class="align-items-center d-flex h-100 justify-content-center w-100"
[historicalDataItems]="historicalDataItems" >
[locale]="user?.settings?.locale" <div class="d-flex justify-content-center">
[ngClass]="{ 'pr-3': deviceType === 'mobile' }" <gf-no-transactions-info-indicator></gf-no-transactions-info-indicator>
[showGradient]="true" </div>
[showLoader]="false"
[showXAxis]="false"
[showYAxis]="false"
></gf-line-chart>
<div
*ngIf="hasPermissionToCreateOrder && historicalDataItems?.length === 0"
class="align-items-center d-flex h-100 justify-content-center w-100"
>
<div class="d-flex justify-content-center">
<gf-no-transactions-info-indicator></gf-no-transactions-info-indicator>
</div> </div>
<gf-line-chart
class="position-absolute"
symbol="Performance"
[currency]="user?.settings?.baseCurrency"
[historicalDataItems]="historicalDataItems"
[hidden]="historicalDataItems?.length === 0"
[locale]="user?.settings?.locale"
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
[showGradient]="true"
[showLoader]="false"
[showXAxis]="false"
[showYAxis]="false"
></gf-line-chart>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,7 +5,8 @@
.chart-container { .chart-container {
aspect-ratio: 16 / 9; aspect-ratio: 16 / 9;
max-height: 50vh; height: auto;
max-width: 67rem;
// Fallback for aspect-ratio (using padding hack) // Fallback for aspect-ratio (using padding hack)
@supports not (aspect-ratio: 16 / 9) { @supports not (aspect-ratio: 16 / 9) {

View File

@ -1,3 +1,7 @@
:host { :host {
display: block; display: block;
ngx-skeleton-loader {
height: 100%;
}
} }

View File

@ -1,10 +1,13 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox'; import { MatCheckboxChange } from '@angular/material/checkbox';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service';
import { import {
STAY_SIGNED_IN, STAY_SIGNED_IN,
SettingsStorageService SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service'; } from '@ghostfolio/client/services/settings-storage.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
@Component({ @Component({
selector: 'gf-login-with-access-token-dialog', selector: 'gf-login-with-access-token-dialog',
@ -16,7 +19,10 @@ export class LoginWithAccessTokenDialog {
public constructor( public constructor(
@Inject(MAT_DIALOG_DATA) public data: any, @Inject(MAT_DIALOG_DATA) public data: any,
public dialogRef: MatDialogRef<LoginWithAccessTokenDialog>, public dialogRef: MatDialogRef<LoginWithAccessTokenDialog>,
private settingsStorageService: SettingsStorageService private internetIdentityService: InternetIdentityService,
private router: Router,
private settingsStorageService: SettingsStorageService,
private tokenStorageService: TokenStorageService
) {} ) {}
ngOnInit() {} ngOnInit() {}
@ -31,4 +37,14 @@ export class LoginWithAccessTokenDialog {
public onClose() { public onClose() {
this.dialogRef.close(); this.dialogRef.close();
} }
public async onLoginWithInternetIdentity() {
try {
const { authToken } = await this.internetIdentityService.login();
this.tokenStorageService.saveToken(authToken);
this.dialogRef.close();
this.router.navigate(['/']);
} catch {}
}
} }

View File

@ -5,16 +5,7 @@
></gf-dialog-header> ></gf-dialog-header>
<div mat-dialog-content> <div mat-dialog-content>
<div> <div class="align-items-center d-flex flex-column">
<ng-container *ngIf="data.hasPermissionToUseSocialLogin">
<div class="text-center">
<a color="accent" href="/api/v1/auth/google" mat-flat-button
><ion-icon class="mr-1" name="logo-google"></ion-icon
><span i18n>Sign in with Google</span></a
>
</div>
<div class="my-3 text-center text-muted" i18n>or</div>
</ng-container>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Security Token</mat-label> <mat-label i18n>Security Token</mat-label>
<textarea <textarea
@ -24,6 +15,29 @@
[(ngModel)]="data.accessToken" [(ngModel)]="data.accessToken"
></textarea> ></textarea>
</mat-form-field> </mat-form-field>
<ng-container *ngIf="data.hasPermissionToUseSocialLogin">
<div class="my-3 text-center text-muted" i18n>or</div>
<div class="d-flex flex-column">
<button
class="mb-2"
mat-stroked-button
(click)="onLoginWithInternetIdentity()"
>
<img
class="mr-2"
src="./assets/icons/internet-computer.svg"
style="height: 0.75rem"
/><span i18n>Sign in with Internet Identity</span>
</button>
<a href="/api/v1/auth/google" mat-stroked-button
><img
class="mr-2"
src="./assets/icons/google.svg"
style="height: 1rem"
/><span i18n>Sign in with Google</span></a
>
</div>
</ng-container>
</div> </div>
</div> </div>
<div mat-dialog-actions> <div mat-dialog-actions>

View File

@ -12,4 +12,12 @@
} }
} }
} }
.mat-form-field {
::ng-deep {
.mat-form-field-wrapper {
padding-bottom: 0;
}
}
}
} }

View File

@ -142,4 +142,15 @@
</button> </button>
</div> </div>
<div
*ngIf="
dataSource.data.length === 0 && hasPermissionToCreateActivity && !isLoading
"
class="p-3 text-center"
>
<gf-no-transactions-info-indicator
[hasBorder]="false"
></gf-no-transactions-info-indicator>
</div>
<mat-paginator class="d-none" [pageSize]="pageSize"></mat-paginator> <mat-paginator class="d-none" [pageSize]="pageSize"></mat-paginator>

View File

@ -27,6 +27,7 @@ import { Subject, Subscription } from 'rxjs';
export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit { export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() deviceType: string; @Input() deviceType: string;
@Input() hasPermissionToCreateActivity: boolean;
@Input() hasPermissionToShowValues = true; @Input() hasPermissionToShowValues = true;
@Input() locale: string; @Input() locale: string;
@Input() pageSize = Number.MAX_SAFE_INTEGER; @Input() pageSize = Number.MAX_SAFE_INTEGER;

View File

@ -8,10 +8,6 @@
<div class="col"> <div class="col">
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-content> <mat-card-content>
<div *ngIf="user.alias" class="d-flex py-1">
<div class="pr-1 w-50" i18n>Alias</div>
<div class="pl-1 w-50">{{ user.alias }}</div>
</div>
<div <div
*ngIf="hasPermissionToUpdateUserSettings && user?.subscription" *ngIf="hasPermissionToUpdateUserSettings && user?.subscription"
class="d-flex py-1" class="d-flex py-1"

View File

@ -4,7 +4,7 @@
<article> <article>
<div class="mb-4 text-center"> <div class="mb-4 text-center">
<h1 class="mb-1">Hallo Ghostfolio 👋</h1> <h1 class="mb-1">Hallo Ghostfolio 👋</h1>
<div class="text-muted"><small>31.07.2021</small></div> <div class="text-muted"><small>2021-07-31</small></div>
</div> </div>
<section class="mb-4"> <section class="mb-4">
<p> <p>

View File

@ -4,7 +4,7 @@
<article> <article>
<div class="mb-4 text-center"> <div class="mb-4 text-center">
<h1 class="mb-1">Hello Ghostfolio 👋</h1> <h1 class="mb-1">Hello Ghostfolio 👋</h1>
<div class="text-muted"><small>31.07.2021</small></div> <div class="text-muted"><small>2021-07-31</small></div>
</div> </div>
<section class="mb-4"> <section class="mb-4">
<p> <p>

View File

@ -7,7 +7,7 @@
👻 Ghostfolio 👻 Ghostfolio
<span class="text-nowrap">First months in Open Source</span> <span class="text-nowrap">First months in Open Source</span>
</h1> </h1>
<div class="text-muted"><small>05.01.2022</small></div> <div class="text-muted"><small>2022-01-05</small></div>
</div> </div>
<section class="mb-4"> <section class="mb-4">
<p> <p>

View File

@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { GhostfolioMeetsInternetIdentityPageComponent } from './ghostfolio-meets-internet-identity-page.component';
const routes: Routes = [
{
path: '',
component: GhostfolioMeetsInternetIdentityPageComponent,
canActivate: [AuthGuard]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class GhostfolioMeetsInternetIdentityRoutingModule {}

View File

@ -0,0 +1,9 @@
import { Component } from '@angular/core';
@Component({
host: { class: 'page' },
selector: 'gf-ghostfolio-meets-internet-identity-page',
styleUrls: ['./ghostfolio-meets-internet-identity-page.scss'],
templateUrl: './ghostfolio-meets-internet-identity-page.html'
})
export class GhostfolioMeetsInternetIdentityPageComponent {}

View File

@ -0,0 +1,183 @@
<div class="blog container">
<div class="row">
<div class="col-md-8 offset-md-2">
<article>
<div class="mb-4 text-center">
<h1 class="mb-1">Ghostfolio meets Internet Identity</h1>
<div class="mb-3 text-muted"><small>2022-07-23</small></div>
<img
alt="Ghostfolio meets Internet Identity Teaser"
class="w-100"
src="./assets/images/blog/ghostfolio-meets-internet-identity.png"
title="Ghostfolio meets Internet Identity"
/>
</div>
<section class="mb-4">
<p>
<a href="https://ghostfol.io">Ghostfolio</a>, the web-based personal
finance management software, supports passwordless authentication as
of now thanks to the integration of
<a href="https://identity.ic0.app">Internet Identity</a>. This
blockchain authentication system enables you to sign in securely and
anonymously to Ghostfolio. With this latest update, Ghostfolio is
ready for Web3.
</p>
<div class="container my-4">
<div class="row">
<div class="col-md-10 offset-md-1">
<blockquote class="blockquote m-0">
<p class="mb-0">Track your portfolio without being tracked</p>
</blockquote>
</div>
</div>
</div>
<p>
To avoid the security issues that arise with password authentication
on the traditional web, the
<a href="https://internetcomputer.org">Internet Computer</a>
blockchain by <a href="https://dfinity.org">dfinity</a> has
introduced a new cryptographic authentication system. It is called
<i>Internet Identity</i> and is as convenient to use as Web2
<a href="https://en.wikipedia.org/wiki/OAuth">OAuth</a> ("Open
Authorization") providers like <i>Google Sign-In</i> or
<i>Facebook Login</i>.
</p>
</section>
<section class="mb-4">
<h2 class="h4">How to use Internet Identity?</h2>
<p>
<i>Internet Identity</i> is based on the
<a href="https://en.wikipedia.org/wiki/WebAuthn"
>WebAuthn protocol</a
>
and uses secure cryptographic authentication. It provides three
options to authenticate yourself:
</p>
<ul>
<li>
The built-in biometric authentication methods of your smartphone
or laptop (fingerprint sensor, <i>Face ID</i>, <i>Touch ID</i>)
</li>
<li>The password or pin to unlock your computer or mobile phone</li>
<li>A security key plugged into the USB port of your computer</li>
</ul>
<p>
When you authenticate with <i>Internet Identity</i>, the service
only gets a dedicated pseudonym rather than sensitive user data like
the email address or phone number. This preserves your anonymity and
prevents you being tracked on the Internet.
</p>
</section>
<section class="mb-4">
<h2 class="h4">The key benefits in a nutshell</h2>
<ul>
<li>
Authenticate yourself securely without the need of an email
address, username, or a password: all you need is your device to
log in.
</li>
<li>
Built-in recovery mechanisms to ensure you are not locked out of
any service that requires the <i>Internet Identity</i>.
</li>
<li>
Log in to various Internet services without being tracked by big
tech companies.
</li>
</ul>
</section>
<section class="mb-4">
<p>
If you would like to provide feedback or get involved in further
development of Ghostfolio, please get in touch by email via
<a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a> or on Twitter
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a>.
</p>
<p>
I look forward to hearing from you.<br />
Thomas from Ghostfolio
</p>
</section>
<section class="mb-4">
<ul class="list-inline">
<li class="list-inline-item">
<span class="badge badge-light">Anonymity</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">App</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Auth Provider</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Authentication</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Blockchain</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Cryptography</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">dfinity</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Face ID</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Fingerprint</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Ghostfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Internet Computer</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Internet Identity</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">OAuth</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Open Source</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">OSS</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Password</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">passwordless</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Portfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Security</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Software</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Technology</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Touch ID</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Wealth Management</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Web3</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">WebAuthn</span>
</li>
</ul>
</section>
</article>
</div>
</div>
</div>

View File

@ -0,0 +1,17 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { GhostfolioMeetsInternetIdentityRoutingModule } from './ghostfolio-meets-internet-identity-page-routing.module';
import { GhostfolioMeetsInternetIdentityPageComponent } from './ghostfolio-meets-internet-identity-page.component';
@NgModule({
declarations: [GhostfolioMeetsInternetIdentityPageComponent],
imports: [
CommonModule,
GhostfolioMeetsInternetIdentityRoutingModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GhostfolioMeetsInternetIdentityPageModule {}

View File

@ -4,14 +4,14 @@
<article> <article>
<div class="mb-4 text-center"> <div class="mb-4 text-center">
<h1 class="mb-1">How do I get my finances in order?</h1> <h1 class="mb-1">How do I get my finances in order?</h1>
<div class="text-muted"><small>14.07.2022</small></div> <div class="text-muted"><small>2022-07-14</small></div>
</div> </div>
<section class="mb-4"> <section class="mb-4">
<p> <p>
Before you can think of Before you can think of
<a [routerLink]="['/resources']">long-term investing</a>, you need <a [routerLink]="['/resources']">long-term investing</a>, you have
to have your finances in order. Take a look at Peter's journey to to get your finances in order. Take a look at Peter's journey to see
see how you can achieve it, too. how you can achieve it, too.
</p> </p>
<p> <p>
Peter enjoys life, but sometimes he overspends a bit. He realizes it Peter enjoys life, but sometimes he overspends a bit. He realizes it
@ -64,8 +64,8 @@
If Peter has spent less money than planned on eating out at If Peter has spent less money than planned on eating out at
restaurants, he can set aside the remaining amount. This way, he can restaurants, he can set aside the remaining amount. This way, he can
treat himself to something special every now and then. From now on, treat himself to something special every now and then. From now on,
he saves a fixed amount of money in a separate account ("pay he saves a fixed amount of money in a separate account (pay
yourself first") by standing order at the beginning of the month. As yourself first) by standing order at the beginning of the month. As
soon as there are three net monthly salaries in the account, he soon as there are three net monthly salaries in the account, he
invests the monthly savings amount in a passively managed global invests the monthly savings amount in a passively managed global
equity fund. This grows his assets over the years and allows him to equity fund. This grows his assets over the years and allows him to
@ -153,6 +153,9 @@
<li class="list-inline-item"> <li class="list-inline-item">
<span class="badge badge-light">Goal</span> <span class="badge badge-light">Goal</span>
</li> </li>
<li class="list-inline-item">
<span class="badge badge-light">Guide</span>
</li>
<li class="list-inline-item"> <li class="list-inline-item">
<span class="badge badge-light">Income</span> <span class="badge badge-light">Income</span>
</li> </li>

View File

@ -2,6 +2,32 @@
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col"> <div class="col">
<h3 class="mb-3 text-center" i18n>Blog</h3> <h3 class="mb-3 text-center" i18n>Blog</h3>
<mat-card class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"
[routerLink]="['/en', 'blog', '2022', '07', 'ghostfolio-meets-internet-identity']"
>
<div class="flex-grow-1">
<div class="h6 m-0 text-truncate">
Ghostfolio meets Internet Identity
</div>
<div class="d-flex text-muted">2022-07-23</div>
</div>
<div class="align-items-center d-flex">
<ion-icon
class="chevron text-muted"
name="chevron-forward-outline"
size="small"
></ion-icon>
</div>
</a>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-content> <mat-card-content>
<div class="container p-0"> <div class="container p-0">
@ -14,7 +40,7 @@
<div class="h6 m-0 text-truncate"> <div class="h6 m-0 text-truncate">
How do I get my finances in order? How do I get my finances in order?
</div> </div>
<div class="d-flex text-muted">14.07.2022</div> <div class="d-flex text-muted">2022-07-14</div>
</div> </div>
<div class="align-items-center d-flex"> <div class="align-items-center d-flex">
<ion-icon <ion-icon
@ -40,7 +66,7 @@
<div class="h6 m-0 text-truncate"> <div class="h6 m-0 text-truncate">
First months in Open Source First months in Open Source
</div> </div>
<div class="d-flex text-muted">05.01.2022</div> <div class="d-flex text-muted">2022-01-05</div>
</div> </div>
<div class="align-items-center d-flex"> <div class="align-items-center d-flex">
<ion-icon <ion-icon
@ -64,7 +90,7 @@
> >
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="h6 m-0 text-truncate">Hello Ghostfolio</div> <div class="h6 m-0 text-truncate">Hello Ghostfolio</div>
<div class="d-flex text-muted">31.07.2021</div> <div class="d-flex text-muted">2021-07-31</div>
</div> </div>
<div class="align-items-center d-flex"> <div class="align-items-center d-flex">
<ion-icon <ion-icon
@ -88,7 +114,7 @@
> >
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="h6 m-0 text-truncate">Hallo Ghostfolio</div> <div class="h6 m-0 text-truncate">Hallo Ghostfolio</div>
<div class="d-flex text-muted">31.07.2021</div> <div class="d-flex text-muted">2021-07-31</div>
</div> </div>
<div class="align-items-center d-flex"> <div class="align-items-center d-flex">
<ion-icon <ion-icon

View File

@ -1,11 +1,17 @@
<div class="container"> <div class="container">
<div class="investment-chart row"> <div class="row">
<div class="col-lg"> <div class="col-lg">
<h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3> <h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3>
<div class="mb-4"> <div class="mb-4">
<div class="align-items-center d-flex mb-4"> <div class="align-items-center d-flex mb-4">
<div class="flex-grow-1 h5 mb-0 text-truncate" i18n> <div
Investment Timeline class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate"
>
<span i18n>Investment Timeline</span>
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
></gf-premium-indicator>
</div> </div>
<gf-toggle <gf-toggle
class="d-none d-lg-block" class="d-none d-lg-block"
@ -15,25 +21,27 @@
(change)="onChangeGroupBy($event.value)" (change)="onChangeGroupBy($event.value)"
></gf-toggle> ></gf-toggle>
</div> </div>
<gf-investment-chart <div class="chart-container">
class="h-100" <gf-investment-chart
[currency]="user?.settings?.baseCurrency" class="h-100"
[daysInMarket]="daysInMarket" [currency]="user?.settings?.baseCurrency"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [daysInMarket]="daysInMarket"
[investments]="investments" [isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[locale]="user?.settings?.locale" [investments]="investments"
[ngClass]="{ 'd-none': mode }" [locale]="user?.settings?.locale"
></gf-investment-chart> [ngClass]="{ 'd-none': mode }"
<gf-investment-chart ></gf-investment-chart>
class="h-100" <gf-investment-chart
groupBy="month" class="h-100"
[currency]="user?.settings?.baseCurrency" groupBy="month"
[daysInMarket]="daysInMarket" [currency]="user?.settings?.baseCurrency"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [daysInMarket]="daysInMarket"
[investments]="investmentsByMonth" [isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[locale]="user?.settings?.locale" [investments]="investmentsByMonth"
[ngClass]="{ 'd-none': !mode }" [locale]="user?.settings?.locale"
></gf-investment-chart> [ngClass]="{ 'd-none': !mode }"
></gf-investment-chart>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,6 +3,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module'; import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module'; import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -15,6 +16,7 @@ import { AnalysisPageComponent } from './analysis-page.component';
AnalysisPageRoutingModule, AnalysisPageRoutingModule,
CommonModule, CommonModule,
GfInvestmentChartModule, GfInvestmentChartModule,
GfPremiumIndicatorModule,
GfToggleModule, GfToggleModule,
GfValueModule, GfValueModule,
MatCardModule, MatCardModule,

View File

@ -1,11 +1,7 @@
:host { :host {
display: block; display: block;
.investment-chart { .chart-container {
.mat-card { aspect-ratio: 16 / 9;
.mat-card-content {
aspect-ratio: 16 / 9;
}
}
} }
} }

View File

@ -15,10 +15,14 @@
<gf-positions-table <gf-positions-table
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType" [deviceType]="deviceType"
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[positions]="positionsArray" [positions]="positionsArray"
></gf-positions-table> ></gf-positions-table>
<div *ngIf="hasPermissionToCreateOrder" class="text-center"> <div
*ngIf="hasPermissionToCreateOrder && positionsArray?.length > 0"
class="text-center"
>
<a <a
class="mt-3" class="mt-3"
i18n i18n

View File

@ -255,6 +255,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
isUUID(this.activityForm.controls['searchSymbol'].value.symbol) isUUID(this.activityForm.controls['searchSymbol'].value.symbol)
? this.activityForm.controls['name'].value ? this.activityForm.controls['name'].value
: this.activityForm.controls['searchSymbol'].value.symbol, : this.activityForm.controls['searchSymbol'].value.symbol,
tags: this.activityForm.controls['tags'].value,
type: this.activityForm.controls['type'].value, type: this.activityForm.controls['type'].value,
unitPrice: this.activityForm.controls['unitPrice'].value unitPrice: this.activityForm.controls['unitPrice'].value
}; };

View File

@ -1,6 +1,6 @@
<gf-dialog-header <gf-dialog-header
mat-dialog-title mat-dialog-title
title="Import Transactions Error" title="Import Activities Error"
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
(closeButtonClicked)="onCancel()" (closeButtonClicked)="onCancel()"
></gf-dialog-header> ></gf-dialog-header>

View File

@ -2,6 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { InfoItem } from '@ghostfolio/common/interfaces'; import { InfoItem } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -34,6 +35,7 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
private dataService: DataService, private dataService: DataService,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private internetIdentityService: InternetIdentityService,
private router: Router, private router: Router,
private tokenStorageService: TokenStorageService private tokenStorageService: TokenStorageService
) { ) {
@ -62,6 +64,14 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
}); });
} }
public async onLoginWithInternetIdentity() {
try {
const { authToken } = await this.internetIdentityService.login();
this.tokenStorageService.saveToken(authToken);
this.router.navigate(['/']);
} catch {}
}
public openShowAccessTokenDialog( public openShowAccessTokenDialog(
accessToken: string, accessToken: string,
authToken: string, authToken: string,

View File

@ -28,16 +28,25 @@
Create Account Create Account
</button> </button>
<ng-container *ngIf="hasPermissionForSocialLogin"> <ng-container *ngIf="hasPermissionForSocialLogin">
<div <div class="my-3 text-muted" i18n>or</div>
class="m-3 text-muted" <button
i18n class="d-block mb-2"
[ngClass]="{'d-inline': deviceType !== 'mobile' }" mat-stroked-button
(click)="onLoginWithInternetIdentity()"
> >
or <img
</div> class="mr-2"
<a color="accent" href="/api/v1/auth/google" mat-flat-button src="./assets/icons/internet-computer.svg"
><ion-icon class="mr-1" name="logo-google"></ion-icon style="height: 0.75rem"
><span i18n>Continue with Google</span></a />
<span i18n>Continue with Internet Identity</span>
</button>
<a class="d-block" href="/api/v1/auth/google" mat-stroked-button
><img
class="mr-2"
src="./assets/icons/google.svg"
style="height: 1rem"
/><span i18n>Continue with Google</span></a
> >
</ng-container> </ng-container>
</div> </div>

View File

@ -20,6 +20,22 @@
</div> </div>
</div> </div>
</div> </div>
<div class="mb-4 media">
<div class="media-body">
<h3 class="h5 mt-0">How do I get my finances in order?</h3>
<div class="mb-1">
Before you can think of long-term investing, you have to get your
finances in order. Learn how you can reach your financial goals
easier and faster in this guide.
</div>
<div>
<a
[routerLink]="['/en', 'blog', '2022', '07', 'how-do-i-get-my-finances-in-order']"
>How do I get my finances in order? →</a
>
</div>
</div>
</div>
</div> </div>
<h2 class="h4 mb-3">Market</h2> <h2 class="h4 mb-3">Market</h2>
<div class="mb-5"> <div class="mb-5">

View File

@ -23,6 +23,7 @@ import {
Export, Export,
Filter, Filter,
InfoItem, InfoItem,
OAuthResponse,
PortfolioChart, PortfolioChart,
PortfolioDetails, PortfolioDetails,
PortfolioInvestments, PortfolioInvestments,
@ -368,7 +369,9 @@ export class DataService {
} }
public loginAnonymous(accessToken: string) { public loginAnonymous(accessToken: string) {
return this.http.get<any>(`/api/v1/auth/anonymous/${accessToken}`); return this.http.get<OAuthResponse>(
`/api/v1/auth/anonymous/${accessToken}`
);
} }
public postAccess(aAccess: CreateAccessDto) { public postAccess(aAccess: CreateAccessDto) {

View File

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

View File

@ -0,0 +1 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 128 128" id="Social_Icons" version="1.1" viewBox="0 0 128 128" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="_x31__stroke"><g id="Google"><rect clip-rule="evenodd" fill="none" fill-rule="evenodd" height="128" width="128"/><path clip-rule="evenodd" d="M27.585,64c0-4.157,0.69-8.143,1.923-11.881L7.938,35.648 C3.734,44.183,1.366,53.801,1.366,64c0,10.191,2.366,19.802,6.563,28.332l21.558-16.503C28.266,72.108,27.585,68.137,27.585,64" fill="#FBBC05" fill-rule="evenodd"/><path clip-rule="evenodd" d="M65.457,26.182c9.031,0,17.188,3.2,23.597,8.436L107.698,16 C96.337,6.109,81.771,0,65.457,0C40.129,0,18.361,14.484,7.938,35.648l21.569,16.471C34.477,37.033,48.644,26.182,65.457,26.182" fill="#EA4335" fill-rule="evenodd"/><path clip-rule="evenodd" d="M65.457,101.818c-16.812,0-30.979-10.851-35.949-25.937 L7.938,92.349C18.361,113.516,40.129,128,65.457,128c15.632,0,30.557-5.551,41.758-15.951L86.741,96.221 C80.964,99.86,73.689,101.818,65.457,101.818" fill="#34A853" fill-rule="evenodd"/><path clip-rule="evenodd" d="M126.634,64c0-3.782-0.583-7.855-1.457-11.636H65.457v24.727 h34.376c-1.719,8.431-6.397,14.912-13.092,19.13l20.474,15.828C118.981,101.129,126.634,84.861,126.634,64" fill="#4285F4" fill-rule="evenodd"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -6,58 +6,62 @@
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"> http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url> <url>
<loc>https://ghostfol.io</loc> <loc>https://ghostfol.io</loc>
<lastmod>2022-07-14T00:00:00+00:00</lastmod> <lastmod>2022-07-23T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/about</loc> <loc>https://ghostfol.io/about</loc>
<lastmod>2022-07-14T00:00:00+00:00</lastmod> <lastmod>2022-07-23T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/about/changelog</loc> <loc>https://ghostfol.io/about/changelog</loc>
<lastmod>2022-07-14T00:00:00+00:00</lastmod> <lastmod>2022-07-23T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/blog</loc> <loc>https://ghostfol.io/blog</loc>
<lastmod>2022-07-14T00:00:00+00:00</lastmod> <lastmod>2022-07-23T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc> <loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
<lastmod>2022-07-14T00:00:00+00:00</lastmod> <lastmod>2022-07-23T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/demo</loc> <loc>https://ghostfol.io/demo</loc>
<lastmod>2022-07-14T00:00:00+00:00</lastmod> <lastmod>2022-07-23T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc> <loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
<lastmod>2022-07-14T00:00:00+00:00</lastmod> <lastmod>2022-07-23T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc> <loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
<lastmod>2022-07-14T00:00:00+00:00</lastmod> <lastmod>2022-07-23T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/07/ghostfolio-meets-internet-identity</loc>
<lastmod>2022-07-23T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc> <loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc>
<lastmod>2022-07-14T00:00:00+00:00</lastmod> <lastmod>2022-07-23T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/features</loc> <loc>https://ghostfol.io/features</loc>
<lastmod>2022-07-14T00:00:00+00:00</lastmod> <lastmod>2022-07-23T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/markets</loc> <loc>https://ghostfol.io/markets</loc>
<lastmod>2022-07-14T00:00:00+00:00</lastmod> <lastmod>2022-07-23T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/pricing</loc> <loc>https://ghostfol.io/pricing</loc>
<lastmod>2022-07-14T00:00:00+00:00</lastmod> <lastmod>2022-07-23T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/register</loc> <loc>https://ghostfol.io/register</loc>
<lastmod>2022-07-14T00:00:00+00:00</lastmod> <lastmod>2022-07-23T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/resources</loc> <loc>https://ghostfol.io/resources</loc>
<lastmod>2022-07-14T00:00:00+00:00</lastmod> <lastmod>2022-07-23T00:00:00+00:00</lastmod>
</url> </url>
</urlset> </urlset>

View File

@ -5,7 +5,6 @@ export interface AdminData {
userCount: number; userCount: number;
users: { users: {
accountCount: number; accountCount: number;
alias: string;
createdAt: Date; createdAt: Date;
engagement: number; engagement: number;
id: string; id: string;

View File

@ -29,6 +29,7 @@ import { PortfolioSummary } from './portfolio-summary.interface';
import { Position } from './position.interface'; import { Position } from './position.interface';
import { BenchmarkResponse } from './responses/benchmark-response.interface'; import { BenchmarkResponse } from './responses/benchmark-response.interface';
import { ResponseError } from './responses/errors.interface'; import { ResponseError } from './responses/errors.interface';
import { OAuthResponse } from './responses/oauth-response.interface';
import { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface'; import { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface';
import { ScraperConfiguration } from './scraper-configuration.interface'; import { ScraperConfiguration } from './scraper-configuration.interface';
import { TimelinePosition } from './timeline-position.interface'; import { TimelinePosition } from './timeline-position.interface';
@ -54,6 +55,7 @@ export {
FilterGroup, FilterGroup,
HistoricalDataItem, HistoricalDataItem,
InfoItem, InfoItem,
OAuthResponse,
PortfolioChart, PortfolioChart,
PortfolioDetails, PortfolioDetails,
PortfolioInvestments, PortfolioInvestments,

View File

@ -0,0 +1,3 @@
export interface OAuthResponse {
authToken: string;
}

View File

@ -8,7 +8,6 @@ export interface User {
id: string; id: string;
}[]; }[];
accounts: Account[]; accounts: Account[];
alias?: string;
id: string; id: string;
permissions: string[]; permissions: string[];
settings: UserSettings; settings: UserSettings;

View File

@ -8,6 +8,5 @@
></ngx-skeleton-loader> ></ngx-skeleton-loader>
<canvas <canvas
#chartCanvas #chartCanvas
class="h-100"
[ngStyle]="{ display: isLoading ? 'none' : 'block' }" [ngStyle]="{ display: isLoading ? 'none' : 'block' }"
></canvas> ></canvas>

View File

@ -175,6 +175,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
data, data,
options: { options: {
animation: false, animation: false,
aspectRatio: 16 / 9,
elements: { elements: {
point: { point: {
hoverBackgroundColor: getBackgroundColor(), hoverBackgroundColor: getBackgroundColor(),

View File

@ -1,5 +1,6 @@
<a <a
class="align-items-center d-flex" class="align-items-center d-flex"
title="Upgrade to Ghostfolio Premium"
[ngStyle]="{ 'pointer-events': enableLink ? 'initial' : 'none' }" [ngStyle]="{ 'pointer-events': enableLink ? 'initial' : 'none' }"
[routerLink]="['/pricing']" [routerLink]="['/pricing']"
><ion-icon class="text-muted" name="diamond-outline"></ion-icon ><ion-icon class="text-muted" name="diamond-outline"></ion-icon

View File

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "1.169.0", "version": "1.173.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
@ -20,6 +20,7 @@
"database:format-schema": "prisma format", "database:format-schema": "prisma format",
"database:generate-typings": "prisma generate", "database:generate-typings": "prisma generate",
"database:gui": "prisma studio", "database:gui": "prisma studio",
"database:gui:prod": "npx dotenv-cli -e .env.prod -- prisma studio",
"database:migrate": "prisma migrate deploy", "database:migrate": "prisma migrate deploy",
"database:push": "prisma db push", "database:push": "prisma db push",
"database:seed": "prisma db seed", "database:seed": "prisma db seed",
@ -42,7 +43,7 @@
"start:server": "nx serve api --watch", "start:server": "nx serve api --watch",
"start:storybook": "nx run ui:storybook", "start:storybook": "nx run ui:storybook",
"test": "nx test", "test": "nx test",
"test:single": "nx test --test-file portfolio-calculator-new.spec.ts", "test:single": "nx test --test-file portfolio-calculator-novn-buy-and-sell-partially.spec.ts",
"ts-node": "ts-node", "ts-node": "ts-node",
"update": "nx migrate latest", "update": "nx migrate latest",
"watch:server": "nx build api --watch", "watch:server": "nx build api --watch",
@ -61,6 +62,12 @@
"@angular/platform-browser-dynamic": "14.0.2", "@angular/platform-browser-dynamic": "14.0.2",
"@angular/router": "14.0.2", "@angular/router": "14.0.2",
"@codewithdan/observable-store": "2.2.11", "@codewithdan/observable-store": "2.2.11",
"@dfinity/agent": "0.12.1",
"@dfinity/auth-client": "0.12.1",
"@dfinity/authentication": "0.12.1",
"@dfinity/candid": "0.12.1",
"@dfinity/identity": "0.12.1",
"@dfinity/principal": "0.12.1",
"@dinero.js/currencies": "2.0.0-alpha.8", "@dinero.js/currencies": "2.0.0-alpha.8",
"@nestjs/bull": "0.5.5", "@nestjs/bull": "0.5.5",
"@nestjs/common": "8.4.7", "@nestjs/common": "8.4.7",
@ -171,9 +178,9 @@
"prettier": "2.7.1", "prettier": "2.7.1",
"replace-in-file": "6.2.0", "replace-in-file": "6.2.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"tslib": "2.0.0",
"ts-jest": "27.1.4", "ts-jest": "27.1.4",
"ts-node": "10.8.1", "ts-node": "10.8.1",
"tslib": "2.0.0",
"typescript": "4.7.3" "typescript": "4.7.3"
}, },
"engines": { "engines": {

View File

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "Provider" ADD VALUE 'INTERNET_IDENTITY';

View File

@ -217,6 +217,7 @@ enum ViewMode {
enum Provider { enum Provider {
ANONYMOUS ANONYMOUS
GOOGLE GOOGLE
INTERNET_IDENTITY
} }
enum Role { enum Role {

View File

@ -0,0 +1,28 @@
{
"meta": {
"date": "2022-07-21T21:28:05.857Z",
"version": "dev"
},
"activities": [
{
"fee": 0,
"quantity": 1,
"type": "SELL",
"unitPrice": 85.73,
"currency": "CHF",
"dataSource": "YAHOO",
"date": "2022-04-07T22:00:00.000Z",
"symbol": "NOVN.SW"
},
{
"fee": 0,
"quantity": 2,
"type": "BUY",
"unitPrice": 75.8,
"currency": "CHF",
"dataSource": "YAHOO",
"date": "2022-03-06T23:00:00.000Z",
"symbol": "NOVN.SW"
}
]
}

118
yarn.lock
View File

@ -1769,6 +1769,48 @@
debug "^3.1.0" debug "^3.1.0"
lodash.once "^4.1.1" lodash.once "^4.1.1"
"@dfinity/agent@0.12.1":
version "0.12.1"
resolved "https://registry.yarnpkg.com/@dfinity/agent/-/agent-0.12.1.tgz#c3acd6419712aca77bf94f76057f100caeb69be4"
integrity sha512-/KSKh248k4pjzvqCzIgYNNi3pTv+DBZ40+QiTBQeFzp6VEg3gfSv5bK2UwC0Plq9xwk7TeeeGLiTv6DI3RjCOQ==
dependencies:
base64-arraybuffer "^0.2.0"
bignumber.js "^9.0.0"
borc "^2.1.1"
js-sha256 "0.9.0"
simple-cbor "^0.4.1"
"@dfinity/auth-client@0.12.1":
version "0.12.1"
resolved "https://registry.yarnpkg.com/@dfinity/auth-client/-/auth-client-0.12.1.tgz#8043aeafe8ba8a000954f94de25a76a2565acafc"
integrity sha512-iZKSVjk9K+35jp+AY3QfGAv0jBfn5LZTwpSXgBKVqZCez3GRniGJirJVTvk7t9yOj4BXN8tuvjIKxTsezPpgLQ==
"@dfinity/authentication@0.12.1":
version "0.12.1"
resolved "https://registry.yarnpkg.com/@dfinity/authentication/-/authentication-0.12.1.tgz#19f157e6eb528518da874f3e10d3f7b2b028e5a4"
integrity sha512-krHR48HNqTOp2NwHoKHirTUXHDfHttWZfSmwBCsQa0xwWkrrLSGb3u+9e1oQjDK1G1eK2TP7T1W2duZmmmrZkg==
"@dfinity/candid@0.12.1":
version "0.12.1"
resolved "https://registry.yarnpkg.com/@dfinity/candid/-/candid-0.12.1.tgz#6ba819c56bc3ff55f6f98bdb39470d1d385084b6"
integrity sha512-YX8jfyy/8Qmz4f1mbjqXUqOmtYcGru1gfYWxlRhKFSkeLH0VeZkfPEmD6EQ25k+18ATPk83MQiZnu0b6AWxBUw==
"@dfinity/identity@0.12.1":
version "0.12.1"
resolved "https://registry.yarnpkg.com/@dfinity/identity/-/identity-0.12.1.tgz#f19bf2ed6bfbbb1f4e7c859c039e4da8dee81857"
integrity sha512-FNrjV4/gG9PjQfGLIoH1DycqSAMaoTZCxB+cSVJRFCvGQKc3F3kn5tj6rIv9LV+NNV1f1qfmTXE8rYsMCmEecg==
dependencies:
"@types/webappsec-credential-management" "^0.6.2"
borc "^2.1.1"
js-sha256 "^0.9.0"
secp256k1 "^4.0.2"
tweetnacl "^1.0.1"
"@dfinity/principal@0.12.1":
version "0.12.1"
resolved "https://registry.yarnpkg.com/@dfinity/principal/-/principal-0.12.1.tgz#d4b1f088beded6c1f1b2efbe68a0632e0b4018f7"
integrity sha512-aL5y0mpzRex6LRSc4LUZyhn2GTFfHyxkakkOZxEM7+ecz8HsKKK+mSo78gL1TCso2QkCL4BqZzxnoIxxKqM1cw==
"@dinero.js/currencies@2.0.0-alpha.8": "@dinero.js/currencies@2.0.0-alpha.8":
version "2.0.0-alpha.8" version "2.0.0-alpha.8"
resolved "https://registry.yarnpkg.com/@dinero.js/currencies/-/currencies-2.0.0-alpha.8.tgz#aa02a04ce3685a9b06a7ce12f8c924726386c3fd" resolved "https://registry.yarnpkg.com/@dinero.js/currencies/-/currencies-2.0.0-alpha.8.tgz#aa02a04ce3685a9b06a7ce12f8c924726386c3fd"
@ -4151,6 +4193,11 @@
resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.7.3.tgz#3193c0a3c03a7d1189016c62b4fba4b149ef5e33" resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.7.3.tgz#3193c0a3c03a7d1189016c62b4fba4b149ef5e33"
integrity sha512-DNviAE5OUcZ5s+XEQHRhERLg8fOp8gSgvyJ4aaFASx5wwaObm+PBwTIMXiOFm1QrSee5oYwEAYb7LMzX2O88gA== integrity sha512-DNviAE5OUcZ5s+XEQHRhERLg8fOp8gSgvyJ4aaFASx5wwaObm+PBwTIMXiOFm1QrSee5oYwEAYb7LMzX2O88gA==
"@types/webappsec-credential-management@^0.6.2":
version "0.6.2"
resolved "https://registry.yarnpkg.com/@types/webappsec-credential-management/-/webappsec-credential-management-0.6.2.tgz#93491de1ffcf57f6558c78949cc8e6c5d826b94e"
integrity sha512-/6w8wmKQOFh+1CL99fcFhH7lli1/ExBdAawXsVPXFC5MBOS6ww/4cmK4crpCw51RaG6sTr477N17Y84l0G69IA==
"@types/webpack-env@^1.16.0": "@types/webpack-env@^1.16.0":
version "1.17.0" version "1.17.0"
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.17.0.tgz#f99ce359f1bfd87da90cc4a57cab0a18f34a48d0" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.17.0.tgz#f99ce359f1bfd87da90cc4a57cab0a18f34a48d0"
@ -5484,6 +5531,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base64-arraybuffer@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45"
integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==
base64-js@^1.0.2, base64-js@^1.2.0, base64-js@^1.3.0, base64-js@^1.3.1: base64-js@^1.0.2, base64-js@^1.2.0, base64-js@^1.3.0, base64-js@^1.3.1:
version "1.5.1" version "1.5.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"
@ -5651,6 +5703,19 @@ bootstrap@4.6.0:
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.0.tgz#97b9f29ac98f98dfa43bf7468262d84392552fd7" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.0.tgz#97b9f29ac98f98dfa43bf7468262d84392552fd7"
integrity sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw== integrity sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==
borc@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/borc/-/borc-2.1.2.tgz#6ce75e7da5ce711b963755117dd1b187f6f8cf19"
integrity sha512-Sy9eoUi4OiKzq7VovMn246iTo17kzuyHJKomCfpWMlI6RpfN1gk95w7d7gH264nApVLg0HZfcpz62/g4VH1Y4w==
dependencies:
bignumber.js "^9.0.0"
buffer "^5.5.0"
commander "^2.15.0"
ieee754 "^1.1.13"
iso-url "~0.4.7"
json-text-sequence "~0.1.0"
readable-stream "^3.6.0"
boxen@^5.1.2: boxen@^5.1.2:
version "5.1.2" version "5.1.2"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50"
@ -6582,7 +6647,7 @@ comma-separated-tokens@^1.0.0:
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea"
integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==
commander@2, commander@^2.11.0, commander@^2.20.0: commander@2, commander@^2.11.0, commander@^2.15.0, commander@^2.20.0:
version "2.20.3" version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@ -7856,6 +7921,11 @@ delegates@^1.0.0:
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
delimit-stream@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/delimit-stream/-/delimit-stream-0.1.0.tgz#9b8319477c0e5f8aeb3ce357ae305fc25ea1cd2b"
integrity sha512-a02fiQ7poS5CnjiJBAsjGLPp5EwVoGHNeu9sziBd9huppRfsAFIpv5zNLv0V1gbop53ilngAf5Kf331AwcoRBQ==
denque@^1.1.0, denque@^1.5.0: denque@^1.1.0, denque@^1.5.0:
version "1.5.1" version "1.5.1"
resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf"
@ -11247,6 +11317,11 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
iso-url@~0.4.7:
version "0.4.7"
resolved "https://registry.yarnpkg.com/iso-url/-/iso-url-0.4.7.tgz#de7e48120dae46921079fe78f325ac9e9217a385"
integrity sha512-27fFRDnPAMnHGLq36bWTpKET+eiXct3ENlCcdcMdk+mjXrb2kw3mhBUg1B7ewAC0kVzlOPhADzQgz1SE6Tglog==
isobject@^2.0.0: isobject@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
@ -11827,6 +11902,11 @@ jest@27.5.1:
import-local "^3.0.2" import-local "^3.0.2"
jest-cli "^27.5.1" jest-cli "^27.5.1"
js-sha256@0.9.0, js-sha256@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966"
integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==
js-string-escape@^1.0.1: js-string-escape@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef"
@ -11942,6 +12022,13 @@ json-stringify-safe@~5.0.1:
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==
json-text-sequence@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/json-text-sequence/-/json-text-sequence-0.1.1.tgz#a72f217dc4afc4629fff5feb304dc1bd51a2f3d2"
integrity sha512-L3mEegEWHRekSHjc7+sc8eJhba9Clq1PZ8kMkzf8OxElhXc8O4TS5MwcVlj9aEbm5dr81N90WHC5nAz3UO971w==
dependencies:
delimit-stream "0.1.0"
json5@2.x, json5@^2.1.2, json5@^2.1.3, json5@^2.2.1: json5@2.x, json5@^2.1.2, json5@^2.1.3, json5@^2.2.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
@ -13202,6 +13289,11 @@ no-case@^3.0.4:
lower-case "^2.0.2" lower-case "^2.0.2"
tslib "^2.0.3" tslib "^2.0.3"
node-addon-api@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32"
integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==
node-addon-api@^3.0.0, node-addon-api@^3.2.1: node-addon-api@^3.0.0, node-addon-api@^3.2.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
@ -13224,6 +13316,11 @@ node-gyp-build-optional-packages@5.0.2:
resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.2.tgz#3de7d30bd1f9057b5dfbaeab4a4442b7fe9c5901" resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.2.tgz#3de7d30bd1f9057b5dfbaeab4a4442b7fe9c5901"
integrity sha512-PiN4NWmlQPqvbEFcH/omQsswWQbe5Z9YK/zdB23irp5j2XibaA2IrGvpSWmVVG4qMZdmPdwPctSy4a86rOMn6g== integrity sha512-PiN4NWmlQPqvbEFcH/omQsswWQbe5Z9YK/zdB23irp5j2XibaA2IrGvpSWmVVG4qMZdmPdwPctSy4a86rOMn6g==
node-gyp-build@^4.2.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40"
integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==
node-gyp-build@^4.2.2, node-gyp-build@^4.3.0: node-gyp-build@^4.2.2, node-gyp-build@^4.3.0:
version "4.4.0" version "4.4.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.4.0.tgz#42e99687ce87ddeaf3a10b99dc06abc11021f3f4" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.4.0.tgz#42e99687ce87ddeaf3a10b99dc06abc11021f3f4"
@ -15735,6 +15832,15 @@ schema-utils@^4.0.0:
ajv-formats "^2.1.1" ajv-formats "^2.1.1"
ajv-keywords "^5.0.0" ajv-keywords "^5.0.0"
secp256k1@^4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.3.tgz#c4559ecd1b8d3c1827ed2d1b94190d69ce267303"
integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==
dependencies:
elliptic "^6.5.4"
node-addon-api "^2.0.0"
node-gyp-build "^4.2.0"
secure-compare@3.0.1: secure-compare@3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3" resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3"
@ -15950,6 +16056,11 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
simple-cbor@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/simple-cbor/-/simple-cbor-0.4.1.tgz#0c88312e87db52b94e0e92f6bd1cf634e86f8a22"
integrity sha512-rijcxtwx2b4Bje3sqeIqw5EeW7UlOIC4YfOdwqIKacpvRQ/D78bWg/4/0m5e0U91oKvlGh7LlJuZCu07ISCC7w==
simple-swizzle@^0.2.2: simple-swizzle@^0.2.2:
version "0.2.2" version "0.2.2"
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
@ -17120,6 +17231,11 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==
tweetnacl@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
twitter-api-v2@1.10.3: twitter-api-v2@1.10.3:
version "1.10.3" version "1.10.3"
resolved "https://registry.yarnpkg.com/twitter-api-v2/-/twitter-api-v2-1.10.3.tgz#07441bd9c4d27433aa0284d900cf60f6328b8239" resolved "https://registry.yarnpkg.com/twitter-api-v2/-/twitter-api-v2-1.10.3.tgz#07441bd9c4d27433aa0284d900cf60f6328b8239"