diff --git a/CHANGELOG.md b/CHANGELOG.md index daa650aa7..87f700f30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Changed the column label from _Index_ to _Name_ in the benchmark component +- Extended the data providers management of the admin control panel - Improved the language localization for German (`de`) ## 2.156.0 - 2025-04-27 diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index e9952ea08..d8507bbb0 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -68,7 +68,7 @@ export class AdminController { @HasPermission(permissions.accessAdminControl) @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getAdminData(): Promise { - return this.adminService.get(); + return this.adminService.get({ user: this.request.user }); } @HasPermission(permissions.accessAdminControl) diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index e72902704..ce8adaf65 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -29,7 +29,7 @@ import { Filter } from '@ghostfolio/common/interfaces'; import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; -import { MarketDataPreset } from '@ghostfolio/common/types'; +import { MarketDataPreset, UserWithSettings } from '@ghostfolio/common/types'; import { BadRequestException, @@ -134,7 +134,9 @@ export class AdminService { } } - public async get(): Promise { + public async get({ user }: { user: UserWithSettings }): Promise { + const dataSources = await this.dataProviderService.getDataSources({ user }); + const [settings, transactionCount, userCount] = await Promise.all([ this.propertyService.get(), this.prismaService.order.count(), @@ -145,6 +147,11 @@ export class AdminService { settings, transactionCount, userCount, + dataProviders: dataSources.map((dataSource) => { + return this.dataProviderService + .getDataProvider(dataSource) + .getDataProviderInfo(); + }), version: environment.version }; } 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 index 7281697bd..bdaecf718 100644 --- a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts +++ b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts @@ -1,5 +1,6 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; +import { GhostfolioService as GhostfolioDataProviderService } from '@ghostfolio/api/services/data-provider/ghostfolio/ghostfolio.service'; import { GetAssetProfileParams, GetDividendsParams, @@ -327,10 +328,15 @@ export class GhostfolioService { } private getDataProviderInfo(): DataProviderInfo { + const ghostfolioDataProviderService = new GhostfolioDataProviderService( + this.configurationService, + this.propertyService + ); + return { + ...ghostfolioDataProviderService.getDataProviderInfo(), isPremium: false, - name: 'Ghostfolio Premium', - url: 'https://ghostfol.io' + name: 'Ghostfolio Premium' }; } diff --git a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts index f9593f0d0..1e8f7eefa 100644 --- a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts +++ b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts @@ -52,6 +52,7 @@ export class AlphaVantageService implements DataProviderInterface { public getDataProviderInfo(): DataProviderInfo { return { + dataSource: DataSource.ALPHA_VANTAGE, isPremium: false, name: 'Alpha Vantage', url: 'https://www.alphavantage.co' diff --git a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts index d53355b9c..7776ff46c 100644 --- a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts +++ b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts @@ -92,6 +92,7 @@ export class CoinGeckoService implements DataProviderInterface { public getDataProviderInfo(): DataProviderInfo { return { + dataSource: DataSource.COINGECKO, isPremium: false, name: 'CoinGecko', url: 'https://coingecko.com' 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 a4edd5bfa..3d8f2e553 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -26,6 +26,7 @@ import { LookupItem, LookupResponse } from '@ghostfolio/common/interfaces'; +import { hasRole } from '@ghostfolio/common/permissions'; import type { Granularity, UserWithSettings } from '@ghostfolio/common/types'; import { Inject, Injectable, Logger } from '@nestjs/common'; @@ -169,6 +170,7 @@ export class DataProviderService { let dataSourcesKey: 'DATA_SOURCES' | 'DATA_SOURCES_LEGACY' = 'DATA_SOURCES'; if ( + !hasRole(user, 'ADMIN') && isBefore(user.createdAt, new Date('2025-03-23')) && this.configurationService.get('DATA_SOURCES_LEGACY')?.length > 0 ) { @@ -185,7 +187,7 @@ export class DataProviderService { PROPERTY_API_KEY_GHOSTFOLIO )) as string; - if (ghostfolioApiKey) { + if (ghostfolioApiKey || hasRole(user, 'ADMIN')) { dataSources.push('GHOSTFOLIO'); } @@ -670,6 +672,7 @@ export class DataProviderService { lookupItem.dataProviderInfo.isPremium = false; } + lookupItem.dataProviderInfo.dataSource = undefined; lookupItem.dataProviderInfo.name = undefined; lookupItem.dataProviderInfo.url = undefined; } else { diff --git a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts index 376b8f159..ddb94bb1a 100644 --- a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts +++ b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts @@ -68,6 +68,7 @@ export class EodHistoricalDataService implements DataProviderInterface { public getDataProviderInfo(): DataProviderInfo { return { + dataSource: DataSource.EOD_HISTORICAL_DATA, isPremium: true, name: 'EOD Historical Data', url: 'https://eodhd.com' diff --git a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts index 4e42201d0..d0e674c4d 100644 --- a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts +++ b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts @@ -223,6 +223,7 @@ export class FinancialModelingPrepService implements DataProviderInterface { public getDataProviderInfo(): DataProviderInfo { return { + dataSource: DataSource.FINANCIAL_MODELING_PREP, isPremium: true, name: 'Financial Modeling Prep', url: 'https://financialmodelingprep.com/developer/docs' diff --git a/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts b/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts index 097464e2f..90354ace5 100644 --- a/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts +++ b/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts @@ -92,9 +92,10 @@ export class GhostfolioService implements DataProviderInterface { public getDataProviderInfo(): DataProviderInfo { return { + dataSource: DataSource.GHOSTFOLIO, isPremium: true, name: 'Ghostfolio', - url: 'https://ghostfo.io' + url: 'https://ghostfol.io' }; } diff --git a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts index 0c466972d..f067f042c 100644 --- a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts +++ b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts @@ -47,6 +47,7 @@ export class GoogleSheetsService implements DataProviderInterface { public getDataProviderInfo(): DataProviderInfo { return { + dataSource: DataSource.GOOGLE_SHEETS, isPremium: false, name: 'Google Sheets', url: 'https://docs.google.com/spreadsheets' diff --git a/apps/api/src/services/data-provider/manual/manual.service.ts b/apps/api/src/services/data-provider/manual/manual.service.ts index 331806098..66e625e47 100644 --- a/apps/api/src/services/data-provider/manual/manual.service.ts +++ b/apps/api/src/services/data-provider/manual/manual.service.ts @@ -64,6 +64,7 @@ export class ManualService implements DataProviderInterface { public getDataProviderInfo(): DataProviderInfo { return { + dataSource: DataSource.MANUAL, isPremium: false }; } diff --git a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts index 7762be426..05f1c0e5d 100644 --- a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts +++ b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts @@ -43,6 +43,7 @@ export class RapidApiService implements DataProviderInterface { public getDataProviderInfo(): DataProviderInfo { return { + dataSource: DataSource.RAPID_API, isPremium: false, name: 'Rapid API', url: 'https://rapidapi.com' diff --git a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts index 6b42c9283..b9be5553e 100644 --- a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts +++ b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts @@ -51,6 +51,7 @@ export class YahooFinanceService implements DataProviderInterface { public getDataProviderInfo(): DataProviderInfo { return { + dataSource: DataSource.YAHOO, isPremium: false, name: 'Yahoo Finance', url: 'https://finance.yahoo.com' 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 305d6ce49..977c8a372 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 @@ -4,74 +4,100 @@

