From 4fe5b8594f15c342792f39d3641ebe04240bf4ba Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:25:56 +0100 Subject: [PATCH] Task/restrict health check of AI service to admin (#8326) * Restrict health check of AI service to admin * Update changelog --- CHANGELOG.md | 2 + .../app/__snapshots__/app.module.spec.ts.snap | 6 ++- apps/api/src/app/app.module.spec.ts | 1 - .../app/endpoints/health/health.controller.ts | 11 +++++- .../src/app/pages/api/api-page.component.ts | 17 +++++++++ apps/client/src/app/pages/api/api-page.html | 38 ++++++++++--------- 6 files changed, 54 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 753a758e4e..75ae035f81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Restricted the health check endpoint of the AI service (`GET api/v1/health/ai`) to administrators +- Increased the timeout in the health check endpoint of the AI service to 30 seconds - Deprecated the `api/v1/access` endpoints in favor of the `api/v1/accesses` endpoints ### Fixed diff --git a/apps/api/src/app/__snapshots__/app.module.spec.ts.snap b/apps/api/src/app/__snapshots__/app.module.spec.ts.snap index 13afcc9f2c..447d0bf608 100644 --- a/apps/api/src/app/__snapshots__/app.module.spec.ts.snap +++ b/apps/api/src/app/__snapshots__/app.module.spec.ts.snap @@ -539,8 +539,12 @@ exports[`AppModule should keep the security of each route 1`] = ` "interceptors": [], }, "GET /health/ai": { - "guards": [], + "guards": [ + "AuthGuard('jwt')", + "HasPermissionGuard", + ], "interceptors": [], + "permission": "accessAdminControl", }, "GET /health/data-enhancer/:name": { "guards": [], diff --git a/apps/api/src/app/app.module.spec.ts b/apps/api/src/app/app.module.spec.ts index d43caf8c46..f146398d20 100644 --- a/apps/api/src/app/app.module.spec.ts +++ b/apps/api/src/app/app.module.spec.ts @@ -67,7 +67,6 @@ const PUBLIC_ROUTES = [ 'GET /auth/oidc/callback', 'GET /benchmarks', 'GET /health', - 'GET /health/ai', 'GET /health/data-enhancer/:name', 'GET /health/data-provider/:dataSource', 'GET /health/liveness', diff --git a/apps/api/src/app/endpoints/health/health.controller.ts b/apps/api/src/app/endpoints/health/health.controller.ts index 32aae38e3c..d5f693113b 100644 --- a/apps/api/src/app/endpoints/health/health.controller.ts +++ b/apps/api/src/app/endpoints/health/health.controller.ts @@ -1,4 +1,6 @@ import { AiService } from '@ghostfolio/api/app/endpoints/ai/ai.service'; +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { AiServiceHealthResponse, @@ -6,6 +8,7 @@ import { DataProviderHealthResponse, LivenessHealthResponse } from '@ghostfolio/common/interfaces'; +import { permissions } from '@ghostfolio/common/permissions'; import { Controller, @@ -15,11 +18,14 @@ import { Logger, Param, Res, + UseGuards, UseInterceptors } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; import { DataSource } from '@prisma/client'; import { Response } from 'express'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; +import ms from 'ms'; import { HealthService } from './health.service'; @@ -52,12 +58,15 @@ export class HealthController { } @Get('ai') + @HasPermission(permissions.accessAdminControl) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getHealthOfAiService( @Res() response: Response ): Promise> { try { const { text } = await this.aiService.generateText({ - prompt: `Reply with the word "OK" and nothing else.` + prompt: `Reply with the word "OK" and nothing else.`, + requestTimeout: ms('30 seconds') }); if (text === 'OK') { 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 36c780a04e..6bdd8ba30f 100644 --- a/apps/client/src/app/pages/api/api-page.component.ts +++ b/apps/client/src/app/pages/api/api-page.component.ts @@ -1,3 +1,4 @@ +import { UserService } from '@ghostfolio/client/services/user/user.service'; import { HEADER_KEY_SKIP_INTERCEPTOR, HEADER_KEY_TOKEN @@ -13,6 +14,7 @@ import { MarketDataOfMarketsResponse, QuotesResponse } from '@ghostfolio/common/interfaces'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { GfFearAndGreedIndexComponent } from '@ghostfolio/ui/fear-and-greed-index'; import { CommonModule } from '@angular/common'; @@ -24,6 +26,7 @@ import { } from '@angular/common/http'; import { ChangeDetectionStrategy, + ChangeDetectorRef, Component, DestroyRef, inject, @@ -57,6 +60,7 @@ export class GfApiPageComponent implements OnInit { FetchResult >; protected dividends$: Observable>; + protected hasPermissionToAccessAdminControl: boolean; protected historicalData$: Observable< FetchResult >; @@ -72,13 +76,26 @@ export class GfApiPageComponent implements OnInit { private apiKey: string; + private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly destroyRef = inject(DestroyRef); private readonly http = inject(HttpClient); + private readonly userService = inject(UserService); public ngOnInit() { this.apiKey = prompt($localize`Please enter your Ghostfolio API key:`) ?? ''; + this.userService.stateChanged + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((state) => { + this.hasPermissionToAccessAdminControl = hasPermission( + state?.user?.permissions, + permissions.accessAdminControl + ); + + this.changeDetectorRef.markForCheck(); + }); + this.aiServiceHealth$ = this.fetchAiServiceHealth(); this.assetProfile$ = this.fetchAssetProfile({ symbol: 'AAPL' }); this.dividends$ = this.fetchDividends({ symbol: 'KO' }); diff --git a/apps/client/src/app/pages/api/api-page.html b/apps/client/src/app/pages/api/api-page.html index b742ff6242..d532368167 100644 --- a/apps/client/src/app/pages/api/api-page.html +++ b/apps/client/src/app/pages/api/api-page.html @@ -1,24 +1,26 @@
- - - AI Service Health - - - @let aiServiceHealth = aiServiceHealth$ | async; - @if (isFetchFailure(aiServiceHealth)) { - 🔴 {{ aiServiceHealth.fetchError }} - } @else if (aiServiceHealth) { - 🟢 {{ aiServiceHealth.status }} - } @else { - - } - - + @if (hasPermissionToAccessAdminControl) { + + + AI Service Health + + + @let aiServiceHealth = aiServiceHealth$ | async; + @if (isFetchFailure(aiServiceHealth)) { + 🔴 {{ aiServiceHealth.fetchError }} + } @else if (aiServiceHealth) { + 🟢 {{ aiServiceHealth.status }} + } @else { + + } + + + } Status