Browse Source

Merge branch 'main' into feature/remove-account-type-from-user-interface

pull/2335/head
Thomas Kaul 2 years ago
committed by GitHub
parent
commit
829f1e9f1a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      CHANGELOG.md
  2. 8
      apps/api/src/app/admin/admin.service.ts
  3. 43
      apps/api/src/app/info/info.service.ts
  4. 11
      apps/api/src/app/logo/logo.service.ts
  5. 7
      apps/api/src/app/user/user.service.ts
  6. 53
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  7. 49
      apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts
  8. 33
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  9. 41
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  10. 13
      apps/api/src/services/data-provider/manual/manual.service.ts
  11. 15
      apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
  12. 12
      apps/client/src/app/components/access-table/access-table.component.html
  13. 1
      apps/client/src/app/components/access-table/access-table.component.ts
  14. 9
      apps/client/src/app/components/access-table/access-table.module.ts
  15. 7
      apps/client/src/app/pages/pricing/pricing-page.component.ts
  16. 2
      apps/client/src/app/pages/pricing/pricing-page.html
  17. 31
      apps/client/src/app/pages/user-account/user-account-page.html
  18. 7
      apps/client/src/app/pages/user-account/user-account-page.scss
  19. 2
      libs/common/src/lib/config.ts

6
CHANGELOG.md

@ -10,17 +10,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added support to drop a file in the import activities dialog - Added support to drop a file in the import activities dialog
- Added a timeout to all data source requests
### Changed ### Changed
- Harmonized the style of the user interface for granting and revoking public access to share the portfolio
- Removed the account type from the user interface as a preparation to remove it from the `Account` database schema - Removed the account type from the user interface as a preparation to remove it from the `Account` database schema
- Improved the logger output of the info service - Improved the logger output of the info service
- Harmonized the logger output: <symbol> (<dataSource>) - Harmonized the logger output: <symbol> (<dataSource>)
- Improved the language localization for Italian (`it`) - Improved the language localization for Italian (`it`)
- Improved the language localization for Dutch (`nl`) - Improved the language localization for Dutch (`nl`)
- Improved the read-only mode
### Fixed ### Fixed
- Fixed the timeout in _EOD Historical Data_ requests
- Fixed an issue with the portfolio summary caused by the language localization for Dutch (`nl`) - Fixed an issue with the portfolio summary caused by the language localization for Dutch (`nl`)
## 2.0.0 - 2023-09-09 ## 2.0.0 - 2023-09-09
@ -2950,7 +2954,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Supported the management of additional currencies in the admin control panel - Supported the management of additional currencies in the admin control panel
- Introduced the system message - Introduced the system message
- Introduced the read only mode - Introduced the read-only mode
### Changed ### Changed

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

