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

refactor: add OpenAPI schema to api-token controller (#1716)

* refactor: add OpenAPI schema to api-token controller

* refactor: address PR comments

* fix: status codes on environment toggling

* fix tests

* refactor: address PR comments

* refactor: expiresAtSchema -> update-api-token-schema
This commit is contained in:
Nuno Góis 2022-06-17 20:35:26 +01:00 committed by GitHub
parent bb3c722f67
commit 2354656632
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 547 additions and 38 deletions

View File

@ -1,8 +1,11 @@
import { OpenAPIV3 } from 'openapi-types';
import { apiTokenSchema } from './spec/api-token-schema';
import { apiTokensSchema } from './spec/api-tokens-schema';
import { cloneFeatureSchema } from './spec/clone-feature-schema';
import { constraintSchema } from './spec/constraint-schema';
import { contextFieldSchema } from './spec/context-field-schema';
import { contextFieldsSchema } from './spec/context-fields-schema';
import { createApiTokenSchema } from './spec/create-api-token-schema';
import { createFeatureSchema } from './spec/create-feature-schema';
import { createStrategySchema } from './spec/create-strategy-schema';
import { environmentSchema } from './spec/environment-schema';
@ -33,24 +36,28 @@ import { splashSchema } from './spec/splash-schema';
import { strategySchema } from './spec/strategy-schema';
import { tagSchema } from './spec/tag-schema';
import { tagsSchema } from './spec/tags-schema';
import { tagTypeSchema } from './spec/tag-type-schema';
import { tagTypesSchema } from './spec/tag-types-schema';
import { uiConfigSchema } from './spec/ui-config-schema';
import { updateFeatureSchema } from './spec/update-feature-schema';
import { updateStrategySchema } from './spec/update-strategy-schema';
import { updateApiTokenSchema } from './spec/update-api-token-schema';
import { updateTagTypeSchema } from './spec/update-tag-type-schema';
import { upsertContextFieldSchema } from './spec/upsert-context-field-schema';
import { validateTagTypeSchema } from './spec/validate-tag-type-schema';
import { variantSchema } from './spec/variant-schema';
import { variantsSchema } from './spec/variants-schema';
import { versionSchema } from './spec/version-schema';
import { tagTypeSchema } from './spec/tag-type-schema';
import { tagTypesSchema } from './spec/tag-types-schema';
import { updateTagTypeSchema } from './spec/update-tag-type-schema';
import { validateTagTypeSchema } from './spec/validate-tag-type-schema';
// All schemas in `openapi/spec` should be listed here.
export const schemas = {
apiTokenSchema,
apiTokensSchema,
cloneFeatureSchema,
constraintSchema,
contextFieldSchema,
contextFieldsSchema,
createApiTokenSchema,
createFeatureSchema,
createStrategySchema,
environmentSchema,
@ -84,6 +91,7 @@ export const schemas = {
uiConfigSchema,
updateFeatureSchema,
updateStrategySchema,
updateApiTokenSchema,
updateTagTypeSchema,
upsertContextFieldSchema,
validateTagTypeSchema,

View File

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`apiTokenSchema empty 1`] = `
Object {
"data": Object {},
"errors": Array [
Object {
"instancePath": "",
"keyword": "required",
"message": "must have required property 'username'",
"params": Object {
"missingProperty": "username",
},
"schemaPath": "#/required",
},
],
"schema": "#/components/schemas/apiTokenSchema",
}
`;

View File

@ -0,0 +1,27 @@
import { ApiTokenType } from '../../types/models/api-token';
import { validateSchema } from '../validate';
import { ApiTokenSchema } from './api-token-schema';
test('apiTokenSchema', () => {
const data: ApiTokenSchema = {
secret: '',
username: '',
type: ApiTokenType.CLIENT,
environment: '',
projects: [],
expiresAt: '2022-01-01T00:00:00.000Z',
createdAt: '2022-01-01T00:00:00.000Z',
seenAt: '2022-01-01T00:00:00.000Z',
project: '',
};
expect(
validateSchema('#/components/schemas/apiTokenSchema', data),
).toBeUndefined();
});
test('apiTokenSchema empty', () => {
expect(
validateSchema('#/components/schemas/apiTokenSchema', {}),
).toMatchSnapshot();
});

View File

@ -0,0 +1,51 @@
import { FromSchema } from 'json-schema-to-ts';
import { ApiTokenType } from '../../types/models/api-token';
export const apiTokenSchema = {
$id: '#/components/schemas/apiTokenSchema',
type: 'object',
additionalProperties: false,
required: ['username', 'type'],
properties: {
secret: {
type: 'string',
},
username: {
type: 'string',
},
type: {
type: 'string',
description: `${Object.values(ApiTokenType).join(', ')}.`,
},
environment: {
type: 'string',
},
project: {
type: 'string',
},
projects: {
type: 'array',
items: {
type: 'string',
},
},
expiresAt: {
type: 'string',
format: 'date-time',
nullable: true,
},
createdAt: {
type: 'string',
format: 'date-time',
nullable: true,
},
seenAt: {
type: 'string',
format: 'date-time',
nullable: true,
},
},
components: {},
} as const;
export type ApiTokenSchema = FromSchema<typeof apiTokenSchema>;

View File

@ -0,0 +1,24 @@
import { FromSchema } from 'json-schema-to-ts';
import { apiTokenSchema } from './api-token-schema';
export const apiTokensSchema = {
$id: '#/components/schemas/apiTokensSchema',
type: 'object',
additionalProperties: false,
required: ['tokens'],
properties: {
tokens: {
type: 'array',
items: {
$ref: '#/components/schemas/apiTokenSchema',
},
},
},
components: {
schemas: {
apiTokenSchema,
},
},
} as const;
export type ApiTokensSchema = FromSchema<typeof apiTokensSchema>;

View File

@ -0,0 +1,41 @@
import { FromSchema } from 'json-schema-to-ts';
import { ApiTokenType } from '../../types/models/api-token';
export const createApiTokenSchema = {
$id: '#/components/schemas/createApiTokenSchema',
type: 'object',
additionalProperties: false,
required: ['username', 'type'],
properties: {
secret: {
type: 'string',
},
username: {
type: 'string',
},
type: {
type: 'string',
description: `${Object.values(ApiTokenType).join(', ')}.`,
},
environment: {
type: 'string',
},
project: {
type: 'string',
},
projects: {
type: 'array',
items: {
type: 'string',
},
},
expiresAt: {
type: 'string',
format: 'date-time',
nullable: true,
},
},
components: {},
} as const;
export type CreateApiTokenSchema = FromSchema<typeof createApiTokenSchema>;

View File

@ -0,0 +1,17 @@
import { FromSchema } from 'json-schema-to-ts';
export const updateApiTokenSchema = {
$id: '#/components/schemas/updateApiTokenSchema',
type: 'object',
additionalProperties: false,
required: ['expiresAt'],
properties: {
expiresAt: {
type: 'string',
format: 'date-time',
},
},
components: {},
} as const;
export type UpdateApiTokenSchema = FromSchema<typeof updateApiTokenSchema>;

View File

@ -16,51 +16,150 @@ import User from '../../types/user';
import { IUnleashConfig } from '../../types/option';
import { ApiTokenType, IApiToken } from '../../types/models/api-token';
import { createApiToken } from '../../schema/api-token-schema';
import { OpenApiService } from '../../services/openapi-service';
import { IUnleashServices } from '../../types';
import { createRequestSchema, createResponseSchema } from '../../openapi';
import {
apiTokensSchema,
ApiTokensSchema,
} from '../../openapi/spec/api-tokens-schema';
import { serializeDates } from '../../types/serialize-dates';
import {
apiTokenSchema,
ApiTokenSchema,
} from '../../openapi/spec/api-token-schema';
import { emptyResponse } from '../../openapi/spec/empty-response';
import { UpdateApiTokenSchema } from '../../openapi/spec/update-api-token-schema';
interface IServices {
apiTokenService: ApiTokenService;
accessService: AccessService;
interface TokenParam {
token: string;
}
class ApiTokenController extends Controller {
export class ApiTokenController extends Controller {
private apiTokenService: ApiTokenService;
private accessService: AccessService;
private openApiService: OpenApiService;
private logger: Logger;
constructor(config: IUnleashConfig, services: IServices) {
constructor(
config: IUnleashConfig,
{
apiTokenService,
accessService,
openApiService,
}: Pick<
IUnleashServices,
'apiTokenService' | 'accessService' | 'openApiService'
>,
) {
super(config);
this.apiTokenService = services.apiTokenService;
this.accessService = services.accessService;
this.apiTokenService = apiTokenService;
this.accessService = accessService;
this.openApiService = openApiService;
this.logger = config.getLogger('api-token-controller.js');
this.get('/', this.getAllApiTokens, READ_API_TOKEN);
this.post('/', this.createApiToken, CREATE_API_TOKEN);
this.put('/:token', this.updateApiToken, UPDATE_API_TOKEN);
this.delete('/:token', this.deleteApiToken, DELETE_API_TOKEN);
this.route({
method: 'get',
path: '',
handler: this.getAllApiTokens,
permission: READ_API_TOKEN,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'getAllApiTokens',
responses: {
200: createResponseSchema('apiTokensSchema'),
},
}),
],
});
this.route({
method: 'post',
path: '',
handler: this.createApiToken,
permission: CREATE_API_TOKEN,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'createApiToken',
requestBody: createRequestSchema('createApiTokenSchema'),
responses: {
201: createResponseSchema('apiTokenSchema'),
},
}),
],
});
this.route({
method: 'put',
path: '/:token',
handler: this.updateApiToken,
permission: UPDATE_API_TOKEN,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'updateApiToken',
requestBody: createRequestSchema('updateApiTokenSchema'),
responses: {
200: emptyResponse,
},
}),
],
});
this.route({
method: 'delete',
path: '/:token',
handler: this.deleteApiToken,
acceptAnyContentType: true,
permission: DELETE_API_TOKEN,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'deleteApiToken',
responses: {
200: emptyResponse,
},
}),
],
});
}
async getAllApiTokens(req: IAuthRequest, res: Response): Promise<void> {
async getAllApiTokens(
req: IAuthRequest,
res: Response<ApiTokensSchema>,
): Promise<void> {
const { user } = req;
const tokens = await this.accessibleTokens(user);
res.json({ tokens });
this.openApiService.respondWithValidation(
200,
res,
apiTokensSchema.$id,
{ tokens: serializeDates(tokens) },
);
}
async createApiToken(req: IAuthRequest, res: Response): Promise<any> {
async createApiToken(
req: IAuthRequest,
res: Response<ApiTokenSchema>,
): Promise<any> {
const createToken = await createApiToken.validateAsync(req.body);
const token = await this.apiTokenService.createApiToken(createToken);
return res.status(201).json(token);
this.openApiService.respondWithValidation(
201,
res,
apiTokenSchema.$id,
serializeDates(token),
);
}
async deleteApiToken(req: IAuthRequest, res: Response): Promise<void> {
const { token } = req.params;
await this.apiTokenService.delete(token);
res.status(200).end();
}
async updateApiToken(req: IAuthRequest, res: Response): Promise<any> {
async updateApiToken(
req: IAuthRequest<TokenParam, void, UpdateApiTokenSchema>,
res: Response,
): Promise<any> {
const { token } = req.params;
const { expiresAt } = req.body;
@ -69,10 +168,20 @@ class ApiTokenController extends Controller {
return res.status(400).send();
}
await this.apiTokenService.updateExpiry(token, expiresAt);
await this.apiTokenService.updateExpiry(token, new Date(expiresAt));
return res.status(200).end();
}
async deleteApiToken(
req: IAuthRequest<TokenParam>,
res: Response,
): Promise<void> {
const { token } = req.params;
await this.apiTokenService.delete(token);
res.status(200).end();
}
private async accessibleTokens(user: User): Promise<IApiToken[]> {
const allTokens = await this.apiTokenService.getAllTokens();
@ -84,9 +193,6 @@ class ApiTokenController extends Controller {
return allTokens;
}
return allTokens.filter((t) => t.type !== ApiTokenType.ADMIN);
return allTokens.filter((token) => token.type !== ApiTokenType.ADMIN);
}
}
module.exports = ApiTokenController;
export default ApiTokenController;

View File

@ -96,7 +96,7 @@ export class EnvironmentsController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'toggleEnvironmentOn',
responses: { 200: emptyResponse },
responses: { 204: emptyResponse },
}),
],
});
@ -111,7 +111,7 @@ export class EnvironmentsController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'toggleEnvironmentOff',
responses: { 200: emptyResponse },
responses: { 204: emptyResponse },
}),
],
});

View File

@ -17,7 +17,7 @@ import StateController from './state';
import TagController from './tag';
import TagTypeController from './tag-type';
import AddonController from './addon';
import ApiTokenController from './api-token';
import { ApiTokenController } from './api-token';
import UserAdminController from './user-admin';
import EmailController from './email';
import UserFeedbackController from './user-feedback';

View File

@ -51,6 +51,68 @@ exports[`should serve the OpenAPI spec 1`] = `
Object {
"components": Object {
"schemas": Object {
"apiTokenSchema": Object {
"additionalProperties": false,
"properties": Object {
"createdAt": Object {
"format": "date-time",
"nullable": true,
"type": "string",
},
"environment": Object {
"type": "string",
},
"expiresAt": Object {
"format": "date-time",
"nullable": true,
"type": "string",
},
"project": Object {
"type": "string",
},
"projects": Object {
"items": Object {
"type": "string",
},
"type": "array",
},
"secret": Object {
"type": "string",
},
"seenAt": Object {
"format": "date-time",
"nullable": true,
"type": "string",
},
"type": Object {
"description": "client, admin.",
"type": "string",
},
"username": Object {
"type": "string",
},
},
"required": Array [
"username",
"type",
],
"type": "object",
},
"apiTokensSchema": Object {
"additionalProperties": false,
"properties": Object {
"tokens": Object {
"items": Object {
"$ref": "#/components/schemas/apiTokenSchema",
},
"type": "array",
},
},
"required": Array [
"tokens",
],
"type": "object",
},
"cloneFeatureSchema": Object {
"properties": Object {
"name": Object {
@ -151,6 +213,43 @@ Object {
},
"type": "array",
},
"createApiTokenSchema": Object {
"additionalProperties": false,
"properties": Object {
"environment": Object {
"type": "string",
},
"expiresAt": Object {
"format": "date-time",
"nullable": true,
"type": "string",
},
"project": Object {
"type": "string",
},
"projects": Object {
"items": Object {
"type": "string",
},
"type": "array",
},
"secret": Object {
"type": "string",
},
"type": Object {
"description": "client, admin.",
"type": "string",
},
"username": Object {
"type": "string",
},
},
"required": Array [
"username",
"type",
],
"type": "object",
},
"createFeatureSchema": Object {
"properties": Object {
"description": Object {
@ -919,6 +1018,19 @@ Object {
],
"type": "object",
},
"updateApiTokenSchema": Object {
"additionalProperties": false,
"properties": Object {
"expiresAt": Object {
"format": "date-time",
"type": "string",
},
},
"required": Array [
"expiresAt",
],
"type": "object",
},
"updateFeatureSchema": Object {
"properties": Object {
"archived": Object {
@ -1142,6 +1254,110 @@ Object {
},
"openapi": "3.0.3",
"paths": Object {
"/api/admin/api-tokens": Object {
"get": Object {
"operationId": "getAllApiTokens",
"responses": Object {
"200": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/apiTokensSchema",
},
},
},
"description": "apiTokensSchema",
},
},
"tags": Array [
"admin",
],
},
"post": Object {
"operationId": "createApiToken",
"requestBody": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/createApiTokenSchema",
},
},
},
"description": "createApiTokenSchema",
"required": true,
},
"responses": Object {
"201": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/apiTokenSchema",
},
},
},
"description": "apiTokenSchema",
},
},
"tags": Array [
"admin",
],
},
},
"/api/admin/api-tokens/{token}": Object {
"delete": Object {
"operationId": "deleteApiToken",
"parameters": Array [
Object {
"in": "path",
"name": "token",
"required": true,
"schema": Object {
"type": "string",
},
},
],
"responses": Object {
"200": Object {
"description": "emptyResponse",
},
},
"tags": Array [
"admin",
],
},
"put": Object {
"operationId": "updateApiToken",
"parameters": Array [
Object {
"in": "path",
"name": "token",
"required": true,
"schema": Object {
"type": "string",
},
},
],
"requestBody": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/updateApiTokenSchema",
},
},
},
"description": "updateApiTokenSchema",
"required": true,
},
"responses": Object {
"200": Object {
"description": "emptyResponse",
},
},
"tags": Array [
"admin",
],
},
},
"/api/admin/archive/features": Object {
"get": Object {
"deprecated": true,
@ -1497,7 +1713,7 @@ Object {
},
],
"responses": Object {
"200": Object {
"204": Object {
"description": "emptyResponse",
},
},
@ -1520,7 +1736,7 @@ Object {
},
],
"responses": Object {
"200": Object {
"204": Object {
"description": "emptyResponse",
},
},