1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-31 01:16:01 +02:00

refactor: remove bootstrap endpoint (#1900)

This commit is contained in:
olav 2022-08-09 15:58:27 +02:00 committed by GitHub
parent eb2de89b3c
commit 49095025ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 96 additions and 665 deletions

View File

@ -7,7 +7,6 @@ import { apiTokenSchema } from './spec/api-token-schema';
import { apiTokensSchema } from './spec/api-tokens-schema';
import { applicationSchema } from './spec/application-schema';
import { applicationsSchema } from './spec/applications-schema';
import { bootstrapUiSchema } from './spec/bootstrap-ui-schema';
import { changePasswordSchema } from './spec/change-password-schema';
import { clientApplicationSchema } from './spec/client-application-schema';
import { clientFeatureSchema } from './spec/client-feature-schema';
@ -116,7 +115,6 @@ export const schemas = {
apiTokensSchema,
applicationSchema,
applicationsSchema,
bootstrapUiSchema,
changePasswordSchema,
clientApplicationSchema,
clientFeatureSchema,

View File

@ -1,157 +0,0 @@
import { validateSchema } from '../validate';
import { BootstrapUiSchema } from './bootstrap-ui-schema';
test('bootstrapUiSchema', () => {
const data: BootstrapUiSchema = {
uiConfig: {
flags: { E: true },
authenticationType: 'open-source',
unleashUrl: 'http://localhost:4242',
version: '4.14.0-beta.0',
baseUriPath: '',
versionInfo: {
current: { oss: '4.14.0-beta.0', enterprise: '' },
latest: {},
isLatest: true,
instanceId: '51c9190a-4ff5-4f47-b73a-7aebe06f9331',
},
},
user: {
isAPI: false,
id: 1,
username: 'admin',
imageUrl:
'https://gravatar.com/avatar/21232f297a57a5a743894a0e4a801fc3?size=42&default=retro',
seenAt: '2022-06-27T12:19:15.838Z',
loginAttempts: 0,
createdAt: '2022-04-08T10:59:25.072Z',
permissions: [
{ permission: 'READ_API_TOKEN' },
{
project: 'default',
environment: 'staging',
permission: 'CREATE_FEATURE_STRATEGY',
},
{
project: 'default',
environment: 'staging',
permission: 'UPDATE_FEATURE_STRATEGY',
},
{ project: 'default', permission: 'UPDATE_FEATURE' },
],
},
email: false,
context: [
{
name: 'appName',
description: 'Allows you to constrain on application name',
stickiness: false,
sortOrder: 2,
legalValues: [],
createdAt: '2022-04-08T10:59:24.374Z',
},
{
name: 'currentTime',
description: '',
stickiness: false,
sortOrder: 10,
legalValues: [],
createdAt: '2022-05-18T08:15:18.917Z',
},
{
name: 'environment',
description:
'Allows you to constrain on application environment',
stickiness: false,
sortOrder: 0,
legalValues: [],
createdAt: '2022-04-08T10:59:24.374Z',
},
{
name: 'userId',
description: 'Allows you to constrain on userId',
stickiness: false,
sortOrder: 1,
legalValues: [],
createdAt: '2022-04-08T10:59:24.374Z',
},
],
featureTypes: [
{
id: 'release',
name: 'Release',
description:
'Release feature toggles are used to release new features.',
lifetimeDays: 40,
},
{
id: 'experiment',
name: 'Experiment',
description:
'Experiment feature toggles are used to test and verify multiple different versions of a feature.',
lifetimeDays: 40,
},
{
id: 'operational',
name: 'Operational',
description:
'Operational feature toggles are used to control aspects of a rollout.',
lifetimeDays: 7,
},
],
tagTypes: [
{
name: 'simple',
description: 'Used to simplify filtering of features',
icon: '#',
},
{ name: 'hashtag', description: '', icon: null },
],
strategies: [
{
displayName: 'Standard',
name: 'default',
editable: false,
description:
'The standard strategy is strictly on / off for your entire userbase.',
parameters: [],
deprecated: false,
},
{
displayName: null,
name: 'gradualRolloutRandom',
editable: true,
description:
'Randomly activate the feature toggle. No stickiness.',
parameters: [
{
name: 'percentage',
type: 'percentage',
description: '',
required: false,
},
],
deprecated: true,
},
],
projects: [
{
name: 'Default',
id: 'default',
description: 'Default project',
health: 74,
featureCount: 10,
memberCount: 3,
updatedAt: '2022-06-28T17:33:53.963Z',
},
],
};
expect(
validateSchema('#/components/schemas/bootstrapUiSchema', {}),
).not.toBeUndefined();
expect(
validateSchema('#/components/schemas/bootstrapUiSchema', data),
).toBeUndefined();
});

View File

@ -1,94 +0,0 @@
import { FromSchema } from 'json-schema-to-ts';
import { uiConfigSchema } from './ui-config-schema';
import { userSchema } from './user-schema';
import { permissionSchema } from './permission-schema';
import { featureTypeSchema } from './feature-type-schema';
import { tagTypeSchema } from './tag-type-schema';
import { contextFieldSchema } from './context-field-schema';
import { strategySchema } from './strategy-schema';
import { projectSchema } from './project-schema';
import { versionSchema } from './version-schema';
import { legalValueSchema } from './legal-value-schema';
export const bootstrapUiSchema = {
$id: '#/components/schemas/bootstrapUiSchema',
type: 'object',
additionalProperties: false,
required: [
'uiConfig',
'user',
'email',
'context',
'featureTypes',
'tagTypes',
'strategies',
'projects',
],
properties: {
uiConfig: {
$ref: '#/components/schemas/uiConfigSchema',
},
user: {
type: 'object',
required: [...userSchema.required],
properties: {
...userSchema.properties,
permissions: {
type: 'array',
items: {
$ref: '#/components/schemas/permissionSchema',
},
},
},
},
email: {
type: 'boolean',
},
context: {
type: 'array',
items: {
$ref: '#/components/schemas/contextFieldSchema',
},
},
featureTypes: {
type: 'array',
items: {
$ref: '#/components/schemas/featureTypeSchema',
},
},
tagTypes: {
type: 'array',
items: {
$ref: '#/components/schemas/tagTypeSchema',
},
},
strategies: {
type: 'array',
items: {
$ref: '#/components/schemas/strategySchema',
},
},
projects: {
type: 'array',
items: {
$ref: '#/components/schemas/projectSchema',
},
},
},
components: {
schemas: {
uiConfigSchema,
userSchema,
permissionSchema,
contextFieldSchema,
featureTypeSchema,
tagTypeSchema,
strategySchema,
projectSchema,
versionSchema,
legalValueSchema,
},
},
} as const;
export type BootstrapUiSchema = FromSchema<typeof bootstrapUiSchema>;

View File

@ -8,7 +8,11 @@ test('contextFieldSchema', () => {
stickiness: false,
sortOrder: 0,
createdAt: '2022-01-01T00:00:00.000Z',
legalValues: [],
legalValues: [
{ value: 'a' },
{ value: 'b', description: '' },
{ value: 'c', description: 'd' },
],
};
expect(

View File

@ -0,0 +1,22 @@
import { validateSchema } from '../validate';
import { ProjectSchema } from './project-schema';
test('projectSchema', () => {
const data: ProjectSchema = {
name: 'Default',
id: 'default',
description: 'Default project',
health: 74,
featureCount: 10,
memberCount: 3,
updatedAt: '2022-06-28T17:33:53.963Z',
};
expect(
validateSchema('#/components/schemas/projectSchema', {}),
).not.toBeUndefined();
expect(
validateSchema('#/components/schemas/projectSchema', data),
).toBeUndefined();
});

View File

@ -0,0 +1,28 @@
import { validateSchema } from '../validate';
import { TagTypesSchema } from './tag-types-schema';
test('tagTypesSchema', () => {
const data: TagTypesSchema = {
version: 1,
tagTypes: [
{
name: 'simple',
description: 'Used to simplify filtering of features',
icon: '#',
},
{
name: 'hashtag',
description: '',
icon: null,
},
],
};
expect(
validateSchema('#/components/schemas/tagTypesSchema', {}),
).not.toBeUndefined();
expect(
validateSchema('#/components/schemas/tagTypesSchema', data),
).toBeUndefined();
});

View File

@ -28,6 +28,9 @@ export const uiConfigSchema = {
disablePasswordAuth: {
type: 'boolean',
},
emailEnabled: {
type: 'boolean',
},
segmentValuesLimit: {
type: 'number',
},

View File

@ -0,0 +1,22 @@
import { validateSchema } from '../validate';
import { UserSchema } from './user-schema';
test('userSchema', () => {
const data: UserSchema = {
isAPI: false,
id: 1,
username: 'admin',
imageUrl: 'avatar',
seenAt: '2022-06-27T12:19:15.838Z',
loginAttempts: 0,
createdAt: '2022-04-08T10:59:25.072Z',
};
expect(
validateSchema('#/components/schemas/userSchema', {}),
).not.toBeUndefined();
expect(
validateSchema('#/components/schemas/userSchema', data),
).toBeUndefined();
});

View File

@ -1,63 +0,0 @@
import supertest from 'supertest';
import { createTestConfig } from '../../../test/config/test-config';
import { randomId } from '../../util/random-id';
import createStores from '../../../test/fixtures/store';
import getApp from '../../app';
import { createServices } from '../../services';
const uiConfig = {
headerBackground: 'red',
slogan: 'hello',
};
async function getSetup() {
const base = `/random${randomId()}`;
const config = createTestConfig({
server: { baseUriPath: base },
ui: uiConfig,
});
const stores = createStores();
const services = createServices(stores, config);
const app = await getApp(config, stores, services);
return {
base,
request: supertest(app),
destroy: () => {
services.versionService.destroy();
services.clientInstanceService.destroy();
services.apiTokenService.destroy();
},
};
}
let request;
let base;
let destroy;
beforeEach(async () => {
const setup = await getSetup();
request = setup.request;
base = setup.base;
destroy = setup.destroy;
});
afterEach(() => {
destroy();
});
test('should get ui config', async () => {
const { body } = await request
.get(`${base}/api/admin/ui-bootstrap`)
.expect('Content-Type', /json/)
.expect(200);
expect(body.uiConfig.slogan).toEqual('hello');
expect(body.email).toEqual(false);
expect(body.user).toHaveProperty('permissions');
expect(body.context).toBeInstanceOf(Array);
expect(body.tagTypes).toBeInstanceOf(Array);
expect(body.strategies).toBeInstanceOf(Array);
expect(body.projects).toBeInstanceOf(Array);
});

View File

@ -1,173 +0,0 @@
import { Response } from 'express';
import Controller from '../controller';
import { AuthedRequest } from '../../types/core';
import { Logger } from '../../logger';
import ContextService from '../../services/context-service';
import TagTypeService from '../../services/tag-type-service';
import StrategyService from '../../services/strategy-service';
import ProjectService from '../../services/project-service';
import { AccessService } from '../../services/access-service';
import { EmailService } from '../../services/email-service';
import { IUnleashConfig } from '../../types/option';
import { IUnleashServices } from '../../types/services';
import VersionService from '../../services/version-service';
import FeatureTypeService from '../../services/feature-type-service';
import version from '../../util/version';
import { IContextField } from '../../types/stores/context-field-store';
import { IFeatureType } from '../../types/stores/feature-type-store';
import { ITagType } from '../../types/stores/tag-type-store';
import { IStrategy } from '../../types/stores/strategy-store';
import { IProject } from '../../types/model';
import { IUserPermission } from '../../types/stores/access-store';
import { OpenApiService } from '../../services/openapi-service';
import { NONE } from '../../types/permissions';
import { createResponseSchema } from '../../openapi/util/create-response-schema';
import {
BootstrapUiSchema,
bootstrapUiSchema,
} from '../../openapi/spec/bootstrap-ui-schema';
import { serializeDates } from '../../types/serialize-dates';
/**
* Provides admin UI configuration.
* Not to be confused with SDK bootstrapping.
*/
class BootstrapUIController extends Controller {
private logger: Logger;
private accessService: AccessService;
private contextService: ContextService;
private emailService: EmailService;
private featureTypeService: FeatureTypeService;
private projectService: ProjectService;
private strategyService: StrategyService;
private tagTypeService: TagTypeService;
private versionService: VersionService;
private openApiService: OpenApiService;
constructor(
config: IUnleashConfig,
{
contextService,
tagTypeService,
strategyService,
projectService,
accessService,
emailService,
versionService,
featureTypeService,
openApiService,
}: Pick<
IUnleashServices,
| 'contextService'
| 'tagTypeService'
| 'strategyService'
| 'projectService'
| 'accessService'
| 'emailService'
| 'versionService'
| 'featureTypeService'
| 'openApiService'
>,
) {
super(config);
this.contextService = contextService;
this.tagTypeService = tagTypeService;
this.strategyService = strategyService;
this.projectService = projectService;
this.accessService = accessService;
this.featureTypeService = featureTypeService;
this.emailService = emailService;
this.versionService = versionService;
this.openApiService = openApiService;
this.logger = config.getLogger('routes/admin-api/bootstrap-ui.ts');
this.route({
method: 'get',
path: '',
handler: this.bootstrap,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['other'],
operationId: 'getBootstrapUiData',
responses: {
202: createResponseSchema('bootstrapUiSchema'),
},
}),
],
});
}
async bootstrap(
req: AuthedRequest,
res: Response<BootstrapUiSchema>,
): Promise<void> {
const jobs: [
Promise<IContextField[]>,
Promise<IFeatureType[]>,
Promise<ITagType[]>,
Promise<IStrategy[]>,
Promise<IProject[]>,
Promise<IUserPermission[]>,
] = [
this.contextService.getAll(),
this.featureTypeService.getAll(),
this.tagTypeService.getAll(),
this.strategyService.getStrategies(),
this.projectService.getProjects(),
this.accessService.getPermissionsForUser(req.user),
];
const [
context,
featureTypes,
tagTypes,
strategies,
projects,
userPermissions,
] = await Promise.all(jobs);
const authenticationType =
this.config.authentication && this.config.authentication.type;
const versionInfo = this.versionService.getVersionInfo();
const uiConfig = {
...this.config.ui,
authenticationType,
unleashUrl: this.config.server.unleashUrl,
version,
baseUriPath: this.config.server.baseUriPath,
versionInfo,
};
this.openApiService.respondWithValidation(
200,
res,
bootstrapUiSchema.$id,
{
uiConfig,
user: {
...serializeDates(req.user),
permissions: userPermissions,
},
email: this.emailService.isEnabled(),
context: serializeDates(context),
featureTypes,
tagTypes,
strategies,
projects: serializeDates(projects),
},
);
}
}
export default BootstrapUIController;
module.exports = BootstrapUIController;

View File

@ -16,12 +16,15 @@ import {
UiConfigSchema,
} from '../../openapi/spec/ui-config-schema';
import { OpenApiService } from '../../services/openapi-service';
import { EmailService } from '../../services/email-service';
class ConfigController extends Controller {
private versionService: VersionService;
private settingService: SettingService;
private emailService: EmailService;
private readonly openApiService: OpenApiService;
constructor(
@ -29,15 +32,20 @@ class ConfigController extends Controller {
{
versionService,
settingService,
emailService,
openApiService,
}: Pick<
IUnleashServices,
'versionService' | 'settingService' | 'openApiService'
| 'versionService'
| 'settingService'
| 'emailService'
| 'openApiService'
>,
) {
super(config);
this.versionService = versionService;
this.settingService = settingService;
this.emailService = emailService;
this.openApiService = openApiService;
this.route({
@ -71,6 +79,7 @@ class ConfigController extends Controller {
const response: UiConfigSchema = {
...this.config.ui,
version,
emailEnabled: this.emailService.isEnabled(),
unleashUrl: this.config.server.unleashUrl,
baseUriPath: this.config.server.baseUriPath,
authenticationType: this.config.authentication?.type,

View File

@ -13,7 +13,6 @@ import UserController from './user';
import ConfigController from './config';
import { ContextController } from './context';
import ClientMetricsController from './client-metrics';
import BootstrapUIController from './bootstrap-ui';
import StateController from './state';
import TagController from './tag';
import TagTypeController from './tag-type';
@ -68,10 +67,6 @@ class AdminApi extends Controller {
'/ui-config',
new ConfigController(config, services).router,
);
this.app.use(
'/ui-bootstrap',
new BootstrapUIController(config, services).router,
);
this.app.use(
'/context',
new ContextController(config, services).router,

View File

@ -1,43 +0,0 @@
import dbInit from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger';
import { setupAppWithAuth } from '../../helpers/test-helper';
let app;
let db;
const email = 'user@getunleash.io';
beforeAll(async () => {
db = await dbInit('ui_bootstrap_serial', getLogger);
app = await setupAppWithAuth(db.stores);
});
afterAll(async () => {
await app.destroy();
await db.destroy();
});
test('Should get ui-bootstrap data', async () => {
// login
await app.request
.post('/auth/demo/login')
.send({
email,
})
.expect(200);
// get user data
await app.request
.get('/api/admin/ui-bootstrap')
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
const bootstrap = res.body;
expect(bootstrap.context).toBeDefined();
expect(bootstrap.featureTypes).toBeDefined();
expect(bootstrap.uiConfig).toBeDefined();
expect(bootstrap.user).toBeDefined();
expect(bootstrap.context.length).toBeGreaterThan(0);
expect(bootstrap.user.email).toBe(email);
});
});

View File

@ -16,7 +16,7 @@ afterAll(async () => {
await db.destroy();
});
test('gets ui config', async () => {
test('gets ui config fields', async () => {
const { body } = await app.request
.get('/api/admin/ui-config')
.expect('Content-Type', /json/)
@ -24,10 +24,12 @@ test('gets ui config', async () => {
expect(body.unleashUrl).toBe('http://localhost:4242');
expect(body.version).toBeDefined();
expect(body.emailEnabled).toBe(false);
});
test('gets ui config with disablePasswordAuth', async () => {
await db.stores.settingStore.insert(simpleAuthKey, { disabled: true });
const { body } = await app.request
.get('/api/admin/ui-config')
.expect('Content-Type', /json/)

View File

@ -305,111 +305,6 @@ Object {
},
"type": "object",
},
"bootstrapUiSchema": Object {
"additionalProperties": false,
"properties": Object {
"context": Object {
"items": Object {
"$ref": "#/components/schemas/contextFieldSchema",
},
"type": "array",
},
"email": Object {
"type": "boolean",
},
"featureTypes": Object {
"items": Object {
"$ref": "#/components/schemas/featureTypeSchema",
},
"type": "array",
},
"projects": Object {
"items": Object {
"$ref": "#/components/schemas/projectSchema",
},
"type": "array",
},
"strategies": Object {
"items": Object {
"$ref": "#/components/schemas/strategySchema",
},
"type": "array",
},
"tagTypes": Object {
"items": Object {
"$ref": "#/components/schemas/tagTypeSchema",
},
"type": "array",
},
"uiConfig": Object {
"$ref": "#/components/schemas/uiConfigSchema",
},
"user": Object {
"properties": Object {
"createdAt": Object {
"format": "date-time",
"type": "string",
},
"email": Object {
"type": "string",
},
"emailSent": Object {
"type": "boolean",
},
"id": Object {
"type": "number",
},
"imageUrl": Object {
"type": "string",
},
"inviteLink": Object {
"type": "string",
},
"isAPI": Object {
"type": "boolean",
},
"loginAttempts": Object {
"type": "number",
},
"name": Object {
"type": "string",
},
"permissions": Object {
"items": Object {
"$ref": "#/components/schemas/permissionSchema",
},
"type": "array",
},
"rootRole": Object {
"type": "number",
},
"seenAt": Object {
"format": "date-time",
"nullable": true,
"type": "string",
},
"username": Object {
"type": "string",
},
},
"required": Array [
"id",
],
"type": "object",
},
},
"required": Array [
"uiConfig",
"user",
"email",
"context",
"featureTypes",
"tagTypes",
"strategies",
"projects",
],
"type": "object",
},
"changePasswordSchema": Object {
"additionalProperties": false,
"properties": Object {
@ -2684,6 +2579,9 @@ Object {
"disablePasswordAuth": Object {
"type": "boolean",
},
"emailEnabled": Object {
"type": "boolean",
},
"environment": Object {
"type": "string",
},
@ -6003,26 +5901,6 @@ If the provided project does not exist, the list of events will be empty.",
],
},
},
"/api/admin/ui-bootstrap": Object {
"get": Object {
"operationId": "getBootstrapUiData",
"responses": Object {
"202": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/bootstrapUiSchema",
},
},
},
"description": "bootstrapUiSchema",
},
},
"tags": Array [
"other",
],
},
},
"/api/admin/ui-config": Object {
"get": Object {
"operationId": "getUIConfig",