@ -8,7 +8,9 @@ import { PropertyService } from '@ghostfolio/api/services/property/property.serv
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
PROPERTY_CURRENCIES PROPERTY_CURRENCIES,
PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_IS_USER_SIGNUP_ENABLED
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { import {
AdminData, AdminData,
@ -305,7 +307,9 @@ export class AdminService {
response = await this.propertyService.delete({ key }); response = await this.propertyService.delete({ key });
} }
if (key === PROPERTY_CURRENCIES) { if (key === PROPERTY_IS_READ_ONLY_MODE && value === 'true') {
await this.putSetting(PROPERTY_IS_USER_SIGNUP_ENABLED, 'false');
} else if (key === PROPERTY_CURRENCIES) {
await this.exchangeRateDataService.initialize(); await this.exchangeRateDataService.initialize();
} }

43
apps/api/src/app/info/info.service.ts

@ -8,6 +8,7 @@ import { PropertyService } from '@ghostfolio/api/services/property/property.serv
import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
DEFAULT_REQUEST_TIMEOUT,
PROPERTY_BETTER_UPTIME_MONITOR_ID, PROPERTY_BETTER_UPTIME_MONITOR_ID,
PROPERTY_COUNTRIES_OF_SUBSCRIBERS, PROPERTY_COUNTRIES_OF_SUBSCRIBERS,
PROPERTY_DEMO_USER_ID, PROPERTY_DEMO_USER_ID,
@ -168,10 +169,18 @@ export class InfoService {
private async countDockerHubPulls(): Promise<number> { private async countDockerHubPulls(): Promise<number> {
try { try {
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const { pull_count } = await got( const { pull_count } = await got(
`https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`, `https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`,
{ {
headers: { 'User-Agent': 'request' } headers: { 'User-Agent': 'request' },
// @ts-ignore
signal: abortController.signal
} }
).json<any>(); ).json<any>();
@ -185,7 +194,16 @@ export class InfoService {
private async countGitHubContributors(): Promise<number> { private async countGitHubContributors(): Promise<number> {
try { try {
const { body } = await got('https://github.com/ghostfolio/ghostfolio'); const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const { body } = await got('https://github.com/ghostfolio/ghostfolio', {
// @ts-ignore
signal: abortController.signal
});
const $ = cheerio.load(body); const $ = cheerio.load(body);
@ -203,10 +221,18 @@ export class InfoService {
private async countGitHubStargazers(): Promise<number> { private async countGitHubStargazers(): Promise<number> {
try { try {
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const { stargazers_count } = await got( const { stargazers_count } = await got(
`https://api.github.com/repos/ghostfolio/ghostfolio`, `https://api.github.com/repos/ghostfolio/ghostfolio`,
{ {
headers: { 'User-Agent': 'request' } headers: { 'User-Agent': 'request' },
// @ts-ignore
signal: abortController.signal
} }
).json<any>(); ).json<any>();
@ -323,18 +349,25 @@ export class InfoService {
PROPERTY_BETTER_UPTIME_MONITOR_ID PROPERTY_BETTER_UPTIME_MONITOR_ID
)) as string; )) as string;
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const { data } = await got( const { data } = await got(
`https://uptime.betterstack.com/api/v2/monitors/${monitorId}/sla?from=${format( `https://uptime.betterstack.com/api/v2/monitors/${monitorId}/sla?from=${format(
subDays(new Date(), 90), subDays(new Date(), 90),
DATE_FORMAT DATE_FORMAT
)}&to${format(new Date(), DATE_FORMAT)}`, )}&to${format(new Date(), DATE_FORMAT)}`,
{ {
headers: { headers: {
Authorization: `Bearer ${this.configurationService.get( Authorization: `Bearer ${this.configurationService.get(
'BETTER_UPTIME_API_KEY' 'BETTER_UPTIME_API_KEY'
)}` )}`
} },
// @ts-ignore
signal: abortController.signal
} }
).json<any>(); ).json<any>();

11
apps/api/src/app/logo/logo.service.ts

@ -1,4 +1,5 @@
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { HttpException, Injectable } from '@nestjs/common'; import { HttpException, Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
@ -41,10 +42,18 @@ export class LogoService {
} }
private getBuffer(aUrl: string) { private getBuffer(aUrl: string) {
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
return got( return got(
`https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${aUrl}&size=64`, `https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${aUrl}&size=64`,
{ {
headers: { 'User-Agent': 'request' } headers: { 'User-Agent': 'request' },
// @ts-ignore
signal: abortController.signal
} }
).buffer(); ).buffer();
} }

7
apps/api/src/app/user/user.service.ts

@ -19,7 +19,7 @@ import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Prisma, Role, User } from '@prisma/client'; import { Prisma, Role, User } from '@prisma/client';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';
import { sortBy } from 'lodash'; import { sortBy, without } from 'lodash';
const crypto = require('crypto'); const crypto = require('crypto');
@ -188,6 +188,11 @@ export class UserService {
currentPermissions.push(permissions.enableSubscriptionInterstitial); currentPermissions.push(permissions.enableSubscriptionInterstitial);
} }
currentPermissions = without(
currentPermissions,
permissions.createAccess
);
// Reset benchmark // Reset benchmark
user.Settings.settings.benchmark = undefined; user.Settings.settings.benchmark = undefined;
} }

53
apps/api/src/services/data-provider/coingecko/coingecko.service.ts

@ -4,7 +4,10 @@ import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import {
DEFAULT_CURRENCY,
DEFAULT_REQUEST_TIMEOUT
} from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces'; import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
@ -40,7 +43,16 @@ export class CoinGeckoService implements DataProviderInterface {
}; };
try { try {
const { name } = await got(`${this.URL}/coins/${aSymbol}`).json<any>(); const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const { name } = await got(`${this.URL}/coins/${aSymbol}`, {
// @ts-ignore
signal: abortController.signal
}).json<any>();
response.name = name; response.name = name;
} catch (error) { } catch (error) {
@ -73,12 +85,22 @@ export class CoinGeckoService implements DataProviderInterface {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
}> { }> {
try { try {
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const { prices } = await got( const { prices } = await got(
`${ `${
this.URL this.URL
}/coins/${aSymbol}/market_chart/range?vs_currency=${DEFAULT_CURRENCY.toLowerCase()}&from=${getUnixTime( }/coins/${aSymbol}/market_chart/range?vs_currency=${DEFAULT_CURRENCY.toLowerCase()}&from=${getUnixTime(
from from
)}&to=${getUnixTime(to)}` )}&to=${getUnixTime(to)}`,
{
// @ts-ignore
signal: abortController.signal
}
).json<any>(); ).json<any>();
const result: { const result: {
@ -122,10 +144,20 @@ export class CoinGeckoService implements DataProviderInterface {
} }
try { try {
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const response = await got( const response = await got(
`${this.URL}/simple/price?ids=${aSymbols.join( `${this.URL}/simple/price?ids=${aSymbols.join(
',' ','
)}&vs_currencies=${DEFAULT_CURRENCY.toLowerCase()}` )}&vs_currencies=${DEFAULT_CURRENCY.toLowerCase()}`,
{
// @ts-ignore
signal: abortController.signal
}
).json<any>(); ).json<any>();
for (const symbol in response) { for (const symbol in response) {
@ -160,9 +192,16 @@ export class CoinGeckoService implements DataProviderInterface {
let items: LookupItem[] = []; let items: LookupItem[] = [];
try { try {
const { coins } = await got( const abortController = new AbortController();
`${this.URL}/search?query=${query}`
).json<any>(); setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const { coins } = await got(`${this.URL}/search?query=${query}`, {
// @ts-ignore
signal: abortController.signal
}).json<any>();
items = coins.map(({ id: symbol, name }) => { items = coins.map(({ id: symbol, name }) => {
return { return {

49
apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts

@ -1,4 +1,5 @@
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
import { Country } from '@ghostfolio/common/interfaces/country.interface'; import { Country } from '@ghostfolio/common/interfaces/country.interface';
import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@ -32,15 +33,35 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
return response; return response;
} }
let abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const profile = await got( const profile = await got(
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol}.json` `${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol}.json`,
{
// @ts-ignore
signal: abortController.signal
}
) )
.json<any>() .json<any>()
.catch(() => { .catch(() => {
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
return got( return got(
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol.split( `${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol.split(
'.' '.'
)?.[0]}.json` )?.[0]}.json`,
{
// @ts-ignore
signal: abortController.signal
}
) )
.json<any>() .json<any>()
.catch(() => { .catch(() => {
@ -54,15 +75,35 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
response.isin = isin; response.isin = isin;
} }
abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const holdings = await got( const holdings = await got(
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json` `${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json`,
{
// @ts-ignore
signal: abortController.signal
}
) )
.json<any>() .json<any>()
.catch(() => { .catch(() => {
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
return got( return got(
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol.split( `${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol.split(
'.' '.'
)?.[0]}.json` )?.[0]}.json`,
{
// @ts-ignore
signal: abortController.signal
}
) )
.json<any>() .json<any>()
.catch(() => { .catch(() => {

33
apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts

@ -78,6 +78,12 @@ export class EodHistoricalDataService implements DataProviderInterface {
const symbol = this.convertToEodSymbol(aSymbol); const symbol = this.convertToEodSymbol(aSymbol);
try { try {
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const response = await got( const response = await got(
`${this.URL}/eod/${symbol}?api_token=${ `${this.URL}/eod/${symbol}?api_token=${
this.apiKey this.apiKey
@ -86,9 +92,8 @@ export class EodHistoricalDataService implements DataProviderInterface {
DATE_FORMAT DATE_FORMAT
)}&period={aGranularity}`, )}&period={aGranularity}`,
{ {
timeout: { // @ts-ignore
request: DEFAULT_REQUEST_TIMEOUT signal: abortController.signal
}
} }
).json<any>(); ).json<any>();
@ -138,14 +143,19 @@ export class EodHistoricalDataService implements DataProviderInterface {
} }
try { try {
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const realTimeResponse = await got( const realTimeResponse = await got(
`${this.URL}/real-time/${symbols[0]}?api_token=${ `${this.URL}/real-time/${symbols[0]}?api_token=${
this.apiKey this.apiKey
}&fmt=json&s=${symbols.join(',')}`, }&fmt=json&s=${symbols.join(',')}`,
{ {
timeout: { // @ts-ignore
request: DEFAULT_REQUEST_TIMEOUT signal: abortController.signal
}
} }
).json<any>(); ).json<any>();
@ -331,12 +341,17 @@ export class EodHistoricalDataService implements DataProviderInterface {
let searchResult = []; let searchResult = [];
try { try {
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const response = await got( const response = await got(
`${this.URL}/search/${aQuery}?api_token=${this.apiKey}`, `${this.URL}/search/${aQuery}?api_token=${this.apiKey}`,
{ {
timeout: { // @ts-ignore
request: DEFAULT_REQUEST_TIMEOUT signal: abortController.signal
}
} }
).json<any>(); ).json<any>();

41
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -5,7 +5,10 @@ import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import {
DEFAULT_CURRENCY,
DEFAULT_REQUEST_TIMEOUT
} from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces'; import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
@ -63,8 +66,18 @@ export class FinancialModelingPrepService implements DataProviderInterface {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
}> { }> {
try { try {
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const { historical } = await got( const { historical } = await got(
`${this.URL}/historical-price-full/${aSymbol}?apikey=${this.apiKey}` `${this.URL}/historical-price-full/${aSymbol}?apikey=${this.apiKey}`,
{
// @ts-ignore
signal: abortController.signal
}
).json<any>(); ).json<any>();
const result: { const result: {
@ -110,8 +123,18 @@ export class FinancialModelingPrepService implements DataProviderInterface {
} }
try { try {
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const response = await got( const response = await got(
`${this.URL}/quote/${aSymbols.join(',')}?apikey=${this.apiKey}` `${this.URL}/quote/${aSymbols.join(',')}?apikey=${this.apiKey}`,
{
// @ts-ignore
signal: abortController.signal
}
).json<any>(); ).json<any>();
for (const { price, symbol } of response) { for (const { price, symbol } of response) {
@ -144,8 +167,18 @@ export class FinancialModelingPrepService implements DataProviderInterface {
let items: LookupItem[] = []; let items: LookupItem[] = [];
try { try {
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const result = await got( const result = await got(
`${this.URL}/search?query=${query}&apikey=${this.apiKey}` `${this.URL}/search?query=${query}&apikey=${this.apiKey}`,
{
// @ts-ignore
signal: abortController.signal
}
).json<any>(); ).json<any>();
items = result.map(({ currency, name, symbol }) => { items = result.map(({ currency, name, symbol }) => {

13
apps/api/src/services/data-provider/manual/manual.service.ts

@ -6,6 +6,7 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
import { import {
DATE_FORMAT, DATE_FORMAT,
extractNumberFromString, extractNumberFromString,
@ -95,7 +96,17 @@ export class ManualService implements DataProviderInterface {
return {}; return {};
} }
const { body } = await got(url, { headers }); const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const { body } = await got(url, {
headers,
// @ts-ignore
signal: abortController.signal
});
const $ = cheerio.load(body); const $ = cheerio.load(body);

15
apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts

@ -5,7 +5,10 @@ import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config'; import {
DEFAULT_REQUEST_TIMEOUT,
ghostfolioFearAndGreedIndexSymbol
} from '@ghostfolio/common/config';
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper'; import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
@ -135,6 +138,12 @@ export class RapidApiService implements DataProviderInterface {
oneYearAgo: { value: number; valueText: string }; oneYearAgo: { value: number; valueText: string };
}> { }> {
try { try {
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const { fgi } = await got( const { fgi } = await got(
`https://fear-and-greed-index.p.rapidapi.com/v1/fgi`, `https://fear-and-greed-index.p.rapidapi.com/v1/fgi`,
{ {
@ -142,7 +151,9 @@ export class RapidApiService implements DataProviderInterface {
useQueryString: 'true', useQueryString: 'true',
'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com', 'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com',
'x-rapidapi-key': this.configurationService.get('RAPID_API_API_KEY') 'x-rapidapi-key': this.configurationService.get('RAPID_API_API_KEY')
} },
// @ts-ignore
signal: abortController.signal
} }
).json<any>(); ).json<any>();

12
apps/client/src/app/components/access-table/access-table.component.html

@ -1,3 +1,15 @@
<div *ngIf="hasPermissionToCreateAccess" class="d-flex justify-content-end">
<a
color="primary"
i18n
mat-flat-button
[queryParams]="{ createDialog: true }"
[routerLink]="[]"
>
Add Access
</a>
</div>
<table class="gf-table w-100" mat-table [dataSource]="dataSource"> <table class="gf-table w-100" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="alias"> <ng-container matColumnDef="alias">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Alias</th> <th *matHeaderCellDef class="px-1" i18n mat-header-cell>Alias</th>

1
apps/client/src/app/components/access-table/access-table.component.ts

@ -19,6 +19,7 @@ import { Access } from '@ghostfolio/common/interfaces';
}) })
export class AccessTableComponent implements OnChanges, OnInit { export class AccessTableComponent implements OnChanges, OnInit {
@Input() accesses: Access[]; @Input() accesses: Access[];
@Input() hasPermissionToCreateAccess = false;
@Input() showActions: boolean; @Input() showActions: boolean;
@Output() accessDeleted = new EventEmitter<string>(); @Output() accessDeleted = new EventEmitter<string>();

9
apps/client/src/app/components/access-table/access-table.module.ts

@ -3,13 +3,20 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router';
import { AccessTableComponent } from './access-table.component'; import { AccessTableComponent } from './access-table.component';
@NgModule({ @NgModule({
declarations: [AccessTableComponent], declarations: [AccessTableComponent],
exports: [AccessTableComponent], exports: [AccessTableComponent],
imports: [CommonModule, MatButtonModule, MatMenuModule, MatTableModule], imports: [
CommonModule,
MatButtonModule,
MatMenuModule,
MatTableModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfPortfolioAccessTableModule {} export class GfPortfolioAccessTableModule {}

7
apps/client/src/app/pages/pricing/pricing-page.component.ts

@ -2,6 +2,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { StripeService } from 'ngx-stripe'; import { StripeService } from 'ngx-stripe';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -17,6 +18,7 @@ export class PricingPageComponent implements OnDestroy, OnInit {
public baseCurrency: string; public baseCurrency: string;
public coupon: number; public coupon: number;
public couponId: string; public couponId: string;
public hasPermissionToUpdateUserSettings: boolean;
public importAndExportTooltipBasic = translate( public importAndExportTooltipBasic = translate(
'DATA_IMPORT_AND_EXPORT_TOOLTIP_BASIC' 'DATA_IMPORT_AND_EXPORT_TOOLTIP_BASIC'
); );
@ -55,6 +57,11 @@ export class PricingPageComponent implements OnDestroy, OnInit {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
this.hasPermissionToUpdateUserSettings = hasPermission(
this.user.permissions,
permissions.updateUserSettings
);
this.coupon = subscriptions?.[this.user?.subscription?.offer]?.coupon; this.coupon = subscriptions?.[this.user?.subscription?.offer]?.coupon;
this.couponId = this.couponId =
subscriptions?.[this.user.subscription.offer]?.couponId; subscriptions?.[this.user.subscription.offer]?.couponId;

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

@ -333,7 +333,7 @@
> >
</p> </p>
<div <div
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="hasPermissionToUpdateUserSettings && user?.subscription?.type === 'Basic'"
class="mt-3 text-center" class="mt-3 text-center"
> >
<button color="primary" mat-flat-button (click)="onCheckout()"> <button color="primary" mat-flat-button (click)="onCheckout()">

31
apps/client/src/app/pages/user-account/user-account-page.html

@ -8,10 +8,7 @@
<div class="col"> <div class="col">
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-content> <mat-card-content>
<div <div *ngIf="user?.subscription" class="d-flex py-1">
*ngIf="hasPermissionToUpdateUserSettings && user?.subscription"
class="d-flex py-1"
>
<div class="pr-1 w-50" i18n>Membership</div> <div class="pr-1 w-50" i18n>Membership</div>
<div class="pl-1 w-50"> <div class="pl-1 w-50">
<div class="align-items-center d-flex mb-1"> <div class="align-items-center d-flex mb-1">
@ -28,7 +25,9 @@
user?.subscription?.expiresAt | date: defaultDateFormat }} user?.subscription?.expiresAt | date: defaultDateFormat }}
</div> </div>
<div *ngIf="user?.subscription?.type === 'Basic'"> <div *ngIf="user?.subscription?.type === 'Basic'">
<ng-container *ngIf="hasPermissionForSubscription"> <ng-container
*ngIf="hasPermissionForSubscription && hasPermissionToUpdateUserSettings"
>
<button <button
color="primary" color="primary"
mat-flat-button mat-flat-button
@ -69,6 +68,7 @@
></gf-premium-indicator ></gf-premium-indicator
></a> ></a>
<a <a
*ngIf="hasPermissionToUpdateUserSettings"
class="mr-2 my-2" class="mr-2 my-2"
i18n i18n
mat-stroked-button mat-stroked-button
@ -287,24 +287,19 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h2 class="h3 mb-3 text-center" i18n>Granted Access</h2> <h2 class="align-items-center d-flex h3 justify-content-center mb-3">
<span i18n>Granted Access</span>
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
></gf-premium-indicator>
</h2>
<gf-access-table <gf-access-table
[accesses]="accesses" [accesses]="accesses"
[hasPermissionToCreateAccess]="hasPermissionToCreateAccess"
[showActions]="hasPermissionToDeleteAccess" [showActions]="hasPermissionToDeleteAccess"
(accessDeleted)="onDeleteAccess($event)" (accessDeleted)="onDeleteAccess($event)"
></gf-access-table> ></gf-access-table>
</div> </div>
</div> </div>
<div *ngIf="hasPermissionToCreateAccess" class="fab-container">
<a
class="align-items-center d-flex justify-content-center"
color="primary"
mat-fab
[queryParams]="{ createDialog: true }"
[routerLink]="[]"
>
<ion-icon name="add-outline" size="large"></ion-icon>
</a>
</div>
</div> </div>

7
apps/client/src/app/pages/user-account/user-account-page.scss

@ -6,13 +6,6 @@
overflow-x: auto; overflow-x: auto;
} }
.fab-container {
position: fixed;
right: 2rem;
bottom: 2rem;
z-index: 999;
}
.hint-text { .hint-text {
font-size: 90%; font-size: 90%;
line-height: 1.2; line-height: 1.2;

2
libs/common/src/lib/config.ts

@ -39,7 +39,7 @@ export const DEFAULT_CURRENCY = 'USD';
export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy'; export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
export const DEFAULT_LANGUAGE_CODE = 'en'; export const DEFAULT_LANGUAGE_CODE = 'en';
export const DEFAULT_PAGE_SIZE = 50; export const DEFAULT_PAGE_SIZE = 50;
export const DEFAULT_REQUEST_TIMEOUT = ms('3 seconds'); export const DEFAULT_REQUEST_TIMEOUT = ms('2 seconds');
export const DEFAULT_ROOT_URL = 'http://localhost:4200'; export const DEFAULT_ROOT_URL = 'http://localhost:4200';
export const EMERGENCY_FUND_TAG_ID = '4452656d-9fa4-4bd0-ba38-70492e31d180'; export const EMERGENCY_FUND_TAG_ID = '4452656d-9fa4-4bd0-ba38-70492e31d180';

Loading…
Cancel
Save