Data Providers

-
-
- - @if (isGhostfolioApiKeyValid === false) { - Early Access - } - Ghostfolio Premium - - - @if (isGhostfolioApiKeyValid === true) { -
- - Valid until - {{ - ghostfolioApiStatus?.subscription?.expiresAt - | date: defaultDateFormat - }} -
- } -
-
- @if (isGhostfolioApiKeyValid === true) { -
-
- {{ ghostfolioApiStatus.dailyRequests }} - of - {{ ghostfolioApiStatus.dailyRequestsMax }} - daily requests + @for (dataProvider of dataProviders; track dataProvider.name) { +
+ @if (dataProvider.name === 'Ghostfolio') { +
+
+ +
+ + Ghostfolio Premium + + @if (isGhostfolioApiKeyValid === false) { + Early Access + } + + @if (isGhostfolioApiKeyValid === true) { +
+ + Valid until + {{ + ghostfolioApiStatus?.subscription?.expiresAt + | date: defaultDateFormat + }} +
+ } +
- - -
+
+ @if (isGhostfolioApiKeyValid === true) { +
+
+ {{ ghostfolioApiStatus.dailyRequests }} + of + {{ ghostfolioApiStatus.dailyRequestsMax }} + daily requests +
+ + + + +
+ } @else if (isGhostfolioApiKeyValid === false) { + - + } +
+ } @else { +
+
+ + {{ dataProvider.name }} +
- } @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 d72466bb4..68c196962 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 @@ -10,6 +10,7 @@ import { import { getDateFormatString } from '@ghostfolio/common/helper'; import { DataProviderGhostfolioStatusResponse, + DataProviderInfo, User } from '@ghostfolio/common/interfaces'; @@ -35,6 +36,7 @@ import { GhostfolioPremiumApiDialogParams } from './ghostfolio-premium-api-dialo standalone: false }) export class AdminSettingsComponent implements OnDestroy, OnInit { + public dataProviders: DataProviderInfo[]; public defaultDateFormat: string; public ghostfolioApiStatus: DataProviderGhostfolioStatusResponse; public isGhostfolioApiKeyValid: boolean; @@ -124,23 +126,36 @@ export class AdminSettingsComponent implements OnDestroy, OnInit { 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; + .fetchAdminData() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ dataProviders, settings }) => { + this.dataProviders = dataProviders.filter(({ dataSource }) => { + return dataSource !== 'MANUAL'; + }); + + this.adminService + .fetchGhostfolioDataProviderStatus( + settings[PROPERTY_API_KEY_GHOSTFOLIO] as string + ) + .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(); + }); this.changeDetectorRef.markForCheck(); }); diff --git a/apps/client/src/app/components/admin-settings/admin-settings.module.ts b/apps/client/src/app/components/admin-settings/admin-settings.module.ts index 5a5c39cde..79b269a62 100644 --- a/apps/client/src/app/components/admin-settings/admin-settings.module.ts +++ b/apps/client/src/app/components/admin-settings/admin-settings.module.ts @@ -1,5 +1,6 @@ import { GfAdminPlatformModule } from '@ghostfolio/client/components/admin-platform/admin-platform.module'; import { GfAdminTagModule } from '@ghostfolio/client/components/admin-tag/admin-tag.module'; +import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { CommonModule } from '@angular/common'; @@ -17,6 +18,7 @@ import { AdminSettingsComponent } from './admin-settings.component'; CommonModule, GfAdminPlatformModule, GfAdminTagModule, + GfAssetProfileIconComponent, GfPremiumIndicatorComponent, MatButtonModule, MatCardModule, diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index 89769122d..cb72fb9fd 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -4,8 +4,7 @@ import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform. import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { HEADER_KEY_SKIP_INTERCEPTOR, - HEADER_KEY_TOKEN, - PROPERTY_API_KEY_GHOSTFOLIO + HEADER_KEY_TOKEN } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { @@ -24,7 +23,6 @@ import { Injectable } from '@angular/core'; import { SortDirection } from '@angular/material/sort'; import { DataSource, MarketData, Platform } from '@prisma/client'; import { JobStatus } from 'bull'; -import { switchMap } from 'rxjs'; import { environment } from '../../environments/environment'; import { DataService } from './data.service'; @@ -115,19 +113,15 @@ export class AdminService { }); } - public fetchGhostfolioDataProviderStatus() { - return this.fetchAdminData().pipe( - switchMap(({ settings }) => { - const headers = new HttpHeaders({ - [HEADER_KEY_SKIP_INTERCEPTOR]: 'true', - [HEADER_KEY_TOKEN]: `Api-Key ${settings[PROPERTY_API_KEY_GHOSTFOLIO]}` - }); - - return this.http.get( - `${environment.production ? 'https://ghostfol.io' : ''}/api/v2/data-providers/ghostfolio/status`, - { headers } - ); - }) + public fetchGhostfolioDataProviderStatus(aApiKey: string) { + const headers = new HttpHeaders({ + [HEADER_KEY_SKIP_INTERCEPTOR]: 'true', + [HEADER_KEY_TOKEN]: `Api-Key ${aApiKey}` + }); + + return this.http.get( + `${environment.production ? 'https://ghostfol.io' : ''}/api/v2/data-providers/ghostfolio/status`, + { headers } ); } diff --git a/libs/common/src/lib/interfaces/admin-data.interface.ts b/libs/common/src/lib/interfaces/admin-data.interface.ts index e14429493..dba85d3ef 100644 --- a/libs/common/src/lib/interfaces/admin-data.interface.ts +++ b/libs/common/src/lib/interfaces/admin-data.interface.ts @@ -1,4 +1,7 @@ +import { DataProviderInfo } from './data-provider-info.interface'; + export interface AdminData { + dataProviders: DataProviderInfo[]; settings: { [key: string]: boolean | object | string | string[] }; transactionCount: number; userCount: number; diff --git a/libs/common/src/lib/interfaces/data-provider-info.interface.ts b/libs/common/src/lib/interfaces/data-provider-info.interface.ts index 79d7d6940..9fba0e62d 100644 --- a/libs/common/src/lib/interfaces/data-provider-info.interface.ts +++ b/libs/common/src/lib/interfaces/data-provider-info.interface.ts @@ -1,4 +1,7 @@ +import { DataSource } from '@prisma/client'; + export interface DataProviderInfo { + dataSource?: DataSource; isPremium: boolean; name?: string; url?: string;