Browse Source

Feature/extend data providers management of admin control panel (#4615)

* Extend data providers management

* Update changelog
pull/4622/head
Thomas Kaul 2 days ago
committed by GitHub
parent
commit
b90bfc3d6e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 2
      apps/api/src/app/admin/admin.controller.ts
  3. 11
      apps/api/src/app/admin/admin.service.ts
  4. 10
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  5. 1
      apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts
  6. 1
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  7. 5
      apps/api/src/services/data-provider/data-provider.service.ts
  8. 1
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  9. 1
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  10. 3
      apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts
  11. 1
      apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts
  12. 1
      apps/api/src/services/data-provider/manual/manual.service.ts
  13. 1
      apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
  14. 1
      apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
  15. 152
      apps/client/src/app/components/admin-settings/admin-settings.component.html
  16. 49
      apps/client/src/app/components/admin-settings/admin-settings.component.ts
  17. 2
      apps/client/src/app/components/admin-settings/admin-settings.module.ts
  18. 26
      apps/client/src/app/services/admin.service.ts
  19. 3
      libs/common/src/lib/interfaces/admin-data.interface.ts
  20. 3
      libs/common/src/lib/interfaces/data-provider-info.interface.ts

1
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

2
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<AdminData> {
return this.adminService.get();
return this.adminService.get({ user: this.request.user });
}
@HasPermission(permissions.accessAdminControl)

11
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<AdminData> {
public async get({ user }: { user: UserWithSettings }): Promise<AdminData> {
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
};
}

10
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'
};
}

1
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'

1
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'

5
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 {

1
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'

1
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'

3
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'
};
}

1
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'

1
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
};
}

1
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'

1
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'

152
apps/client/src/app/components/admin-settings/admin-settings.component.html

@ -4,74 +4,100 @@
<h2 class="text-center" i18n>Data Providers</h2>
<mat-card appearance="outlined">
<mat-card-content>
<div class="align-items-center d-flex my-3">
<div class="w-50">
<a
class="align-items-center d-inline-flex"
target="_blank"
[href]="pricingUrl"
>
@if (isGhostfolioApiKeyValid === false) {
<span class="badge badge-warning mr-1" i18n
>Early Access</span
>
}
Ghostfolio Premium
<gf-premium-indicator
class="d-inline-block ml-1"
[enableLink]="false"
/>
</a>
@if (isGhostfolioApiKeyValid === true) {
<div class="line-height-1">
<small class="text-muted">
<ng-container i18n>Valid until</ng-container>
{{
ghostfolioApiStatus?.subscription?.expiresAt
| date: defaultDateFormat
}}</small
>
</div>
}
</div>
<div class="w-50">
@if (isGhostfolioApiKeyValid === true) {
<div class="align-items-center d-flex flex-wrap">
<div class="flex-grow-1 mr-3">
{{ ghostfolioApiStatus.dailyRequests }}
<ng-container i18n>of</ng-container>
{{ ghostfolioApiStatus.dailyRequestsMax }}
<ng-container i18n>daily requests</ng-container>
@for (dataProvider of dataProviders; track dataProvider.name) {
<div class="align-items-center d-flex my-3">
@if (dataProvider.name === 'Ghostfolio') {
<div class="w-50">
<div class="d-flex">
<gf-asset-profile-icon
class="mr-1"
[url]="dataProvider.url"
/>
<div>
<a
class="align-items-center d-inline-flex"
target="_blank"
[href]="pricingUrl"
>
Ghostfolio Premium
<gf-premium-indicator
class="d-inline-block ml-1"
[enableLink]="false"
/>
@if (isGhostfolioApiKeyValid === false) {
<span class="badge badge-warning ml-2" i18n
>Early Access</span
>
}
</a>
@if (isGhostfolioApiKeyValid === true) {
<div class="line-height-1">
<small class="text-muted">
<ng-container i18n>Valid until</ng-container>
{{
ghostfolioApiStatus?.subscription?.expiresAt
| date: defaultDateFormat
}}</small
>
</div>
}
</div>
</div>
<button
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="ghostfolioApiMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-horizontal" />
</button>
<mat-menu #ghostfolioApiMenu="matMenu" xPosition="before">
<button mat-menu-item (click)="onRemoveGhostfolioApiKey()">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" />
<span i18n>Remove API key</span>
</span>
</div>
<div class="w-50">
@if (isGhostfolioApiKeyValid === true) {
<div class="align-items-center d-flex flex-wrap">
<div class="flex-grow-1 mr-3">
{{ ghostfolioApiStatus.dailyRequests }}
<ng-container i18n>of</ng-container>
{{ ghostfolioApiStatus.dailyRequestsMax }}
<ng-container i18n>daily requests</ng-container>
</div>
<button
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="ghostfolioApiMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-horizontal" />
</button>
<mat-menu #ghostfolioApiMenu="matMenu" xPosition="before">
<button
mat-menu-item
(click)="onRemoveGhostfolioApiKey()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" />
<span i18n>Remove API key</span>
</span>
</button>
</mat-menu>
</div>
} @else if (isGhostfolioApiKeyValid === false) {
<button
color="accent"
mat-flat-button
(click)="onSetGhostfolioApiKey()"
>
<ion-icon class="mr-1" name="key-outline" />
<span i18n>Set API key</span>
</button>
</mat-menu>
}
</div>
} @else {
<div class="w-50">
<div class="d-flex">
<gf-asset-profile-icon
class="mr-1"
[url]="dataProvider.url"
/>
{{ dataProvider.name }}
</div>
</div>
} @else if (isGhostfolioApiKeyValid === false) {
<button
color="accent"
mat-flat-button
(click)="onSetGhostfolioApiKey()"
>
<ion-icon class="mr-1" name="key-outline" />
<span i18n>Set API key</span>
</button>
<div class="w-50"></div>
}
</div>
</div>
}
</mat-card-content>
</mat-card>
</div>

49
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();
});

2
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,

26
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<DataProviderGhostfolioStatusResponse>(
`${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<DataProviderGhostfolioStatusResponse>(
`${environment.production ? 'https://ghostfol.io' : ''}/api/v2/data-providers/ghostfolio/status`,
{ headers }
);
}

3
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;

3
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;

Loading…
Cancel
Save