Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
678f1f0051 | |||
71c7e37b5a | |||
80459371f3 | |||
35f1f348a8 | |||
0bb0b12991 | |||
d887de50d2 | |||
2571e5b8c0 | |||
e444d717e5 | |||
1866e26c1d | |||
9923074e04 | |||
c367e61b85 | |||
364f1ad9b9 | |||
2394cbd6fe | |||
a74d5cce20 | |||
95bcc3f32d | |||
e9dbd4a55d | |||
d440b09dc9 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
|
||||||
|
50
CHANGELOG.md
50
CHANGELOG.md
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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() {
|
||||||
|
@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
|
@ -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({
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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') }
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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') }
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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') }
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -2,5 +2,6 @@
|
|||||||
"LUNA1": "Terra",
|
"LUNA1": "Terra",
|
||||||
"LUNA2": "Terra",
|
"LUNA2": "Terra",
|
||||||
"SGB1": "Songbird",
|
"SGB1": "Songbird",
|
||||||
"UNI1": "Uniswap"
|
"UNI1": "Uniswap",
|
||||||
|
"UST": "TerraUSD"
|
||||||
}
|
}
|
||||||
|
@ -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' }),
|
||||||
|
@ -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');
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
@ -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: () =>
|
||||||
|
@ -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'"
|
||||||
|
@ -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)"
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
|
@ -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) {
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
ngx-skeleton-loader {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -12,4 +12,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mat-form-field {
|
||||||
|
::ng-deep {
|
||||||
|
.mat-form-field-wrapper {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 {}
|
@ -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 {}
|
@ -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>
|
@ -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 {}
|
@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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) {
|
||||||
|
55
apps/client/src/app/services/internet-identity.service.ts
Normal file
55
apps/client/src/app/services/internet-identity.service.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
1
apps/client/src/assets/icons/google.svg
Normal file
1
apps/client/src/assets/icons/google.svg
Normal 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 |
28
apps/client/src/assets/icons/internet-computer.svg
Normal file
28
apps/client/src/assets/icons/internet-computer.svg
Normal 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 |
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
export interface OAuthResponse {
|
||||||
|
authToken: string;
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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(),
|
||||||
|
@ -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
|
||||||
|
13
package.json
13
package.json
@ -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": {
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "Provider" ADD VALUE 'INTERNET_IDENTITY';
|
@ -217,6 +217,7 @@ enum ViewMode {
|
|||||||
enum Provider {
|
enum Provider {
|
||||||
ANONYMOUS
|
ANONYMOUS
|
||||||
GOOGLE
|
GOOGLE
|
||||||
|
INTERNET_IDENTITY
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Role {
|
enum Role {
|
||||||
|
28
test/import/ok-novn-buy-and-sell-partially.json
Normal file
28
test/import/ok-novn-buy-and-sell-partially.json
Normal 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
118
yarn.lock
@ -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"
|
||||||
|
Reference in New Issue
Block a user