From 403234ae81876a2393fc8331bf3d5ee76848cd54 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Mon, 13 May 2019 21:29:00 +0200 Subject: [PATCH] Moves API key creation to server --- server/controllers/user.controller.js | 40 +---- .../user.controller/__tests__/apiKey.test.js | 143 ++++++++++++++++++ server/controllers/user.controller/apiKey.js | 66 ++++++++ server/models/__mocks__/user.js | 12 ++ server/routes/user.routes.js | 2 +- 5 files changed, 224 insertions(+), 39 deletions(-) create mode 100644 server/controllers/user.controller/__tests__/apiKey.test.js create mode 100644 server/controllers/user.controller/apiKey.js create mode 100644 server/models/__mocks__/user.js diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index 8243fc8c..3b5b2705 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -8,6 +8,8 @@ import { renderResetPassword, } from '../views/mail'; +export * from './user.controller/apiKey'; + const random = (done) => { crypto.randomBytes(20, (err, buf) => { const token = buf.toString('hex'); @@ -353,41 +355,3 @@ export function updateSettings(req, res) { }); } -export function addApiKey(req, res) { - User.findById(req.user.id, (err, user) => { - if (err) { - res.status(500).json({ error: err }); - return; - } - if (!user) { - res.status(404).json({ error: 'User not found' }); - return; - } - if (!req.body.label || !req.body.encodedKey) { - res.status(400).json({ error: 'Expected field \'label\' or \'encodedKey\' was not present in request body' }); - return; - } - user.apiKeys.push({ label: req.body.label, hashedKey: req.body.encodedKey }); - saveUser(res, user); - }); -} - -export function removeApiKey(req, res) { - User.findById(req.user.id, (err, user) => { - if (err) { - res.status(500).json({ error: err }); - return; - } - if (!user) { - res.status(404).json({ error: 'User not found' }); - return; - } - const keyToDelete = user.apiKeys.find(key => key.id === req.params.keyId); - if (!keyToDelete) { - res.status(404).json({ error: 'Key does not exist for user' }); - return; - } - user.apiKeys.pull({ _id: req.params.keyId }); - saveUser(res, user); - }); -} diff --git a/server/controllers/user.controller/__tests__/apiKey.test.js b/server/controllers/user.controller/__tests__/apiKey.test.js new file mode 100644 index 00000000..01fe67e8 --- /dev/null +++ b/server/controllers/user.controller/__tests__/apiKey.test.js @@ -0,0 +1,143 @@ +/* @jest-environment node */ + +import { createApiKey, removeApiKey } from '../../user.controller/apiKey'; + +jest.mock('../../../models/user'); + +const createResponseMock = function (done) { + const json = jest.fn(() => { + if (done) { done(); } + }); + const status = jest.fn(() => ({ json })); + + return { + status, + json + }; +}; + +const User = require('../../../models/user').default; + +describe('user.controller', () => { + beforeEach(() => { + User.__setFindById(null, null); + }); + + describe('createApiKey', () => { + it('returns an error if user doesn\'t exist', () => { + const request = { user: { id: '1234' } }; + const response = createResponseMock(); + + createApiKey(request, response); + + expect(User.findById.mock.calls[0][0]).toBe('1234'); + expect(response.status).toHaveBeenCalledWith(404); + expect(response.json).toHaveBeenCalledWith({ + error: 'User not found' + }); + }); + + it('returns an error if label not provided', () => { + User.__setFindById(undefined, { + apiKeys: [] + }); + const request = { user: { id: '1234' }, body: {} }; + const response = createResponseMock(); + + createApiKey(request, response); + + expect(response.status).toHaveBeenCalledWith(400); + expect(response.json).toHaveBeenCalledWith({ + error: 'Expected field \'label\' was not present in request body' + }); + }); + + it('returns generated API key to the user', (done) => { + let response; + + const request = { + user: { id: '1234' }, + body: { label: 'my key' } + }; + + const foundUser = { + apiKeys: [], + save: jest.fn(callback => callback()) + }; + + const checkExpecations = () => { + expect(foundUser.apiKeys[0].label).toBe('my key'); + expect(typeof foundUser.apiKeys[0].hashedKey).toBe('string'); + + expect(response.json).toHaveBeenCalledWith({ + token: foundUser.apiKeys[0].hashedKey + }); + + done(); + }; + + response = createResponseMock(checkExpecations); + + User.__setFindById(undefined, foundUser); + + createApiKey(request, response); + }); + }); + + describe('removeApiKey', () => { + it('returns an error if user doesn\'t exist', () => { + const request = { user: { id: '1234' } }; + const response = createResponseMock(); + + removeApiKey(request, response); + + expect(User.findById.mock.calls[0][0]).toBe('1234'); + expect(response.status).toHaveBeenCalledWith(404); + expect(response.json).toHaveBeenCalledWith({ + error: 'User not found' + }); + }); + + it('returns an error if specified key doesn\'t exist', () => { + const request = { + user: { id: '1234' }, + params: { keyId: 'not-a-real-key' } + }; + const response = createResponseMock(); + + const foundUser = { + apiKeys: [], + save: jest.fn(callback => callback()) + }; + User.__setFindById(undefined, foundUser); + + removeApiKey(request, response); + + expect(response.status).toHaveBeenCalledWith(404); + expect(response.json).toHaveBeenCalledWith({ + error: 'Key does not exist for user' + }); + }); + + it.skip('removes key if it exists', () => { + const request = { + user: { id: '1234' }, + params: { keyId: 'the-key' } + }; + const response = createResponseMock(); + + const foundUser = { + apiKeys: [{ label: 'the-label', id: 'the-key' }], + save: jest.fn(callback => callback()) + }; + User.__setFindById(undefined, foundUser); + + removeApiKey(request, response); + + expect(response.status).toHaveBeenCalledWith(404); + expect(response.json).toHaveBeenCalledWith({ + error: 'Key does not exist for user' + }); + }); + }); +}); diff --git a/server/controllers/user.controller/apiKey.js b/server/controllers/user.controller/apiKey.js new file mode 100644 index 00000000..35a0ba36 --- /dev/null +++ b/server/controllers/user.controller/apiKey.js @@ -0,0 +1,66 @@ +import crypto from 'crypto'; + +import User from '../../models/user'; + +/** + * Generates a unique token to be used as a Personal Access Token + * @returns Promise A promise that resolves to the token, or an Error + */ +function generateApiKey() { + return new Promise((resolve, reject) => { + crypto.randomBytes(20, (err, buf) => { + if (err) { + reject(err); + } + const key = buf.toString('hex'); + resolve(Buffer.from(key).toString('base64')); + }); + }); +} + +export function createApiKey(req, res) { + User.findById(req.user.id, async (err, user) => { + if (!user) { + res.status(404).json({ error: 'User not found' }); + return; + } + + if (!req.body.label) { + res.status(400).json({ error: 'Expected field \'label\' was not present in request body' }); + return; + } + + const hashedKey = await generateApiKey(); + + user.apiKeys.push({ label: req.body.label, hashedKey }); + + user.save((saveErr) => { + if (saveErr) { + res.status(500).json({ error: saveErr }); + return; + } + + res.json({ token: hashedKey }); + }); + }); +} + +export function removeApiKey(req, res) { + User.findById(req.user.id, (err, user) => { + if (err) { + res.status(500).json({ error: err }); + return; + } + if (!user) { + res.status(404).json({ error: 'User not found' }); + return; + } + const keyToDelete = user.apiKeys.find(key => key.id === req.params.keyId); + if (!keyToDelete) { + res.status(404).json({ error: 'Key does not exist for user' }); + return; + } + user.apiKeys.pull({ _id: req.params.keyId }); + saveUser(res, user); + }); +} diff --git a/server/models/__mocks__/user.js b/server/models/__mocks__/user.js new file mode 100644 index 00000000..585d8b67 --- /dev/null +++ b/server/models/__mocks__/user.js @@ -0,0 +1,12 @@ +let __err = null; +let __user = null; + +export default { + __setFindById(err, user) { + __err = err; + __user = user; + }, + findById: jest.fn(async (id, callback) => { + callback(__err, __user); + }) +}; diff --git a/server/routes/user.routes.js b/server/routes/user.routes.js index 49b09a94..3273025d 100644 --- a/server/routes/user.routes.js +++ b/server/routes/user.routes.js @@ -18,7 +18,7 @@ router.post('/reset-password/:token', UserController.updatePassword); router.put('/account', isAuthenticated, UserController.updateSettings); -router.put('/account/api-keys', isAuthenticated, UserController.addApiKey); +router.post('/account/api-keys', isAuthenticated, UserController.createApiKey); router.delete('/account/api-keys/:keyId', isAuthenticated, UserController.removeApiKey);