Compare commits
27 Commits
Author | SHA1 | Date | |
---|---|---|---|
678f1f0051 | |||
71c7e37b5a | |||
80459371f3 | |||
35f1f348a8 | |||
0bb0b12991 | |||
d887de50d2 | |||
2571e5b8c0 | |||
e444d717e5 | |||
1866e26c1d | |||
9923074e04 | |||
c367e61b85 | |||
364f1ad9b9 | |||
2394cbd6fe | |||
a74d5cce20 | |||
95bcc3f32d | |||
e9dbd4a55d | |||
d440b09dc9 | |||
cc16ba5dc8 | |||
d10227bc39 | |||
4e214c32e8 | |||
49e2862e03 | |||
34e33a2400 | |||
ec9bc984af | |||
2388c494df | |||
d71ab10eed | |||
0e0592180f | |||
60e2aff488 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -24,15 +24,16 @@
|
||||
|
||||
# misc
|
||||
/.angular/cache
|
||||
.env.prod
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/dist
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
yarn-error.log
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
|
77
CHANGELOG.md
77
CHANGELOG.md
@ -5,6 +5,83 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 1.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
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for the cryptocurrency _Songbird_ (`SGB1-USD`)
|
||||
- Added support for the cryptocurrency _Terra 2.0_ (`LUNA2-USD`)
|
||||
- Added a blog post
|
||||
|
||||
### Changed
|
||||
|
||||
- Refreshed the cryptocurrencies list to support more coins by default
|
||||
- Upgraded `date-fns` from version `2.22.1` to `2.28.0`
|
||||
|
||||
## 1.168.0 - 10.07.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Extended the investment timeline grouped by month
|
||||
|
||||
### Changed
|
||||
|
||||
- Handled an occasional currency pair inconsistency in the _Yahoo Finance_ service (`GBP=X` instead of `USDGBP=X`)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the content height of the account detail dialog
|
||||
|
||||
## 1.167.0 - 07.07.2022
|
||||
|
||||
### Added
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.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 { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
@ -24,7 +23,6 @@ export class AdminService {
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly dataGatheringService: DataGatheringService,
|
||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||
private readonly marketDataService: MarketDataService,
|
||||
private readonly prismaService: PrismaService,
|
||||
@ -174,7 +172,6 @@ export class AdminService {
|
||||
_count: {
|
||||
select: { Account: true, Order: true }
|
||||
},
|
||||
alias: true,
|
||||
Analytics: {
|
||||
select: {
|
||||
activityCount: true,
|
||||
@ -194,7 +191,7 @@ export class AdminService {
|
||||
});
|
||||
|
||||
return usersWithAnalytics.map(
|
||||
({ _count, alias, Analytics, createdAt, id, Subscription }) => {
|
||||
({ _count, Analytics, createdAt, id, Subscription }) => {
|
||||
const daysSinceRegistration =
|
||||
differenceInDays(new Date(), createdAt) + 1;
|
||||
const engagement = Analytics.activityCount / daysSinceRegistration;
|
||||
@ -206,7 +203,6 @@ export class AdminService {
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
alias,
|
||||
createdAt,
|
||||
engagement,
|
||||
id,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { OAuthResponse } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
@ -31,7 +32,9 @@ export class AuthController {
|
||||
) {}
|
||||
|
||||
@Get('anonymous/:accessToken')
|
||||
public async accessTokenLogin(@Param('accessToken') accessToken: string) {
|
||||
public async accessTokenLogin(
|
||||
@Param('accessToken') accessToken: string
|
||||
): Promise<OAuthResponse> {
|
||||
try {
|
||||
const authToken = await this.authService.validateAnonymousLogin(
|
||||
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')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
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 { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Provider } from '@prisma/client';
|
||||
|
||||
import { ValidateOAuthLoginParams } from './interfaces/interfaces';
|
||||
|
||||
@ -13,7 +14,7 @@ export class AuthService {
|
||||
private readonly userService: UserService
|
||||
) {}
|
||||
|
||||
public async validateAnonymousLogin(accessToken: string) {
|
||||
public async validateAnonymousLogin(accessToken: string): Promise<string> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const hashedAccessToken = this.userService.createAccessToken(
|
||||
@ -26,7 +27,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (user) {
|
||||
const jwt: string = this.jwtService.sign({
|
||||
const jwt = this.jwtService.sign({
|
||||
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({
|
||||
provider,
|
||||
thirdPartyId
|
||||
@ -57,13 +85,14 @@ export class AuthService {
|
||||
});
|
||||
}
|
||||
|
||||
const jwt: string = this.jwtService.sign({
|
||||
return this.jwtService.sign({
|
||||
id: user.id
|
||||
});
|
||||
|
||||
return jwt;
|
||||
} catch (err) {
|
||||
throw new InternalServerErrorException('validateOAuthLogin', err.message);
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(
|
||||
'validateOAuthLogin',
|
||||
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 {
|
||||
return await this.importService.import({
|
||||
maxActivitiesToImport,
|
||||
activities: importData.activities,
|
||||
userId: this.request.user.id
|
||||
});
|
||||
|
@ -17,9 +17,11 @@ export class ImportService {
|
||||
|
||||
public async import({
|
||||
activities,
|
||||
maxActivitiesToImport,
|
||||
userId
|
||||
}: {
|
||||
activities: Partial<CreateOrderDto>[];
|
||||
maxActivitiesToImport: number;
|
||||
userId: string;
|
||||
}): Promise<void> {
|
||||
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(
|
||||
(account) => {
|
||||
@ -81,19 +87,15 @@ export class ImportService {
|
||||
|
||||
private async validateActivities({
|
||||
activities,
|
||||
maxActivitiesToImport,
|
||||
userId
|
||||
}: {
|
||||
activities: Partial<CreateOrderDto>[];
|
||||
maxActivitiesToImport: number;
|
||||
userId: string;
|
||||
}) {
|
||||
if (
|
||||
activities?.length > this.configurationService.get('MAX_ORDERS_TO_IMPORT')
|
||||
) {
|
||||
throw new Error(
|
||||
`Too many activities (${this.configurationService.get(
|
||||
'MAX_ORDERS_TO_IMPORT'
|
||||
)} at most)`
|
||||
);
|
||||
if (activities?.length > maxActivitiesToImport) {
|
||||
throw new Error(`Too many activities (${maxActivitiesToImport} at most)`);
|
||||
}
|
||||
|
||||
const existingActivities = await this.orderService.orders({
|
||||
|
@ -1,5 +1,12 @@
|
||||
import { AssetClass, AssetSubClass, DataSource, Type } from '@prisma/client';
|
||||
import {
|
||||
AssetClass,
|
||||
AssetSubClass,
|
||||
DataSource,
|
||||
Tag,
|
||||
Type
|
||||
} from '@prisma/client';
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsISO8601,
|
||||
IsNumber,
|
||||
@ -39,6 +46,10 @@ export class CreateOrderDto {
|
||||
@IsString()
|
||||
symbol: string;
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
tags?: Tag[];
|
||||
|
||||
@IsEnum(Type, { each: true })
|
||||
type: Type;
|
||||
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
DataSource,
|
||||
Order,
|
||||
Prisma,
|
||||
Tag,
|
||||
Type as TypeOfOrder
|
||||
} from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
@ -71,6 +72,7 @@ export class OrderService {
|
||||
currency?: string;
|
||||
dataSource?: DataSource;
|
||||
symbol?: string;
|
||||
tags?: Tag[];
|
||||
userId: string;
|
||||
}
|
||||
): Promise<Order> {
|
||||
@ -80,6 +82,8 @@ export class OrderService {
|
||||
return account.isDefault === true;
|
||||
});
|
||||
|
||||
const tags = data.tags ?? [];
|
||||
|
||||
let Account = {
|
||||
connect: {
|
||||
id_userId: {
|
||||
@ -142,6 +146,7 @@ export class OrderService {
|
||||
delete data.currency;
|
||||
delete data.dataSource;
|
||||
delete data.symbol;
|
||||
delete data.tags;
|
||||
delete data.userId;
|
||||
|
||||
const orderData: Prisma.OrderCreateInput = data;
|
||||
@ -150,7 +155,12 @@ export class OrderService {
|
||||
data: {
|
||||
...orderData,
|
||||
Account,
|
||||
isDraft
|
||||
isDraft,
|
||||
tags: {
|
||||
connect: tags.map(({ id }) => {
|
||||
return { id };
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -298,6 +308,7 @@ export class OrderService {
|
||||
currency?: string;
|
||||
dataSource?: DataSource;
|
||||
symbol?: string;
|
||||
tags?: Tag[];
|
||||
};
|
||||
where: Prisma.OrderWhereUniqueInput;
|
||||
}): Promise<Order> {
|
||||
@ -305,6 +316,8 @@ export class OrderService {
|
||||
delete data.Account;
|
||||
}
|
||||
|
||||
const tags = data.tags ?? [];
|
||||
|
||||
let isDraft = false;
|
||||
|
||||
if (data.type === 'ITEM') {
|
||||
@ -331,11 +344,17 @@ export class OrderService {
|
||||
delete data.currency;
|
||||
delete data.dataSource;
|
||||
delete data.symbol;
|
||||
delete data.tags;
|
||||
|
||||
return this.prismaService.order.update({
|
||||
data: {
|
||||
...data,
|
||||
isDraft
|
||||
isDraft,
|
||||
tags: {
|
||||
connect: tags.map(({ id }) => {
|
||||
return { id };
|
||||
})
|
||||
}
|
||||
},
|
||||
where
|
||||
});
|
||||
|
@ -1,5 +1,12 @@
|
||||
import { AssetClass, AssetSubClass, DataSource, Type } from '@prisma/client';
|
||||
import {
|
||||
AssetClass,
|
||||
AssetSubClass,
|
||||
DataSource,
|
||||
Tag,
|
||||
Type
|
||||
} from '@prisma/client';
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsISO8601,
|
||||
IsNumber,
|
||||
@ -41,6 +48,10 @@ export class UpdateOrderDto {
|
||||
@IsString()
|
||||
symbol: string;
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
tags?: Tag[];
|
||||
|
||||
@IsString()
|
||||
type: Type;
|
||||
|
||||
|
@ -62,6 +62,10 @@ describe('PortfolioCalculator', () => {
|
||||
parseDate('2021-11-22')
|
||||
);
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth();
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(currentPositions).toEqual({
|
||||
@ -91,6 +95,15 @@ describe('PortfolioCalculator', () => {
|
||||
],
|
||||
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')
|
||||
);
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth();
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(currentPositions).toEqual({
|
||||
@ -80,6 +84,14 @@ describe('PortfolioCalculator', () => {
|
||||
],
|
||||
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()
|
||||
);
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth();
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(currentPositions).toEqual({
|
||||
@ -51,6 +55,10 @@ describe('PortfolioCalculator', () => {
|
||||
positions: [],
|
||||
totalInvestment: new Big(0)
|
||||
});
|
||||
|
||||
expect(investments).toEqual([]);
|
||||
|
||||
expect(investmentsByMonth).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -62,6 +62,10 @@ describe('PortfolioCalculator', () => {
|
||||
parseDate('2022-03-07')
|
||||
);
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth();
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(currentPositions).toEqual({
|
||||
@ -91,6 +95,16 @@ describe('PortfolioCalculator', () => {
|
||||
],
|
||||
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') }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -14,8 +14,11 @@ import {
|
||||
format,
|
||||
isAfter,
|
||||
isBefore,
|
||||
isSameMonth,
|
||||
isSameYear,
|
||||
max,
|
||||
min
|
||||
min,
|
||||
set
|
||||
} from 'date-fns';
|
||||
import { first, flatten, isNumber, sortBy } from 'lodash';
|
||||
|
||||
@ -323,6 +326,53 @@ export class PortfolioCalculator {
|
||||
});
|
||||
}
|
||||
|
||||
public getInvestmentsByMonth(): { date: string; investment: Big }[] {
|
||||
if (this.orders.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const investments = [];
|
||||
let currentDate: Date;
|
||||
let investmentByMonth = new Big(0);
|
||||
|
||||
for (const [index, order] of this.orders.entries()) {
|
||||
if (
|
||||
isSameMonth(parseDate(order.date), currentDate) &&
|
||||
isSameYear(parseDate(order.date), currentDate)
|
||||
) {
|
||||
// Same month: Add up investments
|
||||
|
||||
investmentByMonth = investmentByMonth.plus(
|
||||
order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
|
||||
);
|
||||
} else {
|
||||
// New month: Store previous month and reset
|
||||
|
||||
if (currentDate) {
|
||||
investments.push({
|
||||
date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
|
||||
investment: investmentByMonth
|
||||
});
|
||||
}
|
||||
|
||||
currentDate = parseDate(order.date);
|
||||
investmentByMonth = order.quantity
|
||||
.mul(order.unitPrice)
|
||||
.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;
|
||||
}
|
||||
|
||||
public async calculateTimeline(
|
||||
timelineSpecification: TimelineSpecification[],
|
||||
endDate: string
|
||||
|
@ -20,7 +20,12 @@ import {
|
||||
PortfolioReport,
|
||||
PortfolioSummary
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import type {
|
||||
DateRange,
|
||||
GroupBy,
|
||||
RequestWithUser
|
||||
} from '@ghostfolio/common/types';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
@ -217,7 +222,8 @@ export class PortfolioController {
|
||||
@Get('investments')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getInvestments(
|
||||
@Headers('impersonation-id') impersonationId: string
|
||||
@Headers('impersonation-id') impersonationId: string,
|
||||
@Query('groupBy') groupBy?: GroupBy
|
||||
): Promise<PortfolioInvestments> {
|
||||
if (
|
||||
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||
@ -229,9 +235,16 @@ export class PortfolioController {
|
||||
);
|
||||
}
|
||||
|
||||
let investments = await this.portfolioService.getInvestments(
|
||||
impersonationId
|
||||
);
|
||||
let investments: InvestmentItem[];
|
||||
|
||||
if (groupBy === 'month') {
|
||||
investments = await this.portfolioService.getInvestments(
|
||||
impersonationId,
|
||||
'month'
|
||||
);
|
||||
} else {
|
||||
investments = await this.portfolioService.getInvestments(impersonationId);
|
||||
}
|
||||
|
||||
if (
|
||||
impersonationId ||
|
||||
|
@ -41,6 +41,7 @@ import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.in
|
||||
import type {
|
||||
AccountWithValue,
|
||||
DateRange,
|
||||
GroupBy,
|
||||
Market,
|
||||
OrderWithAccount,
|
||||
RequestWithUser
|
||||
@ -64,6 +65,7 @@ import {
|
||||
max,
|
||||
parse,
|
||||
parseISO,
|
||||
set,
|
||||
setDayOfYear,
|
||||
startOfDay,
|
||||
subDays,
|
||||
@ -183,7 +185,8 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
public async getInvestments(
|
||||
aImpersonationId: string
|
||||
aImpersonationId: string,
|
||||
groupBy?: GroupBy
|
||||
): Promise<InvestmentItem[]> {
|
||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||
|
||||
@ -204,28 +207,57 @@ export class PortfolioService {
|
||||
return [];
|
||||
}
|
||||
|
||||
const investments = portfolioCalculator.getInvestments().map((item) => {
|
||||
return {
|
||||
date: item.date,
|
||||
investment: item.investment.toNumber()
|
||||
};
|
||||
});
|
||||
let investments: InvestmentItem[];
|
||||
|
||||
// Add investment of today
|
||||
const investmentOfToday = investments.filter((investment) => {
|
||||
return investment.date === format(new Date(), DATE_FORMAT);
|
||||
});
|
||||
|
||||
if (investmentOfToday.length <= 0) {
|
||||
const pastInvestments = investments.filter((investment) => {
|
||||
return isBefore(parseDate(investment.date), new Date());
|
||||
if (groupBy === 'month') {
|
||||
investments = portfolioCalculator.getInvestmentsByMonth().map((item) => {
|
||||
return {
|
||||
date: item.date,
|
||||
investment: item.investment.toNumber()
|
||||
};
|
||||
});
|
||||
const lastInvestment = pastInvestments[pastInvestments.length - 1];
|
||||
|
||||
investments.push({
|
||||
date: format(new Date(), DATE_FORMAT),
|
||||
investment: lastInvestment?.investment ?? 0
|
||||
// Add investment of current month
|
||||
const dateOfCurrentMonth = format(
|
||||
set(new Date(), { date: 1 }),
|
||||
DATE_FORMAT
|
||||
);
|
||||
const investmentOfCurrentMonth = investments.filter(({ date }) => {
|
||||
return date === dateOfCurrentMonth;
|
||||
});
|
||||
|
||||
if (investmentOfCurrentMonth.length <= 0) {
|
||||
investments.push({
|
||||
date: dateOfCurrentMonth,
|
||||
investment: 0
|
||||
});
|
||||
}
|
||||
} else {
|
||||
investments = portfolioCalculator
|
||||
.getInvestments()
|
||||
.map(({ date, investment }) => {
|
||||
return {
|
||||
date,
|
||||
investment: investment.toNumber()
|
||||
};
|
||||
});
|
||||
|
||||
// Add investment of today
|
||||
const investmentOfToday = investments.filter(({ date }) => {
|
||||
return date === format(new Date(), DATE_FORMAT);
|
||||
});
|
||||
|
||||
if (investmentOfToday.length <= 0) {
|
||||
const pastInvestments = investments.filter(({ date }) => {
|
||||
return isBefore(parseDate(date), new Date());
|
||||
});
|
||||
const lastInvestment = pastInvestments[pastInvestments.length - 1];
|
||||
|
||||
investments.push({
|
||||
date: format(new Date(), DATE_FORMAT),
|
||||
investment: lastInvestment?.investment ?? 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return sortBy(investments, (investment) => {
|
||||
|
@ -36,14 +36,7 @@ export class UserService {
|
||||
}
|
||||
|
||||
public async getUser(
|
||||
{
|
||||
Account,
|
||||
alias,
|
||||
id,
|
||||
permissions,
|
||||
Settings,
|
||||
subscription
|
||||
}: UserWithSettings,
|
||||
{ Account, id, permissions, Settings, subscription }: UserWithSettings,
|
||||
aLocale = locale
|
||||
): Promise<IUser> {
|
||||
const access = await this.prismaService.access.findMany({
|
||||
@ -63,7 +56,6 @@ export class UserService {
|
||||
}
|
||||
|
||||
return {
|
||||
alias,
|
||||
id,
|
||||
permissions,
|
||||
subscription,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,7 @@
|
||||
{
|
||||
"LUNA1": "Terra",
|
||||
"UNI1": "Uniswap"
|
||||
"LUNA2": "Terra",
|
||||
"SGB1": "Songbird",
|
||||
"UNI1": "Uniswap",
|
||||
"UST": "TerraUSD"
|
||||
}
|
||||
|
@ -33,8 +33,8 @@ export class ConfigurationService {
|
||||
GOOGLE_SHEETS_PRIVATE_KEY: str({ default: '' }),
|
||||
HOST: host({ default: '0.0.0.0' }),
|
||||
JWT_SECRET_KEY: str({}),
|
||||
MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }),
|
||||
MAX_ITEM_IN_CACHE: num({ default: 9999 }),
|
||||
MAX_ORDERS_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }),
|
||||
PORT: port({ default: 3333 }),
|
||||
RAKUTEN_RAPID_API_KEY: str({ default: '' }),
|
||||
REDIS_HOST: host({ default: 'localhost' }),
|
||||
|
@ -37,10 +37,15 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
}
|
||||
|
||||
public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
|
||||
const symbol = aYahooFinanceSymbol.replace(
|
||||
let symbol = aYahooFinanceSymbol.replace(
|
||||
new RegExp(`-${this.baseCurrency}$`),
|
||||
this.baseCurrency
|
||||
);
|
||||
|
||||
if (symbol.includes('=X') && !symbol.includes(this.baseCurrency)) {
|
||||
symbol = `${this.baseCurrency}${symbol}`;
|
||||
}
|
||||
|
||||
return symbol.replace('=X', '');
|
||||
}
|
||||
|
||||
@ -261,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;
|
||||
} catch (error) {
|
||||
Logger.error(error, 'YahooFinanceService');
|
||||
|
@ -122,15 +122,6 @@ export class ExchangeRateDataService {
|
||||
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;
|
||||
|
||||
if (aFromCurrency !== aToCurrency) {
|
||||
|
@ -23,8 +23,8 @@ export interface Environment extends CleanedEnvAccessors {
|
||||
GOOGLE_SHEETS_ID: string;
|
||||
GOOGLE_SHEETS_PRIVATE_KEY: string;
|
||||
JWT_SECRET_KEY: string;
|
||||
MAX_ACTIVITIES_TO_IMPORT: number;
|
||||
MAX_ITEM_IN_CACHE: number;
|
||||
MAX_ORDERS_TO_IMPORT: number;
|
||||
PORT: number;
|
||||
RAKUTEN_RAPID_API_KEY: string;
|
||||
REDIS_HOST: string;
|
||||
|
@ -78,6 +78,20 @@ const routes: Routes = [
|
||||
'./pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module'
|
||||
).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',
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'./pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.module'
|
||||
).then((m) => m.HowDoIGetMyFinancesInOrderPageModule)
|
||||
},
|
||||
{
|
||||
path: 'features',
|
||||
loadChildren: () =>
|
||||
|
@ -1,3 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.mat-dialog-content {
|
||||
max-height: unset;
|
||||
}
|
||||
}
|
||||
|
@ -29,11 +29,10 @@
|
||||
<td class="mat-cell px-1 py-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="d-none d-sm-inline-block"
|
||||
>{{ userItem.alias || userItem.id }}</span
|
||||
>{{ userItem.id }}</span
|
||||
>
|
||||
<span class="d-inline-block d-sm-none"
|
||||
>{{ userItem.alias || (userItem.id | slice:0:5) +
|
||||
'...' }}</span
|
||||
>{{ (userItem.id | slice:0:5) + '...' }}</span
|
||||
>
|
||||
<gf-premium-indicator
|
||||
*ngIf="userItem?.subscription?.type === 'Premium'"
|
||||
|
@ -124,13 +124,11 @@
|
||||
: 'radio-button-on-outline'
|
||||
"
|
||||
></ion-icon>
|
||||
<span *ngIf="user?.alias">{{ user.alias }}</span>
|
||||
<span *ngIf="!user?.alias" i18n><span></span>Me</span>
|
||||
<span i18n>Me</span>
|
||||
</button>
|
||||
<button
|
||||
*ngFor="let accessItem of user?.access"
|
||||
class="align-items-center d-flex"
|
||||
disabled="false"
|
||||
mat-menu-item
|
||||
(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"
|
||||
>
|
||||
<div class="row w-100">
|
||||
<div class="chart-container col">
|
||||
<gf-line-chart
|
||||
class="position-absolute"
|
||||
symbol="Performance"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[locale]="user?.settings?.locale"
|
||||
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
|
||||
[showGradient]="true"
|
||||
[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 class="col p-0">
|
||||
<div class="chart-container mx-auto position-relative">
|
||||
<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>
|
||||
|
@ -5,7 +5,8 @@
|
||||
|
||||
.chart-container {
|
||||
aspect-ratio: 16 / 9;
|
||||
max-height: 50vh;
|
||||
height: auto;
|
||||
max-width: 67rem;
|
||||
|
||||
// Fallback for aspect-ratio (using padding hack)
|
||||
@supports not (aspect-ratio: 16 / 9) {
|
||||
|
@ -1,3 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
ngx-skeleton-loader {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,10 @@ import {
|
||||
transformTickToAbbreviation
|
||||
} from '@ghostfolio/common/helper';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import { GroupBy } from '@ghostfolio/common/types';
|
||||
import {
|
||||
BarController,
|
||||
BarElement,
|
||||
Chart,
|
||||
LineController,
|
||||
LineElement,
|
||||
@ -42,6 +45,7 @@ import { addDays, isAfter, parseISO, subDays } from 'date-fns';
|
||||
export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
@Input() currency: string;
|
||||
@Input() daysInMarket: number;
|
||||
@Input() groupBy: GroupBy;
|
||||
@Input() investments: InvestmentItem[];
|
||||
@Input() isInPercent = false;
|
||||
@Input() locale: string;
|
||||
@ -53,6 +57,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
|
||||
public constructor() {
|
||||
Chart.register(
|
||||
BarController,
|
||||
BarElement,
|
||||
LinearScale,
|
||||
LineController,
|
||||
LineElement,
|
||||
@ -78,7 +84,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
private initialize() {
|
||||
this.isLoading = true;
|
||||
|
||||
if (this.investments?.length > 0) {
|
||||
if (!this.groupBy && this.investments?.length > 0) {
|
||||
// Extend chart by 5% of days in market (before)
|
||||
const firstItem = this.investments[0];
|
||||
this.investments.unshift({
|
||||
@ -102,13 +108,14 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
const data = {
|
||||
labels: this.investments.map((position) => {
|
||||
return position.date;
|
||||
labels: this.investments.map((investmentItem) => {
|
||||
return investmentItem.date;
|
||||
}),
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||
borderWidth: 2,
|
||||
borderWidth: this.groupBy ? 0 : 2,
|
||||
data: this.investments.map((position) => {
|
||||
return position.investment;
|
||||
}),
|
||||
@ -137,6 +144,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
this.chart = new Chart(this.chartCanvas.nativeElement, {
|
||||
data,
|
||||
options: {
|
||||
animation: false,
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0
|
||||
@ -178,8 +186,10 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
grid: {
|
||||
borderColor: `rgba(${getTextColor()}, 0.1)`,
|
||||
color: `rgba(${getTextColor()}, 0.8)`,
|
||||
display: false
|
||||
display: false,
|
||||
drawBorder: false
|
||||
},
|
||||
position: 'right',
|
||||
ticks: {
|
||||
callback: (value: number) => {
|
||||
return transformTickToAbbreviation(value);
|
||||
@ -192,12 +202,12 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
},
|
||||
plugins: [getVerticalHoverLinePlugin(this.chartCanvas)],
|
||||
type: 'line'
|
||||
type: this.groupBy ? 'bar' : 'line'
|
||||
});
|
||||
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
private getTooltipPluginConfiguration() {
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service';
|
||||
import {
|
||||
STAY_SIGNED_IN,
|
||||
SettingsStorageService
|
||||
} from '@ghostfolio/client/services/settings-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-login-with-access-token-dialog',
|
||||
@ -16,7 +19,10 @@ export class LoginWithAccessTokenDialog {
|
||||
public constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
public dialogRef: MatDialogRef<LoginWithAccessTokenDialog>,
|
||||
private settingsStorageService: SettingsStorageService
|
||||
private internetIdentityService: InternetIdentityService,
|
||||
private router: Router,
|
||||
private settingsStorageService: SettingsStorageService,
|
||||
private tokenStorageService: TokenStorageService
|
||||
) {}
|
||||
|
||||
ngOnInit() {}
|
||||
@ -31,4 +37,14 @@ export class LoginWithAccessTokenDialog {
|
||||
public onClose() {
|
||||
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>
|
||||
|
||||
<div mat-dialog-content>
|
||||
<div>
|
||||
<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>
|
||||
<div class="align-items-center d-flex flex-column">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Security Token</mat-label>
|
||||
<textarea
|
||||
@ -24,6 +15,29 @@
|
||||
[(ngModel)]="data.accessToken"
|
||||
></textarea>
|
||||
</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 mat-dialog-actions>
|
||||
|
@ -12,4 +12,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-form-field {
|
||||
::ng-deep {
|
||||
.mat-form-field-wrapper {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -142,4 +142,15 @@
|
||||
</button>
|
||||
</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>
|
||||
|
@ -27,6 +27,7 @@ import { Subject, Subscription } from 'rxjs';
|
||||
export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
@Input() baseCurrency: string;
|
||||
@Input() deviceType: string;
|
||||
@Input() hasPermissionToCreateActivity: boolean;
|
||||
@Input() hasPermissionToShowValues = true;
|
||||
@Input() locale: string;
|
||||
@Input() pageSize = Number.MAX_SAFE_INTEGER;
|
||||
|
@ -8,10 +8,6 @@
|
||||
<div class="col">
|
||||
<mat-card class="mb-3">
|
||||
<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
|
||||
*ngIf="hasPermissionToUpdateUserSettings && user?.subscription"
|
||||
class="d-flex py-1"
|
||||
|
@ -3,8 +3,8 @@
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1" i18n>Hallo Ghostfolio 👋</h1>
|
||||
<div class="text-muted"><small>31.07.2021</small></div>
|
||||
<h1 class="mb-1">Hallo Ghostfolio 👋</h1>
|
||||
<div class="text-muted"><small>2021-07-31</small></div>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
|
@ -3,8 +3,8 @@
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1" i18n>Hello Ghostfolio 👋</h1>
|
||||
<div class="text-muted"><small>31.07.2021</small></div>
|
||||
<h1 class="mb-1">Hello Ghostfolio 👋</h1>
|
||||
<div class="text-muted"><small>2021-07-31</small></div>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
|
@ -3,11 +3,11 @@
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1" i18n>
|
||||
<h1 class="mb-1">
|
||||
👻 Ghostfolio –
|
||||
<span class="text-nowrap">First months in Open Source</span>
|
||||
</h1>
|
||||
<div class="text-muted"><small>05.01.2022</small></div>
|
||||
<div class="text-muted"><small>2022-01-05</small></div>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<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;
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { HowDoIGetMyFinancesInOrderPageComponent } from './how-do-i-get-my-finances-in-order-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: HowDoIGetMyFinancesInOrderPageComponent,
|
||||
canActivate: [AuthGuard]
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class HowDoIGetMyFinancesInOrderRoutingModule {}
|
@ -0,0 +1,9 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-how-do-i-get-my-finances-in-order-page',
|
||||
styleUrls: ['./how-do-i-get-my-finances-in-order-page.scss'],
|
||||
templateUrl: './how-do-i-get-my-finances-in-order-page.html'
|
||||
})
|
||||
export class HowDoIGetMyFinancesInOrderPageComponent {}
|
@ -0,0 +1,209 @@
|
||||
<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">How do I get my finances in order?</h1>
|
||||
<div class="text-muted"><small>2022-07-14</small></div>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
Before you can think of
|
||||
<a [routerLink]="['/resources']">long-term investing</a>, you have
|
||||
to get your finances in order. Take a look at Peter's journey to see
|
||||
how you can achieve it, too.
|
||||
</p>
|
||||
<p>
|
||||
Peter enjoys life, but sometimes he overspends a bit. He realizes it
|
||||
when money runs out already in the middle of the month. Then the
|
||||
next few days become difficult and saving money is out of the
|
||||
question. That is why he wants to plan his monthly budget in the
|
||||
future.
|
||||
</p>
|
||||
<p>
|
||||
Peter has a decent salary in his job. But as soon as the salary
|
||||
arrives in his account, it melts away. In order to find out where
|
||||
his money is disappearing, he has decided to plan his monthly
|
||||
budget. He wants to be able to put money aside for major expenses
|
||||
and set financial goals.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Keeping a traditional or digital budget book</h2>
|
||||
<p>
|
||||
First, Peter obtains an overview of his personal finances. To do so,
|
||||
he starts keeping a budget book. This can be done on paper by
|
||||
listing his income and expenses for a few months, or he can create a
|
||||
simple spreadsheet in Excel. In addition, many credit card providers
|
||||
offer the feature within their apps of having expenses automatically
|
||||
analyzed according to different categories. According to the
|
||||
<a href="https://www.bfs.admin.ch/bfs/en/home.html"
|
||||
>Swiss Federal Statistical Office</a
|
||||
>, households in Switzerland spend around 20 percent of their
|
||||
disposable income on housing and around 10 percent on groceries.
|
||||
</p>
|
||||
<p>
|
||||
With the smartphone app, Peter has a better overview of his
|
||||
financial affairs. The application assigns the bookings to
|
||||
individual categories. Peter can assign specific budgets to each of
|
||||
them. This way, he is always informed about how much money he can
|
||||
still spend on restaurant visits in the current month, for example.
|
||||
A traditional method is the so-called
|
||||
<a
|
||||
href="https://www.investopedia.com/envelope-budgeting-system-5208026"
|
||||
>envelope method</a
|
||||
>. One envelope is labeled for each category like groceries, rent or
|
||||
student loans. The monthly budget is put into the envelopes in cash.
|
||||
Many apps offer the same budgeting system in a more convenient,
|
||||
virtual way.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Planning and investing</h2>
|
||||
<p>
|
||||
If Peter has spent less money than planned on eating out at
|
||||
restaurants, he can set aside the remaining amount. This way, he can
|
||||
treat himself to something special every now and then. From now on,
|
||||
he saves a fixed amount of money in a separate account (“pay
|
||||
yourself first”) by standing order at the beginning of the month. As
|
||||
soon as there are three net monthly salaries in the account, he
|
||||
invests the monthly savings amount in a passively managed global
|
||||
equity fund. This grows his assets over the years and allows him to
|
||||
supplement his pension later.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">How to achieve your financial goals?</h2>
|
||||
<p>
|
||||
If you follow these five actionable tips, you can reach your
|
||||
financial goals easier and faster. Start with one tip and when you
|
||||
implement it well, you can try the next one to ultimately have more
|
||||
money at the end of the month.
|
||||
</p>
|
||||
<h3 class="h5">1. Visualize your goals</h3>
|
||||
<p>
|
||||
Start visualizing your goals. For example, hang up pictures of the
|
||||
travel destination you are saving for. Imagine that you have already
|
||||
achieved the goal to slowly adapt your mindset.
|
||||
</p>
|
||||
<h3 class="h5">2. Write off personal items</h3>
|
||||
<p>
|
||||
Do as a business does and write off purchases annually. For a new
|
||||
car, you could set aside one-sixth of the purchase price each year.
|
||||
</p>
|
||||
<h3 class="h5">3. Save at the beginning of the month</h3>
|
||||
<p>
|
||||
Have a savings amount deducted from your account at the beginning of
|
||||
the month. Then you will pay yourself first and spend less money.
|
||||
</p>
|
||||
<h3 class="h5">4. Follow the 50-30-20 rule</h3>
|
||||
<p>
|
||||
You need 50 percent of your disposable income for fixed costs. 30
|
||||
percent can be spent on personal needs such as hobbies, travel or
|
||||
consumer electronics. 20 percent is left for savings or to pay off
|
||||
potential debts.
|
||||
</p>
|
||||
<h3 class="h5">5. Track your progress</h3>
|
||||
<p>
|
||||
If you have any money to spare, invest it in a broadly diversified,
|
||||
low-cost portfolio excluding the risks of individual stocks. Track
|
||||
the progress of your portfolio and net worth with
|
||||
<a href="https://ghostfol.io">Ghostfolio</a>, a web-based personal
|
||||
finance management software.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<ul class="list-inline">
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">App</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Assets</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Budget</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Cash</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Debt</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Equity</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">ETF</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Expense</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Finance</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Fintech</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Fund</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">Goal</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Guide</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Income</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Investment</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Money</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Net Worth</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Pension</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Personal Finance</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Planning</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">Salary</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Saving</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">Spreadsheet</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Stock</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Strategy</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Wealth Management</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 { HowDoIGetMyFinancesInOrderRoutingModule } from './how-do-i-get-my-finances-in-order-page-routing.module';
|
||||
import { HowDoIGetMyFinancesInOrderPageComponent } from './how-do-i-get-my-finances-in-order-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [HowDoIGetMyFinancesInOrderPageComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
HowDoIGetMyFinancesInOrderRoutingModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class HowDoIGetMyFinancesInOrderPageModule {}
|
@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
@ -2,10 +2,62 @@
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center" i18n>Blog</h3>
|
||||
<mat-card class="blog-container">
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap mb-3 no-gutters row">
|
||||
<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-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex w-100"
|
||||
[routerLink]="['/en', 'blog', '2022', '07', 'how-do-i-get-my-finances-in-order']"
|
||||
>
|
||||
<div class="flex-grow-1">
|
||||
<div class="h6 m-0 text-truncate">
|
||||
How do I get my finances in order?
|
||||
</div>
|
||||
<div class="d-flex text-muted">2022-07-14</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-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex w-100"
|
||||
[routerLink]="['/en', 'blog', '2022', '01', 'ghostfolio-first-months-in-open-source']"
|
||||
@ -14,7 +66,7 @@
|
||||
<div class="h6 m-0 text-truncate">
|
||||
First months in Open Source
|
||||
</div>
|
||||
<div class="d-flex text-muted">05.01.2022</div>
|
||||
<div class="d-flex text-muted">2022-01-05</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
@ -25,14 +77,20 @@
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-nowrap mb-3 no-gutters row">
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<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', '2021', '07', 'hello-ghostfolio']"
|
||||
>
|
||||
<div class="flex-grow-1">
|
||||
<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 class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
@ -43,6 +101,12 @@
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<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"
|
||||
@ -50,7 +114,7 @@
|
||||
>
|
||||
<div class="flex-grow-1">
|
||||
<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 class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
|
@ -4,6 +4,7 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/imperso
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { Position, User } from '@ghostfolio/common/interfaces';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import { GroupBy, ToggleOption } from '@ghostfolio/common/types';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { sortBy } from 'lodash';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
@ -22,6 +23,12 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
public deviceType: string;
|
||||
public hasImpersonationId: boolean;
|
||||
public investments: InvestmentItem[];
|
||||
public investmentsByMonth: InvestmentItem[];
|
||||
public mode: GroupBy;
|
||||
public modeOptions: ToggleOption[] = [
|
||||
{ label: 'Monthly', value: 'month' },
|
||||
{ label: 'Accumulating', value: undefined }
|
||||
];
|
||||
public top3: Position[];
|
||||
public user: User;
|
||||
|
||||
@ -55,6 +62,15 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.dataService
|
||||
.fetchInvestmentsByMonth()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ investments }) => {
|
||||
this.investmentsByMonth = investments;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.dataService
|
||||
.fetchPositions({ range: 'max' })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
@ -86,6 +102,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public onChangeGroupBy(aMode: GroupBy) {
|
||||
this.mode = aMode;
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
|
@ -1,17 +1,47 @@
|
||||
<div class="container">
|
||||
<div class="investment-chart row">
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3>
|
||||
<div class="mb-3">
|
||||
<div class="h5 mb-3" i18n>Investment Timeline</div>
|
||||
<gf-investment-chart
|
||||
class="h-100"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[daysInMarket]="daysInMarket"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[investments]="investments"
|
||||
[locale]="user?.settings?.locale"
|
||||
></gf-investment-chart>
|
||||
<div class="mb-4">
|
||||
<div class="align-items-center d-flex mb-4">
|
||||
<div
|
||||
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>
|
||||
<gf-toggle
|
||||
class="d-none d-lg-block"
|
||||
[defaultValue]="mode"
|
||||
[isLoading]="false"
|
||||
[options]="modeOptions"
|
||||
(change)="onChangeGroupBy($event.value)"
|
||||
></gf-toggle>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<gf-investment-chart
|
||||
class="h-100"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[daysInMarket]="daysInMarket"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[investments]="investments"
|
||||
[locale]="user?.settings?.locale"
|
||||
[ngClass]="{ 'd-none': mode }"
|
||||
></gf-investment-chart>
|
||||
<gf-investment-chart
|
||||
class="h-100"
|
||||
groupBy="month"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[daysInMarket]="daysInMarket"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[investments]="investmentsByMonth"
|
||||
[locale]="user?.settings?.locale"
|
||||
[ngClass]="{ 'd-none': !mode }"
|
||||
></gf-investment-chart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,6 +2,8 @@ import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
|
||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
@ -14,6 +16,8 @@ import { AnalysisPageComponent } from './analysis-page.component';
|
||||
AnalysisPageRoutingModule,
|
||||
CommonModule,
|
||||
GfInvestmentChartModule,
|
||||
GfPremiumIndicatorModule,
|
||||
GfToggleModule,
|
||||
GfValueModule,
|
||||
MatCardModule,
|
||||
NgxSkeletonLoaderModule
|
||||
|
@ -1,11 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.investment-chart {
|
||||
.mat-card {
|
||||
.mat-card-content {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
}
|
||||
.chart-container {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
}
|
||||
|
@ -15,10 +15,14 @@
|
||||
<gf-positions-table
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positionsArray"
|
||||
></gf-positions-table>
|
||||
<div *ngIf="hasPermissionToCreateOrder" class="text-center">
|
||||
<div
|
||||
*ngIf="hasPermissionToCreateOrder && positionsArray?.length > 0"
|
||||
class="text-center"
|
||||
>
|
||||
<a
|
||||
class="mt-3"
|
||||
i18n
|
||||
|
@ -255,6 +255,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
||||
isUUID(this.activityForm.controls['searchSymbol'].value.symbol)
|
||||
? this.activityForm.controls['name'].value
|
||||
: this.activityForm.controls['searchSymbol'].value.symbol,
|
||||
tags: this.activityForm.controls['tags'].value,
|
||||
type: this.activityForm.controls['type'].value,
|
||||
unitPrice: this.activityForm.controls['unitPrice'].value
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
<gf-dialog-header
|
||||
mat-dialog-title
|
||||
title="Import Transactions Error"
|
||||
title="Import Activities Error"
|
||||
[deviceType]="data.deviceType"
|
||||
(closeButtonClicked)="onCancel()"
|
||||
></gf-dialog-header>
|
||||
|
@ -2,6 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
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 { InfoItem } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
@ -34,6 +35,7 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private dialog: MatDialog,
|
||||
private internetIdentityService: InternetIdentityService,
|
||||
private router: Router,
|
||||
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(
|
||||
accessToken: string,
|
||||
authToken: string,
|
||||
|
@ -28,16 +28,25 @@
|
||||
Create Account
|
||||
</button>
|
||||
<ng-container *ngIf="hasPermissionForSocialLogin">
|
||||
<div
|
||||
class="m-3 text-muted"
|
||||
i18n
|
||||
[ngClass]="{'d-inline': deviceType !== 'mobile' }"
|
||||
<div class="my-3 text-muted" i18n>or</div>
|
||||
<button
|
||||
class="d-block mb-2"
|
||||
mat-stroked-button
|
||||
(click)="onLoginWithInternetIdentity()"
|
||||
>
|
||||
or
|
||||
</div>
|
||||
<a color="accent" href="/api/v1/auth/google" mat-flat-button
|
||||
><ion-icon class="mr-1" name="logo-google"></ion-icon
|
||||
><span i18n>Continue with Google</span></a
|
||||
<img
|
||||
class="mr-2"
|
||||
src="./assets/icons/internet-computer.svg"
|
||||
style="height: 0.75rem"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
|
@ -20,6 +20,22 @@
|
||||
</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>
|
||||
<h2 class="h4 mb-3">Market</h2>
|
||||
<div class="mb-5">
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
Export,
|
||||
Filter,
|
||||
InfoItem,
|
||||
OAuthResponse,
|
||||
PortfolioChart,
|
||||
PortfolioDetails,
|
||||
PortfolioInvestments,
|
||||
@ -204,6 +205,22 @@ export class DataService {
|
||||
);
|
||||
}
|
||||
|
||||
public fetchInvestmentsByMonth(): Observable<PortfolioInvestments> {
|
||||
return this.http
|
||||
.get<any>('/api/v1/portfolio/investments', {
|
||||
params: { groupBy: 'month' }
|
||||
})
|
||||
.pipe(
|
||||
map((response) => {
|
||||
if (response.firstOrderDate) {
|
||||
response.firstOrderDate = parseISO(response.firstOrderDate);
|
||||
}
|
||||
|
||||
return response;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public fetchSymbolItem({
|
||||
dataSource,
|
||||
includeHistoricalData,
|
||||
@ -352,7 +369,9 @@ export class DataService {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
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,54 +6,62 @@
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
<url>
|
||||
<loc>https://ghostfol.io</loc>
|
||||
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-07-23T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/about</loc>
|
||||
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-07-23T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/about/changelog</loc>
|
||||
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-07-23T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/blog</loc>
|
||||
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-07-23T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
|
||||
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-07-23T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/demo</loc>
|
||||
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-07-23T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
|
||||
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-07-23T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
|
||||
<lastmod>2022-07-01T00: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>
|
||||
<loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc>
|
||||
<lastmod>2022-07-23T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/features</loc>
|
||||
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-07-23T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/markets</loc>
|
||||
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-07-23T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/pricing</loc>
|
||||
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-07-23T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/register</loc>
|
||||
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-07-23T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/resources</loc>
|
||||
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-07-23T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
</urlset>
|
||||
|
@ -43,7 +43,7 @@ export function getTooltipPositionerMapTop(
|
||||
chart: Chart,
|
||||
position: TooltipPosition
|
||||
) {
|
||||
if (!position) {
|
||||
if (!position || !chart?.chartArea) {
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
|
@ -5,7 +5,6 @@ export interface AdminData {
|
||||
userCount: number;
|
||||
users: {
|
||||
accountCount: number;
|
||||
alias: string;
|
||||
createdAt: Date;
|
||||
engagement: number;
|
||||
id: string;
|
||||
|
@ -29,6 +29,7 @@ import { PortfolioSummary } from './portfolio-summary.interface';
|
||||
import { Position } from './position.interface';
|
||||
import { BenchmarkResponse } from './responses/benchmark-response.interface';
|
||||
import { ResponseError } from './responses/errors.interface';
|
||||
import { OAuthResponse } from './responses/oauth-response.interface';
|
||||
import { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface';
|
||||
import { ScraperConfiguration } from './scraper-configuration.interface';
|
||||
import { TimelinePosition } from './timeline-position.interface';
|
||||
@ -54,6 +55,7 @@ export {
|
||||
FilterGroup,
|
||||
HistoricalDataItem,
|
||||
InfoItem,
|
||||
OAuthResponse,
|
||||
PortfolioChart,
|
||||
PortfolioDetails,
|
||||
PortfolioInvestments,
|
||||
|
@ -0,0 +1,3 @@
|
||||
export interface OAuthResponse {
|
||||
authToken: string;
|
||||
}
|
@ -8,7 +8,6 @@ export interface User {
|
||||
id: string;
|
||||
}[];
|
||||
accounts: Account[];
|
||||
alias?: string;
|
||||
id: string;
|
||||
permissions: string[];
|
||||
settings: UserSettings;
|
||||
|
1
libs/common/src/lib/types/group-by.type.ts
Normal file
1
libs/common/src/lib/types/group-by.type.ts
Normal file
@ -0,0 +1 @@
|
||||
export type GroupBy = 'month';
|
@ -2,7 +2,8 @@ import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
|
||||
import { AccountWithValue } from './account-with-value.type';
|
||||
import type { DateRange } from './date-range.type';
|
||||
import type { Granularity } from './granularity.type';
|
||||
import { MarketState } from './market-state-type';
|
||||
import { GroupBy } from './group-by.type';
|
||||
import { MarketState } from './market-state.type';
|
||||
import { Market } from './market.type';
|
||||
import type { OrderWithAccount } from './order-with-account.type';
|
||||
import type { RequestWithUser } from './request-with-user.type';
|
||||
@ -13,6 +14,7 @@ export type {
|
||||
AccountWithValue,
|
||||
DateRange,
|
||||
Granularity,
|
||||
GroupBy,
|
||||
Market,
|
||||
MarketState,
|
||||
OrderWithAccount,
|
||||
|
@ -8,6 +8,5 @@
|
||||
></ngx-skeleton-loader>
|
||||
<canvas
|
||||
#chartCanvas
|
||||
class="h-100"
|
||||
[ngStyle]="{ display: isLoading ? 'none' : 'block' }"
|
||||
></canvas>
|
||||
|
@ -175,6 +175,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
|
||||
data,
|
||||
options: {
|
||||
animation: false,
|
||||
aspectRatio: 16 / 9,
|
||||
elements: {
|
||||
point: {
|
||||
hoverBackgroundColor: getBackgroundColor(),
|
||||
|
@ -1,5 +1,6 @@
|
||||
<a
|
||||
class="align-items-center d-flex"
|
||||
title="Upgrade to Ghostfolio Premium"
|
||||
[ngStyle]="{ 'pointer-events': enableLink ? 'initial' : 'none' }"
|
||||
[routerLink]="['/pricing']"
|
||||
><ion-icon class="text-muted" name="diamond-outline"></ion-icon
|
||||
|
15
package.json
15
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "1.167.0",
|
||||
"version": "1.173.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -20,6 +20,7 @@
|
||||
"database:format-schema": "prisma format",
|
||||
"database:generate-typings": "prisma generate",
|
||||
"database:gui": "prisma studio",
|
||||
"database:gui:prod": "npx dotenv-cli -e .env.prod -- prisma studio",
|
||||
"database:migrate": "prisma migrate deploy",
|
||||
"database:push": "prisma db push",
|
||||
"database:seed": "prisma db seed",
|
||||
@ -42,7 +43,7 @@
|
||||
"start:server": "nx serve api --watch",
|
||||
"start:storybook": "nx run ui:storybook",
|
||||
"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",
|
||||
"update": "nx migrate latest",
|
||||
"watch:server": "nx build api --watch",
|
||||
@ -61,6 +62,12 @@
|
||||
"@angular/platform-browser-dynamic": "14.0.2",
|
||||
"@angular/router": "14.0.2",
|
||||
"@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",
|
||||
"@nestjs/bull": "0.5.5",
|
||||
"@nestjs/common": "8.4.7",
|
||||
@ -93,7 +100,7 @@
|
||||
"color": "4.0.1",
|
||||
"countries-list": "2.6.1",
|
||||
"countup.js": "2.0.7",
|
||||
"date-fns": "2.22.1",
|
||||
"date-fns": "2.28.0",
|
||||
"envalid": "7.3.1",
|
||||
"google-spreadsheet": "3.2.0",
|
||||
"http-status-codes": "2.2.0",
|
||||
@ -171,9 +178,9 @@
|
||||
"prettier": "2.7.1",
|
||||
"replace-in-file": "6.2.0",
|
||||
"rimraf": "3.0.2",
|
||||
"tslib": "2.0.0",
|
||||
"ts-jest": "27.1.4",
|
||||
"ts-node": "10.8.1",
|
||||
"tslib": "2.0.0",
|
||||
"typescript": "4.7.3"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "Provider" ADD VALUE 'INTERNET_IDENTITY';
|
@ -217,6 +217,7 @@ enum ViewMode {
|
||||
enum Provider {
|
||||
ANONYMOUS
|
||||
GOOGLE
|
||||
INTERNET_IDENTITY
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
126
yarn.lock
126
yarn.lock
@ -1769,6 +1769,48 @@
|
||||
debug "^3.1.0"
|
||||
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":
|
||||
version "2.0.0-alpha.8"
|
||||
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"
|
||||
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":
|
||||
version "1.17.0"
|
||||
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"
|
||||
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:
|
||||
version "1.5.1"
|
||||
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"
|
||||
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:
|
||||
version "5.1.2"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||
@ -7710,10 +7775,10 @@ data-urls@^2.0.0:
|
||||
whatwg-mimetype "^2.3.0"
|
||||
whatwg-url "^8.0.0"
|
||||
|
||||
date-fns@2.22.1:
|
||||
version "2.22.1"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.22.1.tgz#1e5af959831ebb1d82992bf67b765052d8f0efc4"
|
||||
integrity sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg==
|
||||
date-fns@2.28.0:
|
||||
version "2.28.0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
|
||||
integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
|
||||
|
||||
date-fns@^1.27.2:
|
||||
version "1.30.1"
|
||||
@ -7856,6 +7921,11 @@ delegates@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||
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:
|
||||
version "1.5.1"
|
||||
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"
|
||||
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:
|
||||
version "2.1.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
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"
|
||||
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:
|
||||
version "2.2.1"
|
||||
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"
|
||||
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:
|
||||
version "3.2.1"
|
||||
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"
|
||||
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:
|
||||
version "4.4.0"
|
||||
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-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:
|
||||
version "3.0.1"
|
||||
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"
|
||||
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:
|
||||
version "0.2.2"
|
||||
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"
|
||||
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:
|
||||
version "1.10.3"
|
||||
resolved "https://registry.yarnpkg.com/twitter-api-v2/-/twitter-api-v2-1.10.3.tgz#07441bd9c4d27433aa0284d900cf60f6328b8239"
|
||||
|
Reference in New Issue
Block a user