diff --git a/src/lib/openapi/spec/instance-admin-stats-schema.ts b/src/lib/openapi/spec/instance-admin-stats-schema.ts index cdbc1f33cf19..0a54a3e7b36d 100644 --- a/src/lib/openapi/spec/instance-admin-stats-schema.ts +++ b/src/lib/openapi/spec/instance-admin-stats-schema.ts @@ -220,6 +220,30 @@ export const instanceAdminStatsSchema = { example: 0, minimum: 0, }, + apiTokens: { + type: 'object', + description: 'The number of API tokens in Unleash, split by type', + properties: { + admin: { + type: 'number', + description: 'The number of admin tokens.', + minimum: 0, + example: 5, + }, + client: { + type: 'number', + description: 'The number of client tokens.', + minimum: 0, + example: 5, + }, + frontend: { + type: 'number', + description: 'The number of frontend tokens.', + minimum: 0, + example: 5, + }, + }, + }, sum: { type: 'string', description: diff --git a/src/lib/routes/admin-api/instance-admin.ts b/src/lib/routes/admin-api/instance-admin.ts index 65e1b457b79c..89d81fabbf8c 100644 --- a/src/lib/routes/admin-api/instance-admin.ts +++ b/src/lib/routes/admin-api/instance-admin.ts @@ -5,7 +5,6 @@ import type { IUnleashServices } from '../../types/services'; import type { IUnleashConfig } from '../../types/option'; import Controller from '../controller'; import { NONE } from '../../types/permissions'; -import type { UiConfigSchema } from '../../openapi/spec/ui-config-schema'; import type { InstanceStatsService, InstanceStatsSigned, @@ -15,6 +14,8 @@ import { createCsvResponseSchema, createResponseSchema, } from '../../openapi/util/create-response-schema'; +import type { InstanceAdminStatsSchema } from '../../openapi'; +import { serializeDates } from '../../types'; class InstanceAdminController extends Controller { private instanceStatsService: InstanceStatsService; @@ -128,17 +129,29 @@ class InstanceAdminController extends Controller { }; } + private serializeStats( + instanceStats: InstanceStatsSigned, + ): InstanceAdminStatsSchema { + const apiTokensObj = Object.fromEntries( + instanceStats.apiTokens.entries(), + ); + return serializeDates({ + ...instanceStats, + apiTokens: apiTokensObj, + }); + } + async getStatistics( - req: AuthedRequest, - res: Response, + _: AuthedRequest, + res: Response, ): Promise { const instanceStats = await this.instanceStatsService.getSignedStats(); - res.json(instanceStats); + res.json(this.serializeStats(instanceStats)); } async getStatisticsCSV( - req: AuthedRequest, - res: Response, + _: AuthedRequest, + res: Response, ): Promise { const instanceStats = await this.instanceStatsService.getSignedStats(); const fileName = `unleash-${ @@ -146,7 +159,7 @@ class InstanceAdminController extends Controller { }-${Date.now()}.csv`; const json2csvParser = new Parser(); - const csv = json2csvParser.parse(instanceStats); + const csv = json2csvParser.parse(this.serializeStats(instanceStats)); res.contentType('csv'); res.attachment(fileName); diff --git a/src/test/e2e/api/admin/instance-admin.e2e.test.ts b/src/test/e2e/api/admin/instance-admin.e2e.test.ts index ec8249799014..5534b4bd80e5 100644 --- a/src/test/e2e/api/admin/instance-admin.e2e.test.ts +++ b/src/test/e2e/api/admin/instance-admin.e2e.test.ts @@ -5,6 +5,7 @@ import { } from '../../helpers/test-helper'; import getLogger from '../../../fixtures/no-logger'; import type { IUnleashStores } from '../../../../lib/types'; +import { ApiTokenType } from '../../../../lib/types/models/api-token'; let app: IUnleashTest; let db: ITestDb; @@ -47,6 +48,43 @@ test('should return instance statistics', async () => { }); }); +test('api tokens are serialized correctly', async () => { + await app.services.apiTokenService.createApiTokenWithProjects({ + tokenName: 'admin', + type: ApiTokenType.ADMIN, + environment: '*', + projects: ['*'], + }); + await app.services.apiTokenService.createApiTokenWithProjects({ + tokenName: 'frontend', + type: ApiTokenType.FRONTEND, + environment: 'default', + projects: ['*'], + }); + await app.services.apiTokenService.createApiTokenWithProjects({ + tokenName: 'client', + type: ApiTokenType.CLIENT, + environment: 'default', + projects: ['*'], + }); + + const { body } = await app.request + .get('/api/admin/instance-admin/statistics') + .expect('Content-Type', /json/) + .expect(200); + + expect(body).toMatchObject({ + apiTokens: { client: 1, admin: 1, frontend: 1 }, + }); + + const { text: csv } = await app.request + .get('/api/admin/instance-admin/statistics/csv') + .expect('Content-Type', /text\/csv/) + .expect(200); + + expect(csv).toMatch(/{""client"":1,""admin"":1,""frontend"":1}/); +}); + test('should return instance statistics with correct number of projects', async () => { await stores.projectStore.create({ id: 'test', @@ -77,7 +115,7 @@ test('should return signed instance statistics', async () => { }); }); -test('should return instance statistics as CVS', async () => { +test('should return instance statistics as CSV', async () => { await stores.featureToggleStore.create('default', { name: 'TestStats2', createdByUserId: 9999,