mirror of
https://github.com/strapi/strapi.git
synced 2025-12-28 07:33:17 +00:00
implement POST endpoint to create api tokens
This commit is contained in:
parent
36af3134be
commit
3fb6b57808
49
packages/core/admin/server/content-types/api-token.js
Normal file
49
packages/core/admin/server/content-types/api-token.js
Normal file
@ -0,0 +1,49 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Lifecycle callbacks for the `ApiToken` model.
|
||||
*/
|
||||
module.exports = {
|
||||
collectionName: 'strapi_api_token',
|
||||
info: {
|
||||
name: 'Api Token',
|
||||
description: '',
|
||||
},
|
||||
options: {},
|
||||
pluginOptions: {
|
||||
'content-manager': {
|
||||
visible: false,
|
||||
},
|
||||
'content-type-builder': {
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
attributes: {
|
||||
name: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
configurable: false,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
configurable: false,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
configurable: false,
|
||||
required: false,
|
||||
default: 'read-only',
|
||||
},
|
||||
accessKey: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
configurable: false,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -4,4 +4,5 @@ module.exports = {
|
||||
permission: { schema: require('./Permission') },
|
||||
user: { schema: require('./User') },
|
||||
role: { schema: require('./Role') },
|
||||
['api-token']: { schema: require('./api-token') },
|
||||
};
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
'use strict';
|
||||
|
||||
const createContext = require('../../../../../../test/helpers/create-context');
|
||||
const apiTokenController = require('../api-token');
|
||||
|
||||
describe('API Token Controller', () => {
|
||||
describe('Create API Token', () => {
|
||||
const body = {
|
||||
name: 'api-token_tests-name',
|
||||
description: 'api-token_tests-description',
|
||||
type: 'read-only',
|
||||
};
|
||||
|
||||
test('Fails if API Token already exist', async () => {
|
||||
const exists = jest.fn(() => true);
|
||||
const badRequest = jest.fn();
|
||||
const ctx = createContext({ body }, { badRequest });
|
||||
|
||||
global.strapi = {
|
||||
admin: {
|
||||
services: {
|
||||
['api-token']: {
|
||||
exists,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await apiTokenController.create(ctx);
|
||||
|
||||
expect(exists).toHaveBeenCalledWith({ name: body.name });
|
||||
expect(badRequest).toHaveBeenCalledWith('Name already taken');
|
||||
});
|
||||
|
||||
test('Create API Token Successfully', async () => {
|
||||
const create = jest.fn().mockResolvedValue(body);
|
||||
const exists = jest.fn(() => false);
|
||||
const badRequest = jest.fn();
|
||||
const created = jest.fn();
|
||||
const ctx = createContext({ body }, { badRequest, created });
|
||||
|
||||
global.strapi = {
|
||||
admin: {
|
||||
services: {
|
||||
['api-token']: {
|
||||
exists,
|
||||
create,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await apiTokenController.create(ctx);
|
||||
|
||||
expect(exists).toHaveBeenCalledWith({ name: body.name });
|
||||
expect(badRequest).not.toHaveBeenCalled();
|
||||
expect(create).toHaveBeenCalledWith(body);
|
||||
expect(created).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
29
packages/core/admin/server/controllers/api-token.js
Normal file
29
packages/core/admin/server/controllers/api-token.js
Normal file
@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
const pick = require('lodash/pick');
|
||||
|
||||
const { getService } = require('../utils');
|
||||
const { validateApiTokenCreationInput } = require('../validation/api-tokens');
|
||||
|
||||
module.exports = {
|
||||
async create(ctx) {
|
||||
const { body } = ctx.request;
|
||||
const apiTokenService = getService('api-token');
|
||||
|
||||
try {
|
||||
await validateApiTokenCreationInput(body);
|
||||
} catch (err) {
|
||||
return ctx.badRequest('ValidationError', err);
|
||||
}
|
||||
|
||||
const attributes = pick(body, ['name', 'description', 'type']);
|
||||
|
||||
if (apiTokenService.exists({ name: attributes.name })) {
|
||||
return ctx.badRequest('Name already taken');
|
||||
}
|
||||
|
||||
const apiToken = await apiTokenService.create(attributes);
|
||||
|
||||
ctx.created({ data: apiToken });
|
||||
},
|
||||
};
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
module.exports = {
|
||||
admin: require('./admin'),
|
||||
'api-token': require('./api-token'),
|
||||
'authenticated-user': require('./authenticated-user'),
|
||||
authentication: require('./authentication'),
|
||||
permission: require('./permission'),
|
||||
|
||||
15
packages/core/admin/server/routes/api-tokens.js
Normal file
15
packages/core/admin/server/routes/api-tokens.js
Normal file
@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api-tokens',
|
||||
handler: 'api-token.create',
|
||||
config: {
|
||||
policies: [
|
||||
'admin::isAuthenticatedAdmin',
|
||||
{ name: 'admin::hasPermissions', options: { actions: ['admin::api-tokens.create'] } },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -0,0 +1,48 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const apiTokenService = require('../api-token');
|
||||
|
||||
describe('API Token', () => {
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
test('Creates a new token', async () => {
|
||||
const create = jest.fn(({ data }) => Promise.resolve(data));
|
||||
|
||||
const mockedApiToken = {
|
||||
randomBytes: 'api-token_test-random-bytes',
|
||||
hexedString: '6170692d746f6b656e5f746573742d72616e646f6d2d6279746573',
|
||||
};
|
||||
|
||||
crypto.randomBytes = jest.fn(() => Buffer.from(mockedApiToken.randomBytes));
|
||||
|
||||
global.strapi = {
|
||||
query() {
|
||||
return { create };
|
||||
},
|
||||
};
|
||||
|
||||
const attributes = {
|
||||
name: 'api-token_tests-name',
|
||||
description: 'api-token_tests-description',
|
||||
type: 'read-only',
|
||||
};
|
||||
|
||||
const res = await apiTokenService.create(attributes);
|
||||
|
||||
expect(create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...attributes,
|
||||
accessKey: mockedApiToken.hexedString,
|
||||
},
|
||||
});
|
||||
expect(res).toEqual({
|
||||
...attributes,
|
||||
accessKey: mockedApiToken.hexedString,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
38
packages/core/admin/server/services/api-token.js
Normal file
38
packages/core/admin/server/services/api-token.js
Normal file
@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* @param {Object} attributes
|
||||
* @param {string} attributes.name
|
||||
* @param {string} [attributes.description]
|
||||
*
|
||||
* @returns boolean
|
||||
*/
|
||||
const exists = (attributes = {}) => {
|
||||
return strapi.query('strapi::api-token').count({ where: attributes }) > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} attributes
|
||||
* @param {'read-only'|'full-access'} attributes.type
|
||||
* @param {string} attributes.name
|
||||
* @param {string} [attributes.description]
|
||||
*
|
||||
* @returns {Promise<Record<'id'|'name'|'description'|'type'|'accessKey', string>>}
|
||||
*/
|
||||
const create = async attributes => {
|
||||
const accessKey = crypto.randomBytes(128).toString('hex');
|
||||
|
||||
return strapi.query('strapi::api-token').create({
|
||||
data: {
|
||||
...attributes,
|
||||
accessKey,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
exists,
|
||||
};
|
||||
@ -12,4 +12,5 @@ module.exports = {
|
||||
condition: require('./condition'),
|
||||
auth: require('./auth'),
|
||||
action: require('./action'),
|
||||
['api-token']: require('./api-token'),
|
||||
};
|
||||
|
||||
95
packages/core/admin/server/tests/admin-api-token.test.e2e.js
Normal file
95
packages/core/admin/server/tests/admin-api-token.test.e2e.js
Normal file
@ -0,0 +1,95 @@
|
||||
'use strict';
|
||||
|
||||
const { createStrapiInstance } = require('../../../../../test/helpers/strapi');
|
||||
const { createAuthRequest } = require('../../../../../test/helpers/request');
|
||||
|
||||
/**
|
||||
* == Test Suite Overview ==
|
||||
*
|
||||
* N° Description
|
||||
* -------------------------------------------
|
||||
* 1. Creates an api token (wrong body)
|
||||
* 2. Creates an api token (successfully)
|
||||
*/
|
||||
|
||||
describe('Admin API Token CRUD (e2e)', () => {
|
||||
let rq;
|
||||
let strapi;
|
||||
|
||||
// Initialization Actions
|
||||
beforeAll(async () => {
|
||||
strapi = await createStrapiInstance();
|
||||
rq = await createAuthRequest({ strapi });
|
||||
});
|
||||
|
||||
// Cleanup actions
|
||||
afterAll(async () => {
|
||||
await strapi.destroy();
|
||||
});
|
||||
|
||||
test('1. Creates an api token (wrong body)', async () => {
|
||||
const body = {
|
||||
name: 'api-token_tests-name',
|
||||
description: 'api-token_tests-description',
|
||||
};
|
||||
|
||||
const res = await rq({
|
||||
url: '/admin/api-tokens',
|
||||
method: 'POST',
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
expect(res.body).toMatchObject({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'ValidationError',
|
||||
data: {
|
||||
type: ['type is a required field'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('2. Creates an api token (successfully)', async () => {
|
||||
const body = {
|
||||
name: 'api-token_tests-name',
|
||||
description: 'api-token_tests-description',
|
||||
type: 'read-only',
|
||||
};
|
||||
|
||||
const res = await rq({
|
||||
url: '/admin/api-tokens',
|
||||
method: 'POST',
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(201);
|
||||
expect(res.body.data).not.toBeNull();
|
||||
expect(res.body.data.id).toBeDefined();
|
||||
expect(res.body.data.accessKey).toBeDefined();
|
||||
expect(res.body.data.name).toBe(body.name);
|
||||
expect(res.body.data.description).toBe(body.description);
|
||||
expect(res.body.data.type).toBe(body.type);
|
||||
});
|
||||
|
||||
test('3. Creates an api token without a description (successfully)', async () => {
|
||||
const body = {
|
||||
name: 'api-token_tests-name',
|
||||
type: 'read-only',
|
||||
};
|
||||
|
||||
const res = await rq({
|
||||
url: '/admin/api-tokens',
|
||||
method: 'POST',
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(201);
|
||||
expect(res.body.data).not.toBeNull();
|
||||
expect(res.body.data.id).toBeDefined();
|
||||
expect(res.body.data.accessKey).toBeDefined();
|
||||
expect(res.body.data.name).toBe(body.name);
|
||||
expect(res.body.data.description).toBe('');
|
||||
expect(res.body.data.type).toBe(body.type);
|
||||
});
|
||||
});
|
||||
2
packages/core/admin/server/utils/index.d.ts
vendored
2
packages/core/admin/server/utils/index.d.ts
vendored
@ -5,6 +5,7 @@ import * as contentType from '../services/content-type';
|
||||
import * as metrics from '../services/metrics';
|
||||
import * as token from '../services/token';
|
||||
import * as auth from '../services/auth';
|
||||
import * as apiToken from '../services/api-token';
|
||||
|
||||
type S = {
|
||||
role: typeof role;
|
||||
@ -14,6 +15,7 @@ type S = {
|
||||
token: typeof token;
|
||||
auth: typeof auth;
|
||||
metrics: typeof metrics;
|
||||
['api-token']: typeof apiToken;
|
||||
};
|
||||
|
||||
export function getService<T extends keyof S>(name: T): ReturnType<S[T]>;
|
||||
|
||||
34
packages/core/admin/server/validation/api-tokens.js
Normal file
34
packages/core/admin/server/validation/api-tokens.js
Normal file
@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
const { yup, formatYupErrors } = require('@strapi/utils');
|
||||
|
||||
const handleReject = error => Promise.reject(formatYupErrors(error));
|
||||
|
||||
const apiTokenCreationSchema = yup
|
||||
.object()
|
||||
.shape({
|
||||
name: yup
|
||||
.string()
|
||||
.min(1)
|
||||
.required(),
|
||||
description: yup.string().optional(),
|
||||
type: yup
|
||||
.string()
|
||||
.oneOf(['read-only', 'full-access'])
|
||||
.required(),
|
||||
})
|
||||
.noUnknown();
|
||||
|
||||
const validateApiTokenCreationInput = data => {
|
||||
return apiTokenCreationSchema
|
||||
.validate(data, { strict: true, abortEarly: false })
|
||||
.catch(handleReject);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validateApiTokenCreationInput,
|
||||
|
||||
schemas: {
|
||||
apiTokenCreationSchema,
|
||||
},
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user