Moves API key creation to server
This commit is contained in:
parent
a860762683
commit
403234ae81
5 changed files with 224 additions and 39 deletions
|
@ -8,6 +8,8 @@ import {
|
||||||
renderResetPassword,
|
renderResetPassword,
|
||||||
} from '../views/mail';
|
} from '../views/mail';
|
||||||
|
|
||||||
|
export * from './user.controller/apiKey';
|
||||||
|
|
||||||
const random = (done) => {
|
const random = (done) => {
|
||||||
crypto.randomBytes(20, (err, buf) => {
|
crypto.randomBytes(20, (err, buf) => {
|
||||||
const token = buf.toString('hex');
|
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
143
server/controllers/user.controller/__tests__/apiKey.test.js
Normal file
143
server/controllers/user.controller/__tests__/apiKey.test.js
Normal file
|
@ -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'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
66
server/controllers/user.controller/apiKey.js
Normal file
66
server/controllers/user.controller/apiKey.js
Normal file
|
@ -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<String> 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);
|
||||||
|
});
|
||||||
|
}
|
12
server/models/__mocks__/user.js
Normal file
12
server/models/__mocks__/user.js
Normal file
|
@ -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);
|
||||||
|
})
|
||||||
|
};
|
|
@ -18,7 +18,7 @@ router.post('/reset-password/:token', UserController.updatePassword);
|
||||||
|
|
||||||
router.put('/account', isAuthenticated, UserController.updateSettings);
|
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);
|
router.delete('/account/api-keys/:keyId', isAuthenticated, UserController.removeApiKey);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue