From 09a6b578bf5899e280fd4cb12333076242541c7f Mon Sep 17 00:00:00 2001 From: olav Date: Wed, 8 Jun 2022 14:57:39 +0200 Subject: [PATCH] refactor: add OpenAPI schema to UI config controller (#1681) --- src/lib/openapi/index.ts | 4 + src/lib/openapi/spec/ui-config-schema.test.ts | 24 ++++ src/lib/openapi/spec/ui-config-schema.ts | 76 +++++++++++ src/lib/openapi/spec/version-schema.ts | 43 ++++++ src/lib/routes/admin-api/config.ts | 86 +++++++----- src/lib/types/option.ts | 1 + .../__snapshots__/openapi.e2e.test.ts.snap | 127 ++++++++++++++++++ 7 files changed, 327 insertions(+), 34 deletions(-) create mode 100644 src/lib/openapi/spec/ui-config-schema.test.ts create mode 100644 src/lib/openapi/spec/ui-config-schema.ts create mode 100644 src/lib/openapi/spec/version-schema.ts diff --git a/src/lib/openapi/index.ts b/src/lib/openapi/index.ts index 920fb1e616..6a241eab2d 100644 --- a/src/lib/openapi/index.ts +++ b/src/lib/openapi/index.ts @@ -18,10 +18,12 @@ import { patchesSchema } from './spec/patches-schema'; import { strategySchema } from './spec/strategy-schema'; import { tagSchema } from './spec/tag-schema'; import { tagsSchema } from './spec/tags-schema'; +import { uiConfigSchema } from './spec/ui-config-schema'; import { updateFeatureSchema } from './spec/update-feature-schema'; import { updateStrategySchema } from './spec/update-strategy-schema'; import { variantSchema } from './spec/variant-schema'; import { variantsSchema } from './spec/variants-schema'; +import { versionSchema } from './spec/version-schema'; // Schemas must have $id property on the form "#/components/schemas/mySchema". export type SchemaId = typeof schemas[keyof typeof schemas]['$id']; @@ -57,10 +59,12 @@ export const schemas = { strategySchema, tagSchema, tagsSchema, + uiConfigSchema, updateFeatureSchema, updateStrategySchema, variantSchema, variantsSchema, + versionSchema, }; export const createRequestSchema = ( diff --git a/src/lib/openapi/spec/ui-config-schema.test.ts b/src/lib/openapi/spec/ui-config-schema.test.ts new file mode 100644 index 0000000000..de09f474cd --- /dev/null +++ b/src/lib/openapi/spec/ui-config-schema.test.ts @@ -0,0 +1,24 @@ +import { validateSchema } from '../validate'; +import { UiConfigSchema } from './ui-config-schema'; + +test('uiConfigSchema', () => { + const data: UiConfigSchema = { + slogan: 'a', + version: 'a', + unleashUrl: 'a', + baseUriPath: 'a', + disablePasswordAuth: false, + segmentValuesLimit: 0, + strategySegmentsLimit: 0, + versionInfo: { + current: {}, + latest: {}, + isLatest: true, + instanceId: 'a', + }, + }; + + expect( + validateSchema('#/components/schemas/uiConfigSchema', data), + ).toBeUndefined(); +}); diff --git a/src/lib/openapi/spec/ui-config-schema.ts b/src/lib/openapi/spec/ui-config-schema.ts new file mode 100644 index 0000000000..8fb15feca7 --- /dev/null +++ b/src/lib/openapi/spec/ui-config-schema.ts @@ -0,0 +1,76 @@ +import { FromSchema } from 'json-schema-to-ts'; +import { versionSchema } from './version-schema'; + +export const uiConfigSchema = { + $id: '#/components/schemas/uiConfigSchema', + type: 'object', + additionalProperties: false, + required: [ + 'version', + 'unleashUrl', + 'baseUriPath', + 'versionInfo', + 'disablePasswordAuth', + 'segmentValuesLimit', + 'strategySegmentsLimit', + ], + properties: { + slogan: { + type: 'string', + }, + name: { + type: 'string', + }, + version: { + type: 'string', + }, + unleashUrl: { + type: 'string', + }, + baseUriPath: { + type: 'string', + }, + disablePasswordAuth: { + type: 'boolean', + }, + segmentValuesLimit: { + type: 'number', + }, + strategySegmentsLimit: { + type: 'number', + }, + flags: { + type: 'object', + additionalProperties: { + type: 'boolean', + }, + }, + links: { + type: 'array', + items: { + type: 'object', + }, + }, + authenticationType: { + type: 'string', + enum: [ + 'open-source', + 'demo', + 'enterprise', + 'hosted', + 'custom', + 'none', + ], + }, + versionInfo: { + $ref: '#/components/schemas/versionSchema', + }, + }, + components: { + schemas: { + versionSchema, + }, + }, +} as const; + +export type UiConfigSchema = FromSchema; diff --git a/src/lib/openapi/spec/version-schema.ts b/src/lib/openapi/spec/version-schema.ts new file mode 100644 index 0000000000..6d0010a348 --- /dev/null +++ b/src/lib/openapi/spec/version-schema.ts @@ -0,0 +1,43 @@ +import { FromSchema } from 'json-schema-to-ts'; + +export const versionSchema = { + $id: '#/components/schemas/versionSchema', + type: 'object', + additionalProperties: false, + required: ['current', 'latest', 'isLatest', 'instanceId'], + properties: { + current: { + type: 'object', + additionalProperties: false, + properties: { + oss: { + type: 'string', + }, + enterprise: { + type: 'string', + }, + }, + }, + latest: { + type: 'object', + additionalProperties: false, + properties: { + oss: { + type: 'string', + }, + enterprise: { + type: 'string', + }, + }, + }, + isLatest: { + type: 'boolean', + }, + instanceId: { + type: 'string', + }, + }, + components: {}, +} as const; + +export type VersionSchema = FromSchema; diff --git a/src/lib/routes/admin-api/config.ts b/src/lib/routes/admin-api/config.ts index f5be01b034..48d9ce820b 100644 --- a/src/lib/routes/admin-api/config.ts +++ b/src/lib/routes/admin-api/config.ts @@ -1,73 +1,91 @@ import { Request, Response } from 'express'; import { IUnleashServices } from '../../types/services'; -import { IAuthType, IUIConfig, IUnleashConfig } from '../../types/option'; +import { IAuthType, IUnleashConfig } from '../../types/option'; import version from '../../util/version'; import Controller from '../controller'; -import VersionService, { IVersionHolder } from '../../services/version-service'; +import VersionService from '../../services/version-service'; import SettingService from '../../services/setting-service'; import { simpleAuthKey, SimpleAuthSettings, } from '../../types/settings/simple-auth-settings'; - -interface IUIConfigResponse extends IUIConfig { - version: string; - unleashUrl: string; - baseUriPath: string; - authenticationType?: IAuthType; - versionInfo: IVersionHolder; - disablePasswordAuth: boolean; - segmentValuesLimit: number; - strategySegmentsLimit: number; -} +import { NONE } from '../../types/permissions'; +import { createResponseSchema } from '../../openapi'; +import { + uiConfigSchema, + UiConfigSchema, +} from '../../openapi/spec/ui-config-schema'; +import { OpenApiService } from '../../services/openapi-service'; class ConfigController extends Controller { private versionService: VersionService; private settingService: SettingService; - private uiConfig: Omit< - IUIConfigResponse, - 'versionInfo' | 'disablePasswordAuth' - >; + private readonly openApiService: OpenApiService; constructor( config: IUnleashConfig, { versionService, settingService, - }: Pick, + openApiService, + }: Pick< + IUnleashServices, + 'versionService' | 'settingService' | 'openApiService' + >, ) { super(config); this.versionService = versionService; this.settingService = settingService; - const authenticationType = - config.authentication && config.authentication.type; - this.uiConfig = { - ...config.ui, - version, - unleashUrl: config.server.unleashUrl, - baseUriPath: config.server.baseUriPath, - authenticationType, - segmentValuesLimit: config.segmentValuesLimit, - strategySegmentsLimit: config.strategySegmentsLimit, - }; - this.get('/', this.getUIConfig); + this.openApiService = openApiService; + + this.route({ + method: 'get', + path: '', + handler: this.getUIConfig, + permission: NONE, + middleware: [ + openApiService.validPath({ + tags: ['admin'], + operationId: 'getUIConfig', + responses: { + 200: createResponseSchema('uiConfigSchema'), + }, + }), + ], + }); } async getUIConfig( req: Request, - res: Response, + res: Response, ): Promise { - const config = this.uiConfig; const simpleAuthSettings = await this.settingService.get(simpleAuthKey); - const versionInfo = this.versionService.getVersionInfo(); const disablePasswordAuth = simpleAuthSettings?.disabled || this.config.authentication.type == IAuthType.NONE; - res.json({ ...config, versionInfo, disablePasswordAuth }); + + const response: UiConfigSchema = { + ...this.config.ui, + version, + unleashUrl: this.config.server.unleashUrl, + baseUriPath: this.config.server.baseUriPath, + authenticationType: this.config.authentication?.type, + segmentValuesLimit: this.config.segmentValuesLimit, + strategySegmentsLimit: this.config.strategySegmentsLimit, + versionInfo: this.versionService.getVersionInfo(), + disablePasswordAuth, + }; + + this.openApiService.respondWithValidation( + 200, + res, + uiConfigSchema.$id, + response, + ); } } export default ConfigController; diff --git a/src/lib/types/option.ts b/src/lib/types/option.ts index 89f650399f..364aa3b368 100644 --- a/src/lib/types/option.ts +++ b/src/lib/types/option.ts @@ -147,6 +147,7 @@ export interface IUIConfig { }, ]; } + export interface ICspDomainOptions { defaultSrc?: string[]; fontSrc?: string[]; diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index d0daa2db66..b67866a363 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -455,6 +455,71 @@ Object { ], "type": "object", }, + "uiConfigSchema": Object { + "additionalProperties": false, + "properties": Object { + "authenticationType": Object { + "enum": Array [ + "open-source", + "demo", + "enterprise", + "hosted", + "custom", + "none", + ], + "type": "string", + }, + "baseUriPath": Object { + "type": "string", + }, + "disablePasswordAuth": Object { + "type": "boolean", + }, + "flags": Object { + "additionalProperties": Object { + "type": "boolean", + }, + "type": "object", + }, + "links": Object { + "items": Object { + "type": "object", + }, + "type": "array", + }, + "name": Object { + "type": "string", + }, + "segmentValuesLimit": Object { + "type": "number", + }, + "slogan": Object { + "type": "string", + }, + "strategySegmentsLimit": Object { + "type": "number", + }, + "unleashUrl": Object { + "type": "string", + }, + "version": Object { + "type": "string", + }, + "versionInfo": Object { + "$ref": "#/components/schemas/versionSchema", + }, + }, + "required": Array [ + "version", + "unleashUrl", + "baseUriPath", + "versionInfo", + "disablePasswordAuth", + "segmentValuesLimit", + "strategySegmentsLimit", + ], + "type": "object", + }, "updateFeatureSchema": Object { "properties": Object { "archived": Object { @@ -567,6 +632,48 @@ Object { }, "type": "array", }, + "versionSchema": Object { + "additionalProperties": false, + "properties": Object { + "current": Object { + "additionalProperties": false, + "properties": Object { + "enterprise": Object { + "type": "string", + }, + "oss": Object { + "type": "string", + }, + }, + "type": "object", + }, + "instanceId": Object { + "type": "string", + }, + "isLatest": Object { + "type": "boolean", + }, + "latest": Object { + "additionalProperties": false, + "properties": Object { + "enterprise": Object { + "type": "string", + }, + "oss": Object { + "type": "string", + }, + }, + "type": "object", + }, + }, + "required": Array [ + "current", + "latest", + "isLatest", + "instanceId", + ], + "type": "object", + }, }, "securitySchemes": Object { "apiKey": Object { @@ -1676,6 +1783,26 @@ Object { ], }, }, + "/api/admin/ui-config": Object { + "get": Object { + "operationId": "getUIConfig", + "responses": Object { + "200": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/uiConfigSchema", + }, + }, + }, + "description": "uiConfigSchema", + }, + }, + "tags": Array [ + "admin", + ], + }, + }, }, "security": Array [ Object {