From 5f98dfa5d6476c8686e0087082a174448ebffc33 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 24 Nov 2024 19:49:08 +0100 Subject: [PATCH] Feature/set up Ghostfolio data provider (#4016) * Set up Ghostfolio data provider * Update translations * Update changelog --- CHANGELOG.md | 2 + apps/api/src/app/app.module.ts | 2 + .../ghostfolio/get-historical.dto.ts | 15 ++ .../ghostfolio/get-quotes.dto.ts | 10 + .../ghostfolio/ghostfolio.controller.ts | 158 +++++++++++ .../ghostfolio/ghostfolio.module.ts | 83 ++++++ .../ghostfolio/ghostfolio.service.ts | 250 ++++++++++++++++++ apps/api/src/app/import/import.service.ts | 3 +- apps/api/src/app/info/info.service.ts | 3 +- apps/api/src/app/user/user.service.ts | 5 +- .../configuration/configuration.service.ts | 3 + .../data-provider/data-provider.module.ts | 5 + .../data-provider/data-provider.service.ts | 33 ++- .../ghostfolio/ghostfolio.service.ts | 221 ++++++++++++++++ .../interfaces/environment.interface.ts | 1 + apps/client/src/app/app-routing.module.ts | 9 + .../admin-settings.component.html | 38 ++- .../admin-settings.component.ts | 83 +++++- ...ghostfolio-premium-api-dialog.component.ts | 20 ++ .../ghostfolio-premium-api-dialog.html | 14 +- apps/client/src/app/core/auth.interceptor.ts | 11 + .../src/app/core/http-response.interceptor.ts | 2 +- .../src/app/pages/api/api-page.component.ts | 110 ++++++++ apps/client/src/app/pages/api/api-page.html | 48 ++++ apps/client/src/app/pages/api/api-page.scss | 3 + apps/client/src/app/services/admin.service.ts | 25 +- apps/client/src/locales/messages.ca.xlf | 114 ++++++-- apps/client/src/locales/messages.de.xlf | 112 ++++++-- apps/client/src/locales/messages.es.xlf | 114 ++++++-- apps/client/src/locales/messages.fr.xlf | 114 ++++++-- apps/client/src/locales/messages.it.xlf | 114 ++++++-- apps/client/src/locales/messages.nl.xlf | 114 ++++++-- apps/client/src/locales/messages.pl.xlf | 114 ++++++-- apps/client/src/locales/messages.pt.xlf | 114 ++++++-- apps/client/src/locales/messages.tr.xlf | 114 ++++++-- apps/client/src/locales/messages.xlf | 104 ++++++-- apps/client/src/locales/messages.zh.xlf | 114 ++++++-- libs/common/src/lib/config.ts | 4 + libs/common/src/lib/interfaces/index.ts | 6 + ...er-ghostfolio-status-response.interface.ts | 4 + .../historical-response.interface.ts | 7 + .../responses/quotes-response.interface.ts | 5 + libs/common/src/lib/permissions.ts | 1 + .../src/lib/types/user-with-settings.type.ts | 1 + .../symbol-autocomplete.component.html | 3 + .../migration.sql | 2 + prisma/schema.prisma | 1 + 47 files changed, 2128 insertions(+), 305 deletions(-) create mode 100644 apps/api/src/app/endpoints/data-providers/ghostfolio/get-historical.dto.ts create mode 100644 apps/api/src/app/endpoints/data-providers/ghostfolio/get-quotes.dto.ts create mode 100644 apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts create mode 100644 apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.module.ts create mode 100644 apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts create mode 100644 apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts create mode 100644 apps/client/src/app/pages/api/api-page.component.ts create mode 100644 apps/client/src/app/pages/api/api-page.html create mode 100644 apps/client/src/app/pages/api/api-page.scss create mode 100644 libs/common/src/lib/interfaces/responses/data-provider-ghostfolio-status-response.interface.ts create mode 100644 libs/common/src/lib/interfaces/responses/historical-response.interface.ts create mode 100644 libs/common/src/lib/interfaces/responses/quotes-response.interface.ts create mode 100644 prisma/migrations/20241103110114_added_ghostfolio_to_data_source/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 57d4e72e9..96e16f18c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added pagination parameters (`skip`, `take`) to the endpoint `GET api/v1/admin/user` - Added pagination response (`count`) to the endpoint `GET api/v1/admin/user` +- Added `GHOSTFOLIO` as a new data source type ### Changed - Extended the allocations by ETF holding on the allocations page by the parent ETFs (experimental) +- Improved the language localization for German (`de`) - Upgraded `countries-and-timezones` from version `3.4.1` to `3.7.2` - Upgraded `Nx` from version `20.0.6` to `20.1.2` diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 2803a0580..4fbdafb08 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -31,6 +31,7 @@ import { AuthDeviceModule } from './auth-device/auth-device.module'; import { AuthModule } from './auth/auth.module'; import { BenchmarkModule } from './benchmark/benchmark.module'; import { CacheModule } from './cache/cache.module'; +import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module'; import { PublicModule } from './endpoints/public/public.module'; import { ExchangeRateModule } from './exchange-rate/exchange-rate.module'; import { ExportModule } from './export/export.module'; @@ -76,6 +77,7 @@ import { UserModule } from './user/user.module'; ExchangeRateModule, ExchangeRateDataModule, ExportModule, + GhostfolioModule, HealthModule, ImportModule, InfoModule, diff --git a/apps/api/src/app/endpoints/data-providers/ghostfolio/get-historical.dto.ts b/apps/api/src/app/endpoints/data-providers/ghostfolio/get-historical.dto.ts new file mode 100644 index 000000000..385c51d52 --- /dev/null +++ b/apps/api/src/app/endpoints/data-providers/ghostfolio/get-historical.dto.ts @@ -0,0 +1,15 @@ +import { Granularity } from '@ghostfolio/common/types'; + +import { IsIn, IsISO8601, IsOptional } from 'class-validator'; + +export class GetHistoricalDto { + @IsISO8601() + from: string; + + @IsIn(['day', 'month'] as Granularity[]) + @IsOptional() + granularity: Granularity; + + @IsISO8601() + to: string; +} diff --git a/apps/api/src/app/endpoints/data-providers/ghostfolio/get-quotes.dto.ts b/apps/api/src/app/endpoints/data-providers/ghostfolio/get-quotes.dto.ts new file mode 100644 index 000000000..e83c1be82 --- /dev/null +++ b/apps/api/src/app/endpoints/data-providers/ghostfolio/get-quotes.dto.ts @@ -0,0 +1,10 @@ +import { Transform } from 'class-transformer'; +import { IsString } from 'class-validator'; + +export class GetQuotesDto { + @IsString({ each: true }) + @Transform(({ value }) => + typeof value === 'string' ? value.split(',') : value + ) + symbols: string[]; +} diff --git a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts new file mode 100644 index 000000000..58a3224c1 --- /dev/null +++ b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts @@ -0,0 +1,158 @@ +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { parseDate } from '@ghostfolio/common/helper'; +import { + DataProviderGhostfolioStatusResponse, + HistoricalResponse, + LookupResponse, + QuotesResponse +} from '@ghostfolio/common/interfaces'; +import { permissions } from '@ghostfolio/common/permissions'; +import { RequestWithUser } from '@ghostfolio/common/types'; + +import { + Controller, + Get, + HttpException, + Inject, + Param, + Query, + UseGuards +} from '@nestjs/common'; +import { REQUEST } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; +import { getReasonPhrase, StatusCodes } from 'http-status-codes'; + +import { GetHistoricalDto } from './get-historical.dto'; +import { GetQuotesDto } from './get-quotes.dto'; +import { GhostfolioService } from './ghostfolio.service'; + +@Controller('data-providers/ghostfolio') +export class GhostfolioController { + public constructor( + private readonly ghostfolioService: GhostfolioService, + @Inject(REQUEST) private readonly request: RequestWithUser + ) {} + + @Get('historical/:symbol') + @HasPermission(permissions.enableDataProviderGhostfolio) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + public async getHistorical( + @Param('symbol') symbol: string, + @Query() query: GetHistoricalDto + ): Promise { + const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests(); + + if ( + this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS), + StatusCodes.TOO_MANY_REQUESTS + ); + } + + try { + const historicalData = await this.ghostfolioService.getHistorical({ + symbol, + from: parseDate(query.from), + granularity: query.granularity, + to: parseDate(query.to) + }); + + await this.ghostfolioService.incrementDailyRequests({ + userId: this.request.user.id + }); + + return historicalData; + } catch { + throw new HttpException( + getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR), + StatusCodes.INTERNAL_SERVER_ERROR + ); + } + } + + @Get('lookup') + @HasPermission(permissions.enableDataProviderGhostfolio) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + public async lookupSymbol( + @Query('includeIndices') includeIndicesParam = 'false', + @Query('query') query = '' + ): Promise { + const includeIndices = includeIndicesParam === 'true'; + const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests(); + + if ( + this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS), + StatusCodes.TOO_MANY_REQUESTS + ); + } + + try { + const result = await this.ghostfolioService.lookup({ + includeIndices, + query: query.toLowerCase() + }); + + await this.ghostfolioService.incrementDailyRequests({ + userId: this.request.user.id + }); + + return result; + } catch { + throw new HttpException( + getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR), + StatusCodes.INTERNAL_SERVER_ERROR + ); + } + } + + @Get('quotes') + @HasPermission(permissions.enableDataProviderGhostfolio) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + public async getQuotes( + @Query() query: GetQuotesDto + ): Promise { + const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests(); + + if ( + this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS), + StatusCodes.TOO_MANY_REQUESTS + ); + } + + try { + const quotes = await this.ghostfolioService.getQuotes({ + symbols: query.symbols + }); + + await this.ghostfolioService.incrementDailyRequests({ + userId: this.request.user.id + }); + + return quotes; + } catch { + throw new HttpException( + getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR), + StatusCodes.INTERNAL_SERVER_ERROR + ); + } + } + + @Get('status') + @HasPermission(permissions.enableDataProviderGhostfolio) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + public async getStatus(): Promise { + return { + dailyRequests: this.request.user.dataProviderGhostfolioDailyRequests, + dailyRequestsMax: await this.ghostfolioService.getMaxDailyRequests() + }; + } +} diff --git a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.module.ts b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.module.ts new file mode 100644 index 000000000..01691bcf4 --- /dev/null +++ b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.module.ts @@ -0,0 +1,83 @@ +import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module'; +import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service'; +import { CoinGeckoService } from '@ghostfolio/api/services/data-provider/coingecko/coingecko.service'; +import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service'; +import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; +import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; +import { EodHistoricalDataService } from '@ghostfolio/api/services/data-provider/eod-historical-data/eod-historical-data.service'; +import { FinancialModelingPrepService } from '@ghostfolio/api/services/data-provider/financial-modeling-prep/financial-modeling-prep.service'; +import { GoogleSheetsService } from '@ghostfolio/api/services/data-provider/google-sheets/google-sheets.service'; +import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service'; +import { RapidApiService } from '@ghostfolio/api/services/data-provider/rapid-api/rapid-api.service'; +import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service'; +import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; +import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; +import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; +import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; + +import { Module } from '@nestjs/common'; + +import { GhostfolioController } from './ghostfolio.controller'; +import { GhostfolioService } from './ghostfolio.service'; + +@Module({ + controllers: [GhostfolioController], + imports: [ + CryptocurrencyModule, + DataProviderModule, + MarketDataModule, + PrismaModule, + PropertyModule, + RedisCacheModule, + SymbolProfileModule + ], + providers: [ + AlphaVantageService, + CoinGeckoService, + ConfigurationService, + DataProviderService, + EodHistoricalDataService, + FinancialModelingPrepService, + GhostfolioService, + GoogleSheetsService, + ManualService, + RapidApiService, + YahooFinanceService, + YahooFinanceDataEnhancerService, + { + inject: [ + AlphaVantageService, + CoinGeckoService, + EodHistoricalDataService, + FinancialModelingPrepService, + GoogleSheetsService, + ManualService, + RapidApiService, + YahooFinanceService + ], + provide: 'DataProviderInterfaces', + useFactory: ( + alphaVantageService, + coinGeckoService, + eodHistoricalDataService, + financialModelingPrepService, + googleSheetsService, + manualService, + rapidApiService, + yahooFinanceService + ) => [ + alphaVantageService, + coinGeckoService, + eodHistoricalDataService, + financialModelingPrepService, + googleSheetsService, + manualService, + rapidApiService, + yahooFinanceService + ] + } + ] +}) +export class GhostfolioModule {} diff --git a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts new file mode 100644 index 000000000..52baa10d6 --- /dev/null +++ b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts @@ -0,0 +1,250 @@ +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; +import { + GetHistoricalParams, + GetQuotesParams, + GetSearchParams +} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; +import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; +import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; +import { PropertyService } from '@ghostfolio/api/services/property/property.service'; +import { + DEFAULT_CURRENCY, + DERIVED_CURRENCIES +} from '@ghostfolio/common/config'; +import { PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS } from '@ghostfolio/common/config'; +import { + DataProviderInfo, + HistoricalResponse, + LookupItem, + LookupResponse, + QuotesResponse +} from '@ghostfolio/common/interfaces'; + +import { Injectable, Logger } from '@nestjs/common'; +import { DataSource } from '@prisma/client'; +import { Big } from 'big.js'; + +@Injectable() +export class GhostfolioService { + public constructor( + private readonly configurationService: ConfigurationService, + private readonly dataProviderService: DataProviderService, + private readonly prismaService: PrismaService, + private readonly propertyService: PropertyService + ) {} + + public async getHistorical({ + from, + granularity, + requestTimeout, + to, + symbol + }: GetHistoricalParams) { + const result: HistoricalResponse = { historicalData: {} }; + + try { + const promises: Promise<{ + [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; + }>[] = []; + + for (const dataProviderService of this.getDataProviderServices()) { + promises.push( + dataProviderService + .getHistorical({ + from, + granularity, + requestTimeout, + symbol, + to + }) + .then((historicalData) => { + result.historicalData = historicalData[symbol]; + + return historicalData; + }) + ); + } + + await Promise.all(promises); + + return result; + } catch (error) { + Logger.error(error, 'GhostfolioService'); + + throw error; + } + } + + public async getMaxDailyRequests() { + return parseInt( + ((await this.propertyService.getByKey( + PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS + )) as string) || '0', + 10 + ); + } + + public async getQuotes({ requestTimeout, symbols }: GetQuotesParams) { + const promises: Promise[] = []; + const results: QuotesResponse = { quotes: {} }; + + try { + for (const dataProvider of this.getDataProviderServices()) { + const maximumNumberOfSymbolsPerRequest = + dataProvider.getMaxNumberOfSymbolsPerRequest?.() ?? + Number.MAX_SAFE_INTEGER; + + for ( + let i = 0; + i < symbols.length; + i += maximumNumberOfSymbolsPerRequest + ) { + const symbolsChunk = symbols.slice( + i, + i + maximumNumberOfSymbolsPerRequest + ); + + const promise = Promise.resolve( + dataProvider.getQuotes({ requestTimeout, symbols: symbolsChunk }) + ); + + promises.push( + promise.then(async (result) => { + for (const [symbol, dataProviderResponse] of Object.entries( + result + )) { + dataProviderResponse.dataSource = 'GHOSTFOLIO'; + + if ( + [ + ...DERIVED_CURRENCIES.map(({ currency }) => { + return `${DEFAULT_CURRENCY}${currency}`; + }), + `${DEFAULT_CURRENCY}USX` + ].includes(symbol) + ) { + continue; + } + + results.quotes[symbol] = dataProviderResponse; + + for (const { + currency, + factor, + rootCurrency + } of DERIVED_CURRENCIES) { + if (symbol === `${DEFAULT_CURRENCY}${rootCurrency}`) { + results.quotes[`${DEFAULT_CURRENCY}${currency}`] = { + ...dataProviderResponse, + currency, + marketPrice: new Big( + result[`${DEFAULT_CURRENCY}${rootCurrency}`].marketPrice + ) + .mul(factor) + .toNumber(), + marketState: 'open' + }; + } + } + } + }) + ); + } + + await Promise.all(promises); + } + + return results; + } catch (error) { + Logger.error(error, 'GhostfolioService'); + + throw error; + } + } + + public async incrementDailyRequests({ userId }: { userId: string }) { + await this.prismaService.analytics.update({ + data: { + dataProviderGhostfolioDailyRequests: { increment: 1 }, + lastRequestAt: new Date() + }, + where: { userId } + }); + } + + public async lookup({ + includeIndices = false, + query + }: GetSearchParams): Promise { + const results: LookupResponse = { items: [] }; + + if (!query) { + return results; + } + + try { + let lookupItems: LookupItem[] = []; + const promises: Promise<{ items: LookupItem[] }>[] = []; + + if (query?.length < 2) { + return { items: lookupItems }; + } + + for (const dataProviderService of this.getDataProviderServices()) { + promises.push( + dataProviderService.search({ + includeIndices, + query + }) + ); + } + + const searchResults = await Promise.all(promises); + + for (const { items } of searchResults) { + if (items?.length > 0) { + lookupItems = lookupItems.concat(items); + } + } + + const filteredItems = lookupItems + .filter(({ currency }) => { + // Only allow symbols with supported currency + return currency ? true : false; + }) + .sort(({ name: name1 }, { name: name2 }) => { + return name1?.toLowerCase().localeCompare(name2?.toLowerCase()); + }) + .map((lookupItem) => { + lookupItem.dataProviderInfo = this.getDataProviderInfo(); + lookupItem.dataSource = 'GHOSTFOLIO'; + + return lookupItem; + }); + + results.items = filteredItems; + return results; + } catch (error) { + Logger.error(error, 'GhostfolioService'); + + throw error; + } + } + + private getDataProviderInfo(): DataProviderInfo { + return { + isPremium: false, + name: 'Ghostfolio Premium', + url: 'https://ghostfol.io' + }; + } + + private getDataProviderServices() { + return this.configurationService + .get('DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER') + .map((dataSource) => { + return this.dataProviderService.getDataProvider(DataSource[dataSource]); + }); + } +} diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 30415970d..e51696b56 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -582,12 +582,13 @@ export class ImportService { const assetProfiles: { [assetProfileIdentifier: string]: Partial; } = {}; + const dataSources = await this.dataProviderService.getDataSources(); for (const [ index, { currency, dataSource, symbol, type } ] of activitiesDto.entries()) { - if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) { + if (!dataSources.includes(dataSource)) { throw new Error( `activities.${index}.dataSource ("${dataSource}") is not valid` ); diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index 62a78d1d8..904a97090 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -7,6 +7,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { DEFAULT_CURRENCY, + HEADER_KEY_TOKEN, PROPERTY_BETTER_UPTIME_MONITOR_ID, PROPERTY_COUNTRIES_OF_SUBSCRIBERS, PROPERTY_DEMO_USER_ID, @@ -347,7 +348,7 @@ export class InfoService { )}&to${format(new Date(), DATE_FORMAT)}`, { headers: { - Authorization: `Bearer ${this.configurationService.get( + [HEADER_KEY_TOKEN]: `Bearer ${this.configurationService.get( 'API_KEY_BETTER_UPTIME' )}` }, diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 443a2a052..54dafda22 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -183,7 +183,9 @@ export class UserService { Settings: Settings as UserWithSettings['Settings'], thirdPartyId, updatedAt, - activityCount: Analytics?.activityCount + activityCount: Analytics?.activityCount, + dataProviderGhostfolioDailyRequests: + Analytics?.dataProviderGhostfolioDailyRequests }; if (user?.Settings) { @@ -307,6 +309,7 @@ export class UserService { // Reset holdings view mode user.Settings.settings.holdingsViewMode = undefined; } else if (user.subscription?.type === 'Premium') { + currentPermissions.push(permissions.enableDataProviderGhostfolio); currentPermissions.push(permissions.reportDataGlitch); currentPermissions = without( diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index 10810deb5..acde7d823 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -35,6 +35,9 @@ export class ConfigurationService { DATA_SOURCES: json({ default: [DataSource.COINGECKO, DataSource.MANUAL, DataSource.YAHOO] }), + DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER: json({ + default: [] + }), ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }), ENABLE_FEATURE_READ_ONLY_MODE: bool({ default: false }), ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }), diff --git a/apps/api/src/services/data-provider/data-provider.module.ts b/apps/api/src/services/data-provider/data-provider.module.ts index dcfc756f2..71b54f01e 100644 --- a/apps/api/src/services/data-provider/data-provider.module.ts +++ b/apps/api/src/services/data-provider/data-provider.module.ts @@ -5,6 +5,7 @@ import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alph import { CoinGeckoService } from '@ghostfolio/api/services/data-provider/coingecko/coingecko.service'; import { EodHistoricalDataService } from '@ghostfolio/api/services/data-provider/eod-historical-data/eod-historical-data.service'; import { FinancialModelingPrepService } from '@ghostfolio/api/services/data-provider/financial-modeling-prep/financial-modeling-prep.service'; +import { GhostfolioService } from '@ghostfolio/api/services/data-provider/ghostfolio/ghostfolio.service'; import { GoogleSheetsService } from '@ghostfolio/api/services/data-provider/google-sheets/google-sheets.service'; import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service'; import { RapidApiService } from '@ghostfolio/api/services/data-provider/rapid-api/rapid-api.service'; @@ -37,6 +38,7 @@ import { DataProviderService } from './data-provider.service'; DataProviderService, EodHistoricalDataService, FinancialModelingPrepService, + GhostfolioService, GoogleSheetsService, ManualService, RapidApiService, @@ -47,6 +49,7 @@ import { DataProviderService } from './data-provider.service'; CoinGeckoService, EodHistoricalDataService, FinancialModelingPrepService, + GhostfolioService, GoogleSheetsService, ManualService, RapidApiService, @@ -58,6 +61,7 @@ import { DataProviderService } from './data-provider.service'; coinGeckoService, eodHistoricalDataService, financialModelingPrepService, + ghostfolioService, googleSheetsService, manualService, rapidApiService, @@ -67,6 +71,7 @@ import { DataProviderService } from './data-provider.service'; coinGeckoService, eodHistoricalDataService, financialModelingPrepService, + ghostfolioService, googleSheetsService, manualService, rapidApiService, diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts index c8a7422d0..3faf5b583 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -11,6 +11,7 @@ import { PropertyService } from '@ghostfolio/api/services/property/property.serv import { DEFAULT_CURRENCY, DERIVED_CURRENCIES, + PROPERTY_API_KEY_GHOSTFOLIO, PROPERTY_DATA_SOURCE_MAPPING } from '@ghostfolio/common/config'; import { @@ -153,6 +154,24 @@ export class DataProviderService { return DataSource[this.configurationService.get('DATA_SOURCE_IMPORT')]; } + public async getDataSources(): Promise { + const dataSources: DataSource[] = this.configurationService + .get('DATA_SOURCES') + .map((dataSource) => { + return DataSource[dataSource]; + }); + + const ghostfolioApiKey = (await this.propertyService.getByKey( + PROPERTY_API_KEY_GHOSTFOLIO + )) as string; + + if (ghostfolioApiKey) { + dataSources.push('GHOSTFOLIO'); + } + + return dataSources.sort(); + } + public async getDividends({ dataSource, from, @@ -589,11 +608,11 @@ export class DataProviderService { return { items: lookupItems }; } - const dataProviderServices = this.configurationService - .get('DATA_SOURCES') - .map((dataSource) => { - return this.getDataProvider(DataSource[dataSource]); - }); + const dataSources = await this.getDataSources(); + + const dataProviderServices = dataSources.map((dataSource) => { + return this.getDataProvider(DataSource[dataSource]); + }); for (const dataProviderService of dataProviderServices) { promises.push( @@ -606,11 +625,11 @@ export class DataProviderService { const searchResults = await Promise.all(promises); - searchResults.forEach(({ items }) => { + for (const { items } of searchResults) { if (items?.length > 0) { lookupItems = lookupItems.concat(items); } - }); + } const filteredItems = lookupItems .filter(({ currency }) => { diff --git a/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts b/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts new file mode 100644 index 000000000..a1ac6b657 --- /dev/null +++ b/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts @@ -0,0 +1,221 @@ +import { environment } from '@ghostfolio/api/environments/environment'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { + DataProviderInterface, + GetDividendsParams, + GetHistoricalParams, + GetQuotesParams, + GetSearchParams +} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; +import { + IDataProviderHistoricalResponse, + IDataProviderResponse +} from '@ghostfolio/api/services/interfaces/interfaces'; +import { PropertyService } from '@ghostfolio/api/services/property/property.service'; +import { + HEADER_KEY_TOKEN, + PROPERTY_API_KEY_GHOSTFOLIO +} from '@ghostfolio/common/config'; +import { DATE_FORMAT } from '@ghostfolio/common/helper'; +import { + DataProviderInfo, + HistoricalResponse, + LookupResponse, + QuotesResponse +} from '@ghostfolio/common/interfaces'; + +import { Injectable, Logger } from '@nestjs/common'; +import { DataSource, SymbolProfile } from '@prisma/client'; +import { format } from 'date-fns'; +import got from 'got'; + +@Injectable() +export class GhostfolioService implements DataProviderInterface { + private apiKey: string; + private readonly URL = environment.production + ? 'https://ghostfol.io/api' + : `${this.configurationService.get('ROOT_URL')}/api`; + + public constructor( + private readonly configurationService: ConfigurationService, + private readonly propertyService: PropertyService + ) { + void this.initialize(); + } + + public async initialize() { + this.apiKey = (await this.propertyService.getByKey( + PROPERTY_API_KEY_GHOSTFOLIO + )) as string; + } + + public canHandle() { + return true; + } + + public async getAssetProfile({ + symbol + }: { + symbol: string; + }): Promise> { + const { items } = await this.search({ query: symbol }); + const searchResult = items?.[0]; + + return { + symbol, + assetClass: searchResult?.assetClass, + assetSubClass: searchResult?.assetSubClass, + currency: searchResult?.currency, + dataSource: this.getName(), + name: searchResult?.name + }; + } + + public getDataProviderInfo(): DataProviderInfo { + return { + isPremium: true, + name: 'Ghostfolio', + url: 'https://ghostfo.io' + }; + } + + public async getDividends({}: GetDividendsParams) { + return {}; + } + + public async getHistorical({ + from, + granularity = 'day', + requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), + symbol, + to + }: GetHistoricalParams): Promise<{ + [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; + }> { + try { + const abortController = new AbortController(); + + setTimeout(() => { + abortController.abort(); + }, requestTimeout); + + const { historicalData } = await got( + `${this.URL}/v1/data-providers/ghostfolio/historical/${symbol}?from=${format(from, DATE_FORMAT)}&granularity=${granularity}&to=${format( + to, + DATE_FORMAT + )}`, + { + headers: this.getRequestHeaders(), + // @ts-ignore + signal: abortController.signal + } + ).json(); + + return { + [symbol]: historicalData + }; + } catch (error) { + throw new Error( + `Could not get historical market data for ${symbol} (${this.getName()}) from ${format( + from, + DATE_FORMAT + )} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}` + ); + } + } + + public getMaxNumberOfSymbolsPerRequest() { + return 20; + } + + public getName(): DataSource { + return DataSource.GHOSTFOLIO; + } + + public async getQuotes({ + requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), + symbols + }: GetQuotesParams): Promise<{ + [symbol: string]: IDataProviderResponse; + }> { + let response: { [symbol: string]: IDataProviderResponse } = {}; + + if (symbols.length <= 0) { + return response; + } + + try { + const abortController = new AbortController(); + + setTimeout(() => { + abortController.abort(); + }, requestTimeout); + + const { quotes } = await got( + `${this.URL}/v1/data-providers/ghostfolio/quotes?symbols=${symbols.join(',')}`, + { + headers: this.getRequestHeaders(), + // @ts-ignore + signal: abortController.signal + } + ).json(); + + response = quotes; + } catch (error) { + let message = error; + + if (error?.code === 'ABORT_ERR') { + message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( + this.configurationService.get('REQUEST_TIMEOUT') / 1000 + ).toFixed(3)} seconds`; + } + + Logger.error(message, 'GhostfolioService'); + } + + return response; + } + + public getTestSymbol() { + return 'AAPL.US'; + } + + public async search({ query }: GetSearchParams): Promise { + let searchResult: LookupResponse = { items: [] }; + + try { + const abortController = new AbortController(); + + setTimeout(() => { + abortController.abort(); + }, this.configurationService.get('REQUEST_TIMEOUT')); + + searchResult = await got( + `${this.URL}/v1/data-providers/ghostfolio/lookup?query=${query}`, + { + headers: this.getRequestHeaders(), + // @ts-ignore + signal: abortController.signal + } + ).json(); + } catch (error) { + let message = error; + + if (error?.code === 'ABORT_ERR') { + message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${( + this.configurationService.get('REQUEST_TIMEOUT') / 1000 + ).toFixed(3)} seconds`; + } + + Logger.error(message, 'GhostfolioService'); + } + + return searchResult; + } + + private getRequestHeaders() { + return { + [HEADER_KEY_TOKEN]: `Bearer ${this.apiKey}` + }; + } +} diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index 8d6dd34de..2f94739fb 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -15,6 +15,7 @@ export interface Environment extends CleanedEnvAccessors { DATA_SOURCE_EXCHANGE_RATES: string; DATA_SOURCE_IMPORT: string; DATA_SOURCES: string[]; + DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER: string[]; ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean; ENABLE_FEATURE_READ_ONLY_MODE: boolean; ENABLE_FEATURE_SOCIAL_LOGIN: boolean; diff --git a/apps/client/src/app/app-routing.module.ts b/apps/client/src/app/app-routing.module.ts index 8a517c5fe..f4b61ea33 100644 --- a/apps/client/src/app/app-routing.module.ts +++ b/apps/client/src/app/app-routing.module.ts @@ -32,6 +32,15 @@ const routes: Routes = [ loadChildren: () => import('./pages/admin/admin-page.module').then((m) => m.AdminPageModule) }, + { + canActivate: [AuthGuard], + loadComponent: () => + import('./pages/api/api-page.component').then( + (c) => c.GfApiPageComponent + ), + path: 'api', + title: 'Ghostfolio API' + }, { path: 'auth', loadChildren: () => diff --git a/apps/client/src/app/components/admin-settings/admin-settings.component.html b/apps/client/src/app/components/admin-settings/admin-settings.component.html index b3a63df7a..35ed556b6 100644 --- a/apps/client/src/app/components/admin-settings/admin-settings.component.html +++ b/apps/client/src/app/components/admin-settings/admin-settings.component.html @@ -11,7 +11,9 @@ target="_blank" [href]="pricingUrl" > - NEW + @if (isGhostfolioApiKeyValid === false) { + NEW + } Ghostfolio Premium
- + @if (isGhostfolioApiKeyValid === true) { +
+
+ {{ ghostfolioApiStatus.dailyRequests }} + of + {{ ghostfolioApiStatus.dailyRequestsMax }} + daily requests +
+ +
+ } @else if (isGhostfolioApiKeyValid === false) { + + }
diff --git a/apps/client/src/app/components/admin-settings/admin-settings.component.ts b/apps/client/src/app/components/admin-settings/admin-settings.component.ts index 2dd2555bd..d25cdfbcd 100644 --- a/apps/client/src/app/components/admin-settings/admin-settings.component.ts +++ b/apps/client/src/app/components/admin-settings/admin-settings.component.ts @@ -1,5 +1,13 @@ +import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type'; +import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; +import { AdminService } from '@ghostfolio/client/services/admin.service'; +import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { User } from '@ghostfolio/common/interfaces'; +import { PROPERTY_API_KEY_GHOSTFOLIO } from '@ghostfolio/common/config'; +import { + DataProviderGhostfolioStatusResponse, + User +} from '@ghostfolio/common/interfaces'; import { ChangeDetectionStrategy, @@ -10,7 +18,7 @@ import { } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { DeviceDetectorService } from 'ngx-device-detector'; -import { Subject, takeUntil } from 'rxjs'; +import { catchError, filter, of, Subject, takeUntil } from 'rxjs'; import { GfGhostfolioPremiumApiDialogComponent } from './ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component'; @@ -21,6 +29,8 @@ import { GfGhostfolioPremiumApiDialogComponent } from './ghostfolio-premium-api- templateUrl: './admin-settings.component.html' }) export class AdminSettingsComponent implements OnDestroy, OnInit { + public ghostfolioApiStatus: DataProviderGhostfolioStatusResponse; + public isGhostfolioApiKeyValid: boolean; public pricingUrl: string; private deviceType: string; @@ -28,9 +38,12 @@ export class AdminSettingsComponent implements OnDestroy, OnInit { private user: User; public constructor( + private adminService: AdminService, private changeDetectorRef: ChangeDetectorRef, + private dataService: DataService, private deviceService: DeviceDetectorService, private matDialog: MatDialog, + private notificationService: NotificationService, private userService: UserService ) {} @@ -50,22 +63,72 @@ export class AdminSettingsComponent implements OnDestroy, OnInit { this.changeDetectorRef.markForCheck(); } }); + + this.initialize(); } - public onSetGhostfolioApiKey() { - this.matDialog.open(GfGhostfolioPremiumApiDialogComponent, { - autoFocus: false, - data: { - deviceType: this.deviceType, - pricingUrl: this.pricingUrl + public onRemoveGhostfolioApiKey() { + this.notificationService.confirm({ + confirmFn: () => { + this.dataService + .putAdminSetting(PROPERTY_API_KEY_GHOSTFOLIO, { value: undefined }) + .subscribe(() => { + this.initialize(); + }); }, - height: this.deviceType === 'mobile' ? '98vh' : undefined, - width: this.deviceType === 'mobile' ? '100vw' : '50rem' + confirmType: ConfirmationDialogType.Warn, + title: $localize`Do you really want to delete the API key?` }); } + public onSetGhostfolioApiKey() { + const dialogRef = this.matDialog.open( + GfGhostfolioPremiumApiDialogComponent, + { + autoFocus: false, + data: { + deviceType: this.deviceType, + pricingUrl: this.pricingUrl + }, + height: this.deviceType === 'mobile' ? '98vh' : undefined, + width: this.deviceType === 'mobile' ? '100vw' : '50rem' + } + ); + + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.initialize(); + }); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); } + + private initialize() { + this.adminService + .fetchGhostfolioDataProviderStatus() + .pipe( + catchError(() => { + this.isGhostfolioApiKeyValid = false; + + this.changeDetectorRef.markForCheck(); + + return of(null); + }), + filter((status) => { + return status !== null; + }), + takeUntil(this.unsubscribeSubject) + ) + .subscribe((status) => { + this.ghostfolioApiStatus = status; + this.isGhostfolioApiKeyValid = true; + + this.changeDetectorRef.markForCheck(); + }); + } } diff --git a/apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component.ts b/apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component.ts index 856ddc852..f15866f13 100644 --- a/apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component.ts +++ b/apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component.ts @@ -1,3 +1,5 @@ +import { DataService } from '@ghostfolio/client/services/data.service'; +import { PROPERTY_API_KEY_GHOSTFOLIO } from '@ghostfolio/common/config'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { CommonModule } from '@angular/common'; @@ -30,10 +32,28 @@ import { GhostfolioPremiumApiDialogParams } from './interfaces/interfaces'; export class GfGhostfolioPremiumApiDialogComponent { public constructor( @Inject(MAT_DIALOG_DATA) public data: GhostfolioPremiumApiDialogParams, + private dataService: DataService, public dialogRef: MatDialogRef ) {} public onCancel() { this.dialogRef.close(); } + + public onSetGhostfolioApiKey() { + let ghostfolioApiKey = prompt( + $localize`Please enter your Ghostfolio API key:` + ); + ghostfolioApiKey = ghostfolioApiKey?.trim(); + + if (ghostfolioApiKey) { + this.dataService + .putAdminSetting(PROPERTY_API_KEY_GHOSTFOLIO, { + value: ghostfolioApiKey + }) + .subscribe(() => { + this.dialogRef.close(); + }); + } + } } diff --git a/apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html b/apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html index 25673075d..f2f753750 100644 --- a/apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html +++ b/apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html @@ -29,9 +29,19 @@ href="mailto:hi@ghostfol.io?Subject=Ghostfolio Premium Data Provider&body=Hello%0D%0DPlease notify me as soon as the Ghostfolio Premium Data Provider is available.%0D%0DKind regards" i18n mat-flat-button + >Notify me - Notify me - +
+ or +
+ diff --git a/apps/client/src/app/core/auth.interceptor.ts b/apps/client/src/app/core/auth.interceptor.ts index b0dbdf641..7491cecf1 100644 --- a/apps/client/src/app/core/auth.interceptor.ts +++ b/apps/client/src/app/core/auth.interceptor.ts @@ -2,6 +2,7 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/imperso import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { HEADER_KEY_IMPERSONATION, + HEADER_KEY_SKIP_INTERCEPTOR, HEADER_KEY_TIMEZONE, HEADER_KEY_TOKEN } from '@ghostfolio/common/config'; @@ -27,6 +28,16 @@ export class AuthInterceptor implements HttpInterceptor { next: HttpHandler ): Observable> { let request = req; + + if (request.headers.has(HEADER_KEY_SKIP_INTERCEPTOR)) { + // Bypass the interceptor + request = request.clone({ + headers: req.headers.delete(HEADER_KEY_SKIP_INTERCEPTOR) + }); + + return next.handle(request); + } + let headers = request.headers.set( HEADER_KEY_TIMEZONE, Intl?.DateTimeFormat().resolvedOptions().timeZone diff --git a/apps/client/src/app/core/http-response.interceptor.ts b/apps/client/src/app/core/http-response.interceptor.ts index 0e24533ef..203d3adf5 100644 --- a/apps/client/src/app/core/http-response.interceptor.ts +++ b/apps/client/src/app/core/http-response.interceptor.ts @@ -103,7 +103,7 @@ export class HttpResponseInterceptor implements HttpInterceptor { } else if (error.status === StatusCodes.UNAUTHORIZED) { if (this.webAuthnService.isEnabled()) { this.router.navigate(['/webauthn']); - } else { + } else if (!error.url.includes('/data-providers/ghostfolio/status')) { this.tokenStorageService.signOut(); } } diff --git a/apps/client/src/app/pages/api/api-page.component.ts b/apps/client/src/app/pages/api/api-page.component.ts new file mode 100644 index 000000000..7b2d70aeb --- /dev/null +++ b/apps/client/src/app/pages/api/api-page.component.ts @@ -0,0 +1,110 @@ +import { DATE_FORMAT } from '@ghostfolio/common/helper'; +import { + DataProviderGhostfolioStatusResponse, + HistoricalResponse, + LookupResponse, + QuotesResponse +} from '@ghostfolio/common/interfaces'; + +import { CommonModule } from '@angular/common'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; +import { format, startOfYear } from 'date-fns'; +import { map, Observable, Subject, takeUntil } from 'rxjs'; + +@Component({ + host: { class: 'page' }, + imports: [CommonModule], + selector: 'gf-api-page', + standalone: true, + styleUrls: ['./api-page.scss'], + templateUrl: './api-page.html' +}) +export class GfApiPageComponent implements OnInit { + public historicalData$: Observable; + public quotes$: Observable; + public status$: Observable; + public symbols$: Observable; + + private unsubscribeSubject = new Subject(); + + public constructor(private http: HttpClient) {} + + public ngOnInit() { + this.historicalData$ = this.fetchHistoricalData({ symbol: 'AAPL.US' }); + this.quotes$ = this.fetchQuotes({ symbols: ['AAPL.US', 'VOO.US'] }); + this.status$ = this.fetchStatus(); + this.symbols$ = this.fetchSymbols({ query: 'apple' }); + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } + + private fetchHistoricalData({ symbol }: { symbol: string }) { + const params = new HttpParams() + .set('from', format(startOfYear(new Date()), DATE_FORMAT)) + .set('to', format(new Date(), DATE_FORMAT)); + + return this.http + .get( + `/api/v1/data-providers/ghostfolio/historical/${symbol}`, + { params } + ) + .pipe( + map(({ historicalData }) => { + return historicalData; + }), + takeUntil(this.unsubscribeSubject) + ); + } + + private fetchQuotes({ symbols }: { symbols: string[] }) { + const params = new HttpParams().set('symbols', symbols.join(',')); + + return this.http + .get('/api/v1/data-providers/ghostfolio/quotes', { + params + }) + .pipe( + map(({ quotes }) => { + return quotes; + }), + takeUntil(this.unsubscribeSubject) + ); + } + + private fetchStatus() { + return this.http + .get( + '/api/v1/data-providers/ghostfolio/status' + ) + .pipe(takeUntil(this.unsubscribeSubject)); + } + + private fetchSymbols({ + includeIndices = false, + query + }: { + includeIndices?: boolean; + query: string; + }) { + let params = new HttpParams().set('query', query); + + if (includeIndices) { + params = params.append('includeIndices', includeIndices); + } + + return this.http + .get('/api/v1/data-providers/ghostfolio/lookup', { + params + }) + .pipe( + map(({ items }) => { + return items; + }), + takeUntil(this.unsubscribeSubject) + ); + } +} diff --git a/apps/client/src/app/pages/api/api-page.html b/apps/client/src/app/pages/api/api-page.html new file mode 100644 index 000000000..d7dca7fea --- /dev/null +++ b/apps/client/src/app/pages/api/api-page.html @@ -0,0 +1,48 @@ +
+
+

Status

+
{{ status$ | async | json }}
+
+
+

Lookup

+ @if (symbols$) { + @let symbols = symbols$ | async; +
    + @for (item of symbols; track item.symbol) { +
  • {{ item.name }} ({{ item.symbol }})
  • + } +
+ } +
+
+

Quotes

+ @if (quotes$) { + @let quotes = quotes$ | async; +
    + @for (quote of quotes | keyvalue; track quote) { +
  • + {{ quote.key }}: {{ quote.value.marketPrice }} + {{ quote.value.currency }} +
  • + } +
+ } +
+
+

Historical

+ @if (historicalData$) { + @let historicalData = historicalData$ | async; +
    + @for ( + historicalDataItem of historicalData | keyvalue; + track historicalDataItem + ) { +
  • + {{ historicalDataItem.key }}: + {{ historicalDataItem.value.marketPrice }} +
  • + } +
+ } +
+
diff --git a/apps/client/src/app/pages/api/api-page.scss b/apps/client/src/app/pages/api/api-page.scss new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/apps/client/src/app/pages/api/api-page.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index 20cfa8ef8..d004671dd 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -5,6 +5,11 @@ import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform. import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto'; import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; +import { + HEADER_KEY_SKIP_INTERCEPTOR, + HEADER_KEY_TOKEN, + PROPERTY_API_KEY_GHOSTFOLIO +} from '@ghostfolio/common/config'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, @@ -13,6 +18,7 @@ import { AdminMarketData, AdminMarketDataDetails, AdminUsers, + DataProviderGhostfolioStatusResponse, EnhancedSymbolProfile, Filter } from '@ghostfolio/common/interfaces'; @@ -23,8 +29,9 @@ import { SortDirection } from '@angular/material/sort'; import { DataSource, MarketData, Platform, Tag } from '@prisma/client'; import { JobStatus } from 'bull'; import { format, parseISO } from 'date-fns'; -import { Observable, map } from 'rxjs'; +import { Observable, map, switchMap } from 'rxjs'; +import { environment } from '../../environments/environment'; import { DataService } from './data.service'; @Injectable({ @@ -136,6 +143,22 @@ export class AdminService { ); } + public fetchGhostfolioDataProviderStatus() { + return this.fetchAdminData().pipe( + switchMap(({ settings }) => { + return this.http.get( + `${environment.production ? 'https://ghostfol.io' : ''}/api/v1/data-providers/ghostfolio/status`, + { + headers: { + [HEADER_KEY_SKIP_INTERCEPTOR]: 'true', + [HEADER_KEY_TOKEN]: `Bearer ${settings[PROPERTY_API_KEY_GHOSTFOLIO]}` + } + } + ); + }) + ); + } + public fetchJobs({ status }: { status?: JobStatus[] }) { let params = new HttpParams(); diff --git a/apps/client/src/locales/messages.ca.xlf b/apps/client/src/locales/messages.ca.xlf index feff4f761..751ef908d 100644 --- a/apps/client/src/locales/messages.ca.xlf +++ b/apps/client/src/locales/messages.ca.xlf @@ -6,7 +6,7 @@ Característiques apps/client/src/app/app-routing.module.ts - 65 + 74 @@ -14,7 +14,7 @@ Internacionalització apps/client/src/app/app-routing.module.ts - 79 + 88 @@ -22,7 +22,7 @@ Iniciar sessió apps/client/src/app/app-routing.module.ts - 141 + 150 apps/client/src/app/components/header/header.component.ts @@ -633,7 +633,7 @@ apps/client/src/app/components/admin-settings/admin-settings.component.ts - 48 + 61 apps/client/src/app/components/header/header.component.ts @@ -1051,7 +1051,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 12 + 16 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 88 @@ -1155,7 +1159,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 26 + 25 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 102 @@ -2191,7 +2199,7 @@ Plataformes apps/client/src/app/components/admin-settings/admin-settings.component.html - 39 + 59 @@ -2199,7 +2207,7 @@ Etiquetes apps/client/src/app/components/admin-settings/admin-settings.component.html - 45 + 65 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -2833,6 +2841,10 @@ or or + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 35 + apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html 31 @@ -4991,7 +5003,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 88 + 181 @@ -6271,7 +6283,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 46 + 40 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 116 @@ -6743,7 +6759,7 @@ Show more libs/ui/src/lib/top-holdings/top-holdings.component.html - 81 + 174 @@ -7303,7 +7319,7 @@ Oops! Could not find any assets. libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html - 37 + 40 @@ -7319,15 +7335,15 @@ NEW apps/client/src/app/components/admin-settings/admin-settings.component.html - 14 + 15 - - Set API Key - Set API Key + + Set API key + Set API key apps/client/src/app/components/admin-settings/admin-settings.component.html - 29 + 48 @@ -7338,14 +7354,6 @@ 23 - - Notify me - Notify me - - apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html - 32 - - Get access to 100’000+ tickers from over 50 exchanges Get access to 100’000+ tickers from over 50 exchanges @@ -7452,6 +7460,62 @@ 69 + + of + of + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 29 + + + + daily requests + daily requests + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 31 + + + + Remove API key + Remove API key + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 38 + + + + Do you really want to delete the API key? + Do you really want to delete the API key? + + apps/client/src/app/components/admin-settings/admin-settings.component.ts + 80 + + + + Please enter your Ghostfolio API key: + Please enter your Ghostfolio API key: + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component.ts + 45 + + + + Notify me + Notify me + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 32 + + + + I have an API key + I have an API key + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 42 + + diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index 796fe7b1f..b57ae2c1e 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -166,7 +166,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 12 + 16 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 88 @@ -238,7 +242,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 26 + 25 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 102 @@ -1034,7 +1042,7 @@ Einloggen apps/client/src/app/app-routing.module.ts - 141 + 150 apps/client/src/app/components/header/header.component.ts @@ -1096,6 +1104,10 @@ or oder + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 35 + apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html 31 @@ -1346,7 +1358,7 @@ Tags apps/client/src/app/components/admin-settings/admin-settings.component.html - 45 + 65 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -1382,7 +1394,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 46 + 40 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 116 @@ -1966,7 +1982,7 @@ Features apps/client/src/app/app-routing.module.ts - 65 + 74 @@ -3982,7 +3998,7 @@ Plattformen apps/client/src/app/components/admin-settings/admin-settings.component.html - 39 + 59 @@ -4714,7 +4730,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 88 + 181 @@ -5461,7 +5477,7 @@ apps/client/src/app/components/admin-settings/admin-settings.component.ts - 48 + 61 apps/client/src/app/components/header/header.component.ts @@ -6623,7 +6639,7 @@ Internationalisierung apps/client/src/app/app-routing.module.ts - 79 + 88 @@ -6687,7 +6703,7 @@ Mehr anzeigen libs/ui/src/lib/top-holdings/top-holdings.component.html - 81 + 174 @@ -7303,7 +7319,7 @@ Ups! Es konnten leider keine Assets gefunden werden. libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html - 37 + 40 @@ -7319,15 +7335,15 @@ NEU apps/client/src/app/components/admin-settings/admin-settings.component.html - 14 + 15 - - Set API Key + + Set API key API-Schlüssel setzen apps/client/src/app/components/admin-settings/admin-settings.component.html - 29 + 48 @@ -7338,14 +7354,6 @@ 23 - - Notify me - Benachrichtige mich - - apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html - 32 - - Get access to 100’000+ tickers from over 50 exchanges Erhalte Zugang zu 100’000+ Tickern von über 50 Handelsplätzen @@ -7452,6 +7460,62 @@ 69 + + of + von + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 29 + + + + daily requests + täglichen Anfragen + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 31 + + + + Remove API key + API-Schlüssel löschen + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 38 + + + + Do you really want to delete the API key? + Möchtest du den API-Schlüssel wirklich löschen? + + apps/client/src/app/components/admin-settings/admin-settings.component.ts + 80 + + + + Please enter your Ghostfolio API key: + Bitte gib den API-Schlüssel ein: + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component.ts + 45 + + + + Notify me + Benachrichtige mich + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 32 + + + + I have an API key + Ich habe einen API-Schlüssel + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 42 + + diff --git a/apps/client/src/locales/messages.es.xlf b/apps/client/src/locales/messages.es.xlf index 70029fccb..13d3706bb 100644 --- a/apps/client/src/locales/messages.es.xlf +++ b/apps/client/src/locales/messages.es.xlf @@ -167,7 +167,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 12 + 16 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 88 @@ -239,7 +243,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 26 + 25 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 102 @@ -1035,7 +1043,7 @@ Iniciar sesión apps/client/src/app/app-routing.module.ts - 141 + 150 apps/client/src/app/components/header/header.component.ts @@ -1097,6 +1105,10 @@ or o + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 35 + apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html 31 @@ -1347,7 +1359,7 @@ Etiquetas apps/client/src/app/components/admin-settings/admin-settings.component.html - 45 + 65 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -1383,7 +1395,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 46 + 40 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 116 @@ -1967,7 +1983,7 @@ Funcionalidades apps/client/src/app/app-routing.module.ts - 65 + 74 @@ -3983,7 +3999,7 @@ Platforms apps/client/src/app/components/admin-settings/admin-settings.component.html - 39 + 59 @@ -4715,7 +4731,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 88 + 181 @@ -5462,7 +5478,7 @@ apps/client/src/app/components/admin-settings/admin-settings.component.ts - 48 + 61 apps/client/src/app/components/header/header.component.ts @@ -6624,7 +6640,7 @@ Internacionalización apps/client/src/app/app-routing.module.ts - 79 + 88 @@ -6688,7 +6704,7 @@ Mostrar más libs/ui/src/lib/top-holdings/top-holdings.component.html - 81 + 174 @@ -7304,7 +7320,7 @@ Oops! Could not find any assets. libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html - 37 + 40 @@ -7320,15 +7336,15 @@ NEW apps/client/src/app/components/admin-settings/admin-settings.component.html - 14 + 15 - - Set API Key - Set API Key + + Set API key + Set API key apps/client/src/app/components/admin-settings/admin-settings.component.html - 29 + 48 @@ -7339,14 +7355,6 @@ 23 - - Notify me - Notify me - - apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html - 32 - - Get access to 100’000+ tickers from over 50 exchanges Get access to 100’000+ tickers from over 50 exchanges @@ -7453,6 +7461,62 @@ 69 + + of + of + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 29 + + + + daily requests + daily requests + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 31 + + + + Remove API key + Remove API key + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 38 + + + + Do you really want to delete the API key? + Do you really want to delete the API key? + + apps/client/src/app/components/admin-settings/admin-settings.component.ts + 80 + + + + Please enter your Ghostfolio API key: + Please enter your Ghostfolio API key: + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component.ts + 45 + + + + Notify me + Notify me + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 32 + + + + I have an API key + I have an API key + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 42 + + diff --git a/apps/client/src/locales/messages.fr.xlf b/apps/client/src/locales/messages.fr.xlf index b442e4611..ee5c6eb63 100644 --- a/apps/client/src/locales/messages.fr.xlf +++ b/apps/client/src/locales/messages.fr.xlf @@ -178,7 +178,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 12 + 16 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 88 @@ -298,7 +302,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 26 + 25 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 102 @@ -954,7 +962,7 @@ Étiquettes apps/client/src/app/components/admin-settings/admin-settings.component.html - 45 + 65 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -1354,7 +1362,7 @@ Se connecter apps/client/src/app/app-routing.module.ts - 141 + 150 apps/client/src/app/components/header/header.component.ts @@ -1456,6 +1464,10 @@ or ou + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 35 + apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html 31 @@ -2378,7 +2390,7 @@ Fonctionnalités apps/client/src/app/app-routing.module.ts - 65 + 74 @@ -3150,7 +3162,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 46 + 40 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 116 @@ -3982,7 +3998,7 @@ Platformes apps/client/src/app/components/admin-settings/admin-settings.component.html - 39 + 59 @@ -4714,7 +4730,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 88 + 181 @@ -5461,7 +5477,7 @@ apps/client/src/app/components/admin-settings/admin-settings.component.ts - 48 + 61 apps/client/src/app/components/header/header.component.ts @@ -6623,7 +6639,7 @@ Internationalisation apps/client/src/app/app-routing.module.ts - 79 + 88 @@ -6687,7 +6703,7 @@ Voir plus libs/ui/src/lib/top-holdings/top-holdings.component.html - 81 + 174 @@ -7303,7 +7319,7 @@ Oops! Could not find any assets. libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html - 37 + 40 @@ -7319,15 +7335,15 @@ NEW apps/client/src/app/components/admin-settings/admin-settings.component.html - 14 + 15 - - Set API Key - Set API Key + + Set API key + Set API key apps/client/src/app/components/admin-settings/admin-settings.component.html - 29 + 48 @@ -7338,14 +7354,6 @@ 23 - - Notify me - Notify me - - apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html - 32 - - Get access to 100’000+ tickers from over 50 exchanges Get access to 100’000+ tickers from over 50 exchanges @@ -7452,6 +7460,62 @@ 69 + + of + of + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 29 + + + + daily requests + daily requests + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 31 + + + + Remove API key + Remove API key + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 38 + + + + Do you really want to delete the API key? + Do you really want to delete the API key? + + apps/client/src/app/components/admin-settings/admin-settings.component.ts + 80 + + + + Please enter your Ghostfolio API key: + Please enter your Ghostfolio API key: + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component.ts + 45 + + + + Notify me + Notify me + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 32 + + + + I have an API key + I have an API key + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 42 + + diff --git a/apps/client/src/locales/messages.it.xlf b/apps/client/src/locales/messages.it.xlf index 848e45d1e..5d8403757 100644 --- a/apps/client/src/locales/messages.it.xlf +++ b/apps/client/src/locales/messages.it.xlf @@ -167,7 +167,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 12 + 16 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 88 @@ -239,7 +243,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 26 + 25 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 102 @@ -1035,7 +1043,7 @@ Accedi apps/client/src/app/app-routing.module.ts - 141 + 150 apps/client/src/app/components/header/header.component.ts @@ -1097,6 +1105,10 @@ or oppure + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 35 + apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html 31 @@ -1347,7 +1359,7 @@ Tag apps/client/src/app/components/admin-settings/admin-settings.component.html - 45 + 65 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -1383,7 +1395,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 46 + 40 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 116 @@ -1967,7 +1983,7 @@ Funzionalità apps/client/src/app/app-routing.module.ts - 65 + 74 @@ -3983,7 +3999,7 @@ Piattaforme apps/client/src/app/components/admin-settings/admin-settings.component.html - 39 + 59 @@ -4715,7 +4731,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 88 + 181 @@ -5462,7 +5478,7 @@ apps/client/src/app/components/admin-settings/admin-settings.component.ts - 48 + 61 apps/client/src/app/components/header/header.component.ts @@ -6624,7 +6640,7 @@ Internazionalizzazione apps/client/src/app/app-routing.module.ts - 79 + 88 @@ -6688,7 +6704,7 @@ Visualizza di più libs/ui/src/lib/top-holdings/top-holdings.component.html - 81 + 174 @@ -7304,7 +7320,7 @@ Oops! Non ho trovato alcun asset. libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html - 37 + 40 @@ -7320,15 +7336,15 @@ NUOVO apps/client/src/app/components/admin-settings/admin-settings.component.html - 14 + 15 - - Set API Key - Imposta API Key + + Set API key + Imposta API Key apps/client/src/app/components/admin-settings/admin-settings.component.html - 29 + 48 @@ -7339,14 +7355,6 @@ 23 - - Notify me - Notificami - - apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html - 32 - - Get access to 100’000+ tickers from over 50 exchanges Ottieni accesso a oltre 100’000+ titoli da oltre 50 borse @@ -7453,6 +7461,62 @@ 69 + + of + of + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 29 + + + + daily requests + daily requests + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 31 + + + + Remove API key + Remove API key + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 38 + + + + Do you really want to delete the API key? + Do you really want to delete the API key? + + apps/client/src/app/components/admin-settings/admin-settings.component.ts + 80 + + + + Please enter your Ghostfolio API key: + Please enter your Ghostfolio API key: + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component.ts + 45 + + + + Notify me + Notify me + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 32 + + + + I have an API key + I have an API key + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 42 + + diff --git a/apps/client/src/locales/messages.nl.xlf b/apps/client/src/locales/messages.nl.xlf index f590449dc..b8dbd38a4 100644 --- a/apps/client/src/locales/messages.nl.xlf +++ b/apps/client/src/locales/messages.nl.xlf @@ -166,7 +166,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 12 + 16 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 88 @@ -238,7 +242,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 26 + 25 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 102 @@ -1034,7 +1042,7 @@ Aanmelden apps/client/src/app/app-routing.module.ts - 141 + 150 apps/client/src/app/components/header/header.component.ts @@ -1096,6 +1104,10 @@ or of + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 35 + apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html 31 @@ -1346,7 +1358,7 @@ Tags apps/client/src/app/components/admin-settings/admin-settings.component.html - 45 + 65 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -1382,7 +1394,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 46 + 40 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 116 @@ -1966,7 +1982,7 @@ Functionaliteiten apps/client/src/app/app-routing.module.ts - 65 + 74 @@ -3982,7 +3998,7 @@ Platforms apps/client/src/app/components/admin-settings/admin-settings.component.html - 39 + 59 @@ -4714,7 +4730,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 88 + 181 @@ -5461,7 +5477,7 @@ apps/client/src/app/components/admin-settings/admin-settings.component.ts - 48 + 61 apps/client/src/app/components/header/header.component.ts @@ -6623,7 +6639,7 @@ Internationalization apps/client/src/app/app-routing.module.ts - 79 + 88 @@ -6687,7 +6703,7 @@ Show more libs/ui/src/lib/top-holdings/top-holdings.component.html - 81 + 174 @@ -7303,7 +7319,7 @@ Oops! Could not find any assets. libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html - 37 + 40 @@ -7319,15 +7335,15 @@ NEW apps/client/src/app/components/admin-settings/admin-settings.component.html - 14 + 15 - - Set API Key - Set API Key + + Set API key + Set API key apps/client/src/app/components/admin-settings/admin-settings.component.html - 29 + 48 @@ -7338,14 +7354,6 @@ 23 - - Notify me - Notify me - - apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html - 32 - - Get access to 100’000+ tickers from over 50 exchanges Get access to 100’000+ tickers from over 50 exchanges @@ -7452,6 +7460,62 @@ 69 + + of + of + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 29 + + + + daily requests + daily requests + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 31 + + + + Remove API key + Remove API key + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 38 + + + + Do you really want to delete the API key? + Do you really want to delete the API key? + + apps/client/src/app/components/admin-settings/admin-settings.component.ts + 80 + + + + Please enter your Ghostfolio API key: + Please enter your Ghostfolio API key: + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component.ts + 45 + + + + Notify me + Notify me + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 32 + + + + I have an API key + I have an API key + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 42 + + diff --git a/apps/client/src/locales/messages.pl.xlf b/apps/client/src/locales/messages.pl.xlf index 04b156715..76e4decb7 100644 --- a/apps/client/src/locales/messages.pl.xlf +++ b/apps/client/src/locales/messages.pl.xlf @@ -260,7 +260,7 @@ apps/client/src/app/components/admin-settings/admin-settings.component.ts - 48 + 61 apps/client/src/app/components/header/header.component.ts @@ -979,7 +979,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 12 + 16 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 88 @@ -1083,7 +1087,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 26 + 25 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 102 @@ -2019,7 +2027,7 @@ Platformy apps/client/src/app/components/admin-settings/admin-settings.component.html - 39 + 59 @@ -2027,7 +2035,7 @@ Tagi apps/client/src/app/components/admin-settings/admin-settings.component.html - 45 + 65 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -2295,7 +2303,7 @@ Zaloguj się apps/client/src/app/app-routing.module.ts - 141 + 150 apps/client/src/app/components/header/header.component.ts @@ -2481,6 +2489,10 @@ or lub + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 35 + apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html 31 @@ -3583,7 +3595,7 @@ Funkcje apps/client/src/app/app-routing.module.ts - 65 + 74 @@ -4599,7 +4611,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 88 + 181 @@ -5675,7 +5687,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 46 + 40 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 116 @@ -6623,7 +6639,7 @@ Internacjonalizacja apps/client/src/app/app-routing.module.ts - 79 + 88 @@ -6687,7 +6703,7 @@ Pokaż więcej libs/ui/src/lib/top-holdings/top-holdings.component.html - 81 + 174 @@ -7303,7 +7319,7 @@ Oops! Could not find any assets. libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html - 37 + 40 @@ -7319,15 +7335,15 @@ NEW apps/client/src/app/components/admin-settings/admin-settings.component.html - 14 + 15 - - Set API Key - Set API Key + + Set API key + Set API key apps/client/src/app/components/admin-settings/admin-settings.component.html - 29 + 48 @@ -7338,14 +7354,6 @@ 23 - - Notify me - Notify me - - apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html - 32 - - Get access to 100’000+ tickers from over 50 exchanges Get access to 100’000+ tickers from over 50 exchanges @@ -7452,6 +7460,62 @@ 69 + + of + of + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 29 + + + + daily requests + daily requests + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 31 + + + + Remove API key + Remove API key + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 38 + + + + Do you really want to delete the API key? + Do you really want to delete the API key? + + apps/client/src/app/components/admin-settings/admin-settings.component.ts + 80 + + + + Please enter your Ghostfolio API key: + Please enter your Ghostfolio API key: + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component.ts + 45 + + + + Notify me + Notify me + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 32 + + + + I have an API key + I have an API key + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 42 + + diff --git a/apps/client/src/locales/messages.pt.xlf b/apps/client/src/locales/messages.pt.xlf index bbaa17cde..404e4d2bc 100644 --- a/apps/client/src/locales/messages.pt.xlf +++ b/apps/client/src/locales/messages.pt.xlf @@ -178,7 +178,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 12 + 16 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 88 @@ -298,7 +302,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 26 + 25 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 102 @@ -1218,7 +1226,7 @@ Iniciar sessão apps/client/src/app/app-routing.module.ts - 141 + 150 apps/client/src/app/components/header/header.component.ts @@ -1328,6 +1336,10 @@ or ou + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 35 + apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html 31 @@ -1650,7 +1662,7 @@ Marcadores apps/client/src/app/components/admin-settings/admin-settings.component.html - 45 + 65 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -1686,7 +1698,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 46 + 40 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 116 @@ -2298,7 +2314,7 @@ Funcionalidades apps/client/src/app/app-routing.module.ts - 65 + 74 @@ -3982,7 +3998,7 @@ Plataformas apps/client/src/app/components/admin-settings/admin-settings.component.html - 39 + 59 @@ -4714,7 +4730,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 88 + 181 @@ -5461,7 +5477,7 @@ apps/client/src/app/components/admin-settings/admin-settings.component.ts - 48 + 61 apps/client/src/app/components/header/header.component.ts @@ -6623,7 +6639,7 @@ Internationalization apps/client/src/app/app-routing.module.ts - 79 + 88 @@ -6687,7 +6703,7 @@ Show more libs/ui/src/lib/top-holdings/top-holdings.component.html - 81 + 174 @@ -7303,7 +7319,7 @@ Oops! Could not find any assets. libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html - 37 + 40 @@ -7319,15 +7335,15 @@ NEW apps/client/src/app/components/admin-settings/admin-settings.component.html - 14 + 15 - - Set API Key - Set API Key + + Set API key + Set API key apps/client/src/app/components/admin-settings/admin-settings.component.html - 29 + 48 @@ -7338,14 +7354,6 @@ 23 - - Notify me - Notify me - - apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html - 32 - - Get access to 100’000+ tickers from over 50 exchanges Get access to 100’000+ tickers from over 50 exchanges @@ -7452,6 +7460,62 @@ 69 + + of + of + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 29 + + + + daily requests + daily requests + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 31 + + + + Remove API key + Remove API key + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 38 + + + + Do you really want to delete the API key? + Do you really want to delete the API key? + + apps/client/src/app/components/admin-settings/admin-settings.component.ts + 80 + + + + Please enter your Ghostfolio API key: + Please enter your Ghostfolio API key: + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component.ts + 45 + + + + Notify me + Notify me + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 32 + + + + I have an API key + I have an API key + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 42 + + diff --git a/apps/client/src/locales/messages.tr.xlf b/apps/client/src/locales/messages.tr.xlf index 3243ae468..307817d03 100644 --- a/apps/client/src/locales/messages.tr.xlf +++ b/apps/client/src/locales/messages.tr.xlf @@ -260,7 +260,7 @@ apps/client/src/app/components/admin-settings/admin-settings.component.ts - 48 + 61 apps/client/src/app/components/header/header.component.ts @@ -943,7 +943,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 12 + 16 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 88 @@ -1047,7 +1051,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 26 + 25 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 102 @@ -1763,7 +1771,7 @@ Etiketler apps/client/src/app/components/admin-settings/admin-settings.component.html - 45 + 65 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -1935,7 +1943,7 @@ Platformlar apps/client/src/app/components/admin-settings/admin-settings.component.html - 39 + 59 @@ -2147,7 +2155,7 @@ Giriş apps/client/src/app/app-routing.module.ts - 141 + 150 apps/client/src/app/components/header/header.component.ts @@ -2333,6 +2341,10 @@ or veya + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 35 + apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html 31 @@ -3135,7 +3147,7 @@ Özellikler apps/client/src/app/app-routing.module.ts - 65 + 74 @@ -4087,7 +4099,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 88 + 181 @@ -5351,7 +5363,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 46 + 40 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 116 @@ -6623,7 +6639,7 @@ Internationalization apps/client/src/app/app-routing.module.ts - 79 + 88 @@ -6687,7 +6703,7 @@ Show more libs/ui/src/lib/top-holdings/top-holdings.component.html - 81 + 174 @@ -7303,7 +7319,7 @@ Oops! Could not find any assets. libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html - 37 + 40 @@ -7319,15 +7335,15 @@ NEW apps/client/src/app/components/admin-settings/admin-settings.component.html - 14 + 15 - - Set API Key - Set API Key + + Set API key + Set API key apps/client/src/app/components/admin-settings/admin-settings.component.html - 29 + 48 @@ -7338,14 +7354,6 @@ 23 - - Notify me - Notify me - - apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html - 32 - - Get access to 100’000+ tickers from over 50 exchanges Get access to 100’000+ tickers from over 50 exchanges @@ -7452,6 +7460,62 @@ 69 + + of + of + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 29 + + + + daily requests + daily requests + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 31 + + + + Remove API key + Remove API key + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 38 + + + + Do you really want to delete the API key? + Do you really want to delete the API key? + + apps/client/src/app/components/admin-settings/admin-settings.component.ts + 80 + + + + Please enter your Ghostfolio API key: + Please enter your Ghostfolio API key: + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component.ts + 45 + + + + Notify me + Notify me + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 32 + + + + I have an API key + I have an API key + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 42 + + diff --git a/apps/client/src/locales/messages.xlf b/apps/client/src/locales/messages.xlf index db47d7127..51a7c32e7 100644 --- a/apps/client/src/locales/messages.xlf +++ b/apps/client/src/locales/messages.xlf @@ -255,7 +255,7 @@ apps/client/src/app/components/admin-settings/admin-settings.component.ts - 48 + 61 apps/client/src/app/components/header/header.component.ts @@ -951,7 +951,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 12 + 16 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 88 @@ -1052,7 +1056,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 26 + 25 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 102 @@ -1917,14 +1925,14 @@ Platforms apps/client/src/app/components/admin-settings/admin-settings.component.html - 39 + 59 Tags apps/client/src/app/components/admin-settings/admin-settings.component.html - 45 + 65 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -2165,7 +2173,7 @@ Sign in apps/client/src/app/app-routing.module.ts - 141 + 150 apps/client/src/app/components/header/header.component.ts @@ -2331,6 +2339,10 @@ or + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 35 + apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html 31 @@ -3326,7 +3338,7 @@ Features apps/client/src/app/app-routing.module.ts - 65 + 74 @@ -4231,7 +4243,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 88 + 181 @@ -5237,7 +5249,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 46 + 40 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 116 @@ -6016,7 +6032,7 @@ Internationalization apps/client/src/app/app-routing.module.ts - 79 + 88 @@ -6072,7 +6088,7 @@ Show more libs/ui/src/lib/top-holdings/top-holdings.component.html - 81 + 174 @@ -6611,7 +6627,7 @@ Oops! Could not find any assets. libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html - 37 + 40 @@ -6621,13 +6637,6 @@ 23 - - Notify me - - apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html - 32 - - Ukraine @@ -6635,11 +6644,11 @@ 92 - - Set API Key + + Set API key apps/client/src/app/components/admin-settings/admin-settings.component.html - 29 + 48 @@ -6653,7 +6662,7 @@ NEW apps/client/src/app/components/admin-settings/admin-settings.component.html - 14 + 15 @@ -6744,6 +6753,55 @@ 5 + + Please enter your Ghostfolio API key: + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component.ts + 45 + + + + of + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 29 + + + + Notify me + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 32 + + + + Do you really want to delete the API key? + + apps/client/src/app/components/admin-settings/admin-settings.component.ts + 80 + + + + I have an API key + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 42 + + + + Remove API key + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 38 + + + + daily requests + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 31 + + diff --git a/apps/client/src/locales/messages.zh.xlf b/apps/client/src/locales/messages.zh.xlf index a1c434da2..d6b9d7bf8 100644 --- a/apps/client/src/locales/messages.zh.xlf +++ b/apps/client/src/locales/messages.zh.xlf @@ -261,7 +261,7 @@ apps/client/src/app/components/admin-settings/admin-settings.component.ts - 48 + 61 apps/client/src/app/components/header/header.component.ts @@ -988,7 +988,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 12 + 16 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 88 @@ -1092,7 +1096,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 26 + 25 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 102 @@ -2036,7 +2044,7 @@ 平台 apps/client/src/app/components/admin-settings/admin-settings.component.html - 39 + 59 @@ -2044,7 +2052,7 @@ 标签 apps/client/src/app/components/admin-settings/admin-settings.component.html - 45 + 65 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -2312,7 +2320,7 @@ 登入 apps/client/src/app/app-routing.module.ts - 141 + 150 apps/client/src/app/components/header/header.component.ts @@ -2498,6 +2506,10 @@ or + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 35 + apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html 31 @@ -3600,7 +3612,7 @@ 功能 apps/client/src/app/app-routing.module.ts - 65 + 74 @@ -4616,7 +4628,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 88 + 181 @@ -5740,7 +5752,11 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 46 + 40 + + + libs/ui/src/lib/top-holdings/top-holdings.component.html + 116 @@ -6624,7 +6640,7 @@ Internationalization apps/client/src/app/app-routing.module.ts - 79 + 88 @@ -6688,7 +6704,7 @@ Show more libs/ui/src/lib/top-holdings/top-holdings.component.html - 81 + 174 @@ -7304,7 +7320,7 @@ Oops! Could not find any assets. libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html - 37 + 40 @@ -7320,15 +7336,15 @@ NEW apps/client/src/app/components/admin-settings/admin-settings.component.html - 14 + 15 - - Set API Key - Set API Key + + Set API key + Set API key apps/client/src/app/components/admin-settings/admin-settings.component.html - 29 + 48 @@ -7339,14 +7355,6 @@ 23 - - Notify me - Notify me - - apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html - 32 - - Get access to 100’000+ tickers from over 50 exchanges Get access to 100’000+ tickers from over 50 exchanges @@ -7453,6 +7461,62 @@ 69 + + of + of + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 29 + + + + daily requests + daily requests + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 31 + + + + Remove API key + Remove API key + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 38 + + + + Do you really want to delete the API key? + Do you really want to delete the API key? + + apps/client/src/app/components/admin-settings/admin-settings.component.ts + 80 + + + + Please enter your Ghostfolio API key: + Please enter your Ghostfolio API key: + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component.ts + 45 + + + + Notify me + Notify me + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 32 + + + + I have an API key + I have an API key + + apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html + 42 + + diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index fbd416bb7..7608e43a8 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -106,17 +106,21 @@ export const PORTFOLIO_SNAPSHOT_PROCESS_JOB_OPTIONS: JobOptions = { export const HEADER_KEY_IMPERSONATION = 'Impersonation-Id'; export const HEADER_KEY_TIMEZONE = 'Timezone'; export const HEADER_KEY_TOKEN = 'Authorization'; +export const HEADER_KEY_SKIP_INTERCEPTOR = 'X-Skip-Interceptor'; export const MAX_TOP_HOLDINGS = 50; export const NUMERICAL_PRECISION_THRESHOLD = 100000; +export const PROPERTY_API_KEY_GHOSTFOLIO = 'API_KEY_GHOSTFOLIO'; export const PROPERTY_BENCHMARKS = 'BENCHMARKS'; export const PROPERTY_BETTER_UPTIME_MONITOR_ID = 'BETTER_UPTIME_MONITOR_ID'; export const PROPERTY_COUNTRIES_OF_SUBSCRIBERS = 'COUNTRIES_OF_SUBSCRIBERS'; export const PROPERTY_COUPONS = 'COUPONS'; export const PROPERTY_CURRENCIES = 'CURRENCIES'; export const PROPERTY_DATA_SOURCE_MAPPING = 'DATA_SOURCE_MAPPING'; +export const PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS = + 'DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS'; export const PROPERTY_DEMO_USER_ID = 'DEMO_USER_ID'; export const PROPERTY_IS_DATA_GATHERING_ENABLED = 'IS_DATA_GATHERING_ENABLED'; export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE'; diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index eb28a6d16..380937949 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -40,13 +40,16 @@ import type { Position } from './position.interface'; import type { Product } from './product'; import type { AccountBalancesResponse } from './responses/account-balances-response.interface'; import type { BenchmarkResponse } from './responses/benchmark-response.interface'; +import type { DataProviderGhostfolioStatusResponse } from './responses/data-provider-ghostfolio-status-response.interface'; import type { ResponseError } from './responses/errors.interface'; +import type { HistoricalResponse } from './responses/historical-response.interface'; import type { ImportResponse } from './responses/import-response.interface'; import type { LookupResponse } from './responses/lookup-response.interface'; import type { OAuthResponse } from './responses/oauth-response.interface'; import type { PortfolioHoldingsResponse } from './responses/portfolio-holdings-response.interface'; import type { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface'; import type { PublicPortfolioResponse } from './responses/public-portfolio-response.interface'; +import type { QuotesResponse } from './responses/quotes-response.interface'; import type { ScraperConfiguration } from './scraper-configuration.interface'; import type { Statistics } from './statistics.interface'; import type { SubscriptionOffer } from './subscription-offer.interface'; @@ -74,12 +77,14 @@ export { BenchmarkProperty, BenchmarkResponse, Coupon, + DataProviderGhostfolioStatusResponse, DataProviderInfo, EnhancedSymbolProfile, Export, Filter, FilterGroup, HistoricalDataItem, + HistoricalResponse, Holding, HoldingWithParents, ImportResponse, @@ -105,6 +110,7 @@ export { Position, Product, PublicPortfolioResponse, + QuotesResponse, ResponseError, ScraperConfiguration, Statistics, diff --git a/libs/common/src/lib/interfaces/responses/data-provider-ghostfolio-status-response.interface.ts b/libs/common/src/lib/interfaces/responses/data-provider-ghostfolio-status-response.interface.ts new file mode 100644 index 000000000..11e9779d2 --- /dev/null +++ b/libs/common/src/lib/interfaces/responses/data-provider-ghostfolio-status-response.interface.ts @@ -0,0 +1,4 @@ +export interface DataProviderGhostfolioStatusResponse { + dailyRequests: number; + dailyRequestsMax: number; +} diff --git a/libs/common/src/lib/interfaces/responses/historical-response.interface.ts b/libs/common/src/lib/interfaces/responses/historical-response.interface.ts new file mode 100644 index 000000000..12309a352 --- /dev/null +++ b/libs/common/src/lib/interfaces/responses/historical-response.interface.ts @@ -0,0 +1,7 @@ +import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; + +export interface HistoricalResponse { + historicalData: { + [date: string]: IDataProviderHistoricalResponse; + }; +} diff --git a/libs/common/src/lib/interfaces/responses/quotes-response.interface.ts b/libs/common/src/lib/interfaces/responses/quotes-response.interface.ts new file mode 100644 index 000000000..79c9d3024 --- /dev/null +++ b/libs/common/src/lib/interfaces/responses/quotes-response.interface.ts @@ -0,0 +1,5 @@ +import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; + +export interface QuotesResponse { + quotes: { [symbol: string]: IDataProviderResponse }; +} diff --git a/libs/common/src/lib/permissions.ts b/libs/common/src/lib/permissions.ts index ab443ea5e..1a81938b5 100644 --- a/libs/common/src/lib/permissions.ts +++ b/libs/common/src/lib/permissions.ts @@ -22,6 +22,7 @@ export const permissions = { deletePlatform: 'deletePlatform', deleteTag: 'deleteTag', deleteUser: 'deleteUser', + enableDataProviderGhostfolio: 'enableDataProviderGhostfolio', enableFearAndGreedIndex: 'enableFearAndGreedIndex', enableImport: 'enableImport', enableBlog: 'enableBlog', diff --git a/libs/common/src/lib/types/user-with-settings.type.ts b/libs/common/src/lib/types/user-with-settings.type.ts index 2a669d26f..5f9835176 100644 --- a/libs/common/src/lib/types/user-with-settings.type.ts +++ b/libs/common/src/lib/types/user-with-settings.type.ts @@ -9,6 +9,7 @@ export type UserWithSettings = User & { Access: Access[]; Account: Account[]; activityCount: number; + dataProviderGhostfolioDailyRequests: number; permissions?: string[]; Settings: Settings & { settings: UserSettings }; subscription?: { diff --git a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html index d055a618a..9d1038e0f 100644 --- a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html +++ b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html @@ -29,6 +29,9 @@ @if (lookupItem.assetSubClass) { · {{ lookupItem.assetSubClassString }} } + @if (lookupItem.dataProviderInfo.name) { + · {{ lookupItem.dataProviderInfo.name }} + } } @empty { diff --git a/prisma/migrations/20241103110114_added_ghostfolio_to_data_source/migration.sql b/prisma/migrations/20241103110114_added_ghostfolio_to_data_source/migration.sql new file mode 100644 index 000000000..9687a87b0 --- /dev/null +++ b/prisma/migrations/20241103110114_added_ghostfolio_to_data_source/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "DataSource" ADD VALUE 'GHOSTFOLIO'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5a34e8e11..5fe4ce7c3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -281,6 +281,7 @@ enum DataSource { COINGECKO EOD_HISTORICAL_DATA FINANCIAL_MODELING_PREP + GHOSTFOLIO GOOGLE_SHEETS MANUAL RAPID_API