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 index b17e80a8d..cebe5f1e9 100644 --- a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts +++ b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts @@ -2,6 +2,9 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.in import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; +import { PropertyService } from '@ghostfolio/api/services/property/property.service'; +import { PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS } from '@ghostfolio/common/config'; +import { DataProviderGhostfolioStatusResponse } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; import { RequestWithUser } from '@ghostfolio/common/types'; @@ -24,6 +27,7 @@ export class GhostfolioController { public constructor( private readonly ghostfolioService: GhostfolioService, private readonly prismaService: PrismaService, + private readonly propertyService: PropertyService, @Inject(REQUEST) private readonly request: RequestWithUser ) {} @@ -35,6 +39,16 @@ export class GhostfolioController { @Query('query') query = '' ): Promise<{ items: LookupItem[] }> { const includeIndices = includeIndicesParam === 'true'; + const maxDailyRequests = await this.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({ @@ -55,6 +69,25 @@ export class GhostfolioController { } } + @Get('status') + @HasPermission(permissions.enableDataProviderGhostfolio) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + public async getStatus(): Promise { + return { + dailyRequests: this.request.user.dataProviderGhostfolioDailyRequests, + dailyRequestsMax: await this.getMaxDailyRequests() + }; + } + + private async getMaxDailyRequests() { + return parseInt( + ((await this.propertyService.getByKey( + PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS + )) as string) || '0', + 10 + ); + } + private async incrementDailyRequests({ userId }: { userId: string }) { await this.prismaService.analytics.update({ data: { diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 71a56e99c..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) { diff --git a/apps/client/src/app/pages/api/api-page.component.ts b/apps/client/src/app/pages/api/api-page.component.ts index 6ad2e8e4d..cdf008208 100644 --- a/apps/client/src/app/pages/api/api-page.component.ts +++ b/apps/client/src/app/pages/api/api-page.component.ts @@ -1,4 +1,5 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; +import { DataProviderGhostfolioStatusResponse } from '@ghostfolio/common/interfaces'; import { CommonModule } from '@angular/common'; import { HttpClient, HttpParams } from '@angular/common/http'; @@ -14,6 +15,7 @@ import { map, Observable, Subject, takeUntil } from 'rxjs'; templateUrl: './api-page.html' }) export class GfApiPageComponent implements OnInit { + public status$: Observable; public symbols$: Observable; private unsubscribeSubject = new Subject(); @@ -21,6 +23,7 @@ export class GfApiPageComponent implements OnInit { public constructor(private http: HttpClient) {} public ngOnInit() { + this.status$ = this.fetchStatus(); this.symbols$ = this.fetchSymbols({ query: 'apple' }); } @@ -29,6 +32,14 @@ export class GfApiPageComponent implements OnInit { this.unsubscribeSubject.complete(); } + private fetchStatus() { + return this.http + .get( + '/api/v1/data-providers/ghostfolio/status' + ) + .pipe(takeUntil(this.unsubscribeSubject)); + } + private fetchSymbols({ includeIndices = false, query diff --git a/apps/client/src/app/pages/api/api-page.html b/apps/client/src/app/pages/api/api-page.html index 7097333ed..df0e820e3 100644 --- a/apps/client/src/app/pages/api/api-page.html +++ b/apps/client/src/app/pages/api/api-page.html @@ -1,4 +1,8 @@
+
+

Status

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

Lookup

    diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index fbd416bb7..929ba9f01 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -117,6 +117,8 @@ 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..aab58e600 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -40,6 +40,7 @@ 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 { ImportResponse } from './responses/import-response.interface'; import type { LookupResponse } from './responses/lookup-response.interface'; @@ -74,6 +75,7 @@ export { BenchmarkProperty, BenchmarkResponse, Coupon, + DataProviderGhostfolioStatusResponse, DataProviderInfo, EnhancedSymbolProfile, Export, 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/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?: {