346 lines
10 KiB
JavaScript
346 lines
10 KiB
JavaScript
/**
|
|
* @fileOverview File to manage integration authorisation token related operations
|
|
*/
|
|
'use strict';
|
|
|
|
const httpStatus = require('http-status-codes');
|
|
const Q = require('q');
|
|
const _ = require('lodash');
|
|
const jwt = require('jsonwebtoken');
|
|
const debug = require('debug')('webconsole-api:controllers:tokens');
|
|
|
|
const utils = require(global.pathPrefix + 'utils.js');
|
|
const mainDB = require(global.pathPrefix + 'mainDB.js');
|
|
const references = require(global.pathPrefix + '../utils/references.js');
|
|
const featureFlags = require(global.pathPrefix + '../utils/feature-flags/feature-flags.js');
|
|
const responsesUtils = require(global.pathPrefix + '../utils/responses.js');
|
|
const tokenUtils = require(global.pathPrefix + '../utils/tokens.js');
|
|
|
|
module.exports = {
|
|
listTokens: listTokens,
|
|
createToken: createToken,
|
|
deleteToken: deleteToken
|
|
};
|
|
|
|
const TOKENS_FEATURE_FLAG = 'tokens';
|
|
const JWT_SECRET = require(global.configFile).integrationsTokenSecret;
|
|
const JWT_ALGORITHM = 'HS256'; // HMAC + SHA256 only
|
|
const JWT_ISSUER = 'bridge-v1'; // Issuer string
|
|
const JWT_OPTIONS = {
|
|
algorithm: JWT_ALGORITHM,
|
|
issuer: JWT_ISSUER,
|
|
noTimestamp: true
|
|
};
|
|
|
|
const FAILED_CREATE_JWT = 'BRIDGE: failed to create the JWT';
|
|
const DATABASE_UPDATE_FAILED = 'BRIDGE: DB update failed';
|
|
|
|
/**
|
|
* Lists the tokens that belong to the current client.
|
|
*
|
|
* @param {Object} req - the request object
|
|
* @param {Object} res - the response object
|
|
*/
|
|
function listTokens(req, res) {
|
|
const clientID = req.session.data.clientID;
|
|
const clientP = references.getClient(clientID);
|
|
|
|
//
|
|
// Iterate through the tokens we have, and turn them into JWTs for returning
|
|
// to the caller
|
|
//
|
|
const listP = clientP.then((client) => {
|
|
let jwtPromises = [];
|
|
const tokens = client.IntegrationTokens || [];
|
|
|
|
for (let i = 0; i < tokens.length; ++i) {
|
|
//
|
|
// Define the payload
|
|
//
|
|
const jwtPayload = {
|
|
id: clientID,
|
|
token: tokens[i].token
|
|
};
|
|
const name = tokens[i].name;
|
|
|
|
//
|
|
// Call the JWT signing function
|
|
//
|
|
const jwtP = Q.nfcall(jwt.sign, jwtPayload, JWT_SECRET, JWT_OPTIONS)
|
|
.then((jwt) => {
|
|
debug('Token encoded', i);
|
|
return {
|
|
name: name,
|
|
token: jwt
|
|
};
|
|
})
|
|
.catch((error) => {
|
|
debug('Failed to encode', error, jwtPayload);
|
|
return Q.reject(FAILED_CREATE_JWT);
|
|
});
|
|
|
|
//
|
|
// Save the promises to the array that we will return
|
|
//
|
|
jwtPromises.push(jwtP);
|
|
}
|
|
return Q.all(jwtPromises);
|
|
});
|
|
|
|
//
|
|
// Send the response depending on results
|
|
//
|
|
Q.all([clientP, listP])
|
|
.spread((client, tokens) => {
|
|
res.status(httpStatus.OK).json(tokens);
|
|
})
|
|
.catch((error) => {
|
|
debug(' - error listing tokens', error);
|
|
const responses = [
|
|
[
|
|
'MongoError',
|
|
httpStatus.BAD_GATEWAY, 31101, 'Database Offline', true
|
|
],
|
|
[
|
|
references.ERRORS.INVALID_CLIENT,
|
|
httpStatus.BAD_REQUEST, 31102, 'Client not found', true
|
|
],
|
|
[
|
|
FAILED_CREATE_JWT,
|
|
httpStatus.INTERNAL_SERVER_ERROR, 31103, 'Failed to generate tokens list.'
|
|
]
|
|
];
|
|
const responseHandler = new responsesUtils.ErrorResponses(responses);
|
|
responseHandler.respond(res, error);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Function to create a token for a merchant
|
|
*
|
|
* @param {Object} req - the request object
|
|
* @param {Object} res - the response object
|
|
*/
|
|
function createToken(req, res) {
|
|
//
|
|
// Check the client is a merchant
|
|
//
|
|
if (!req.session.data.isMerchant) {
|
|
res.status(httpStatus.FORBIDDEN).json({
|
|
code: 999,
|
|
info: 'Client is not a merchant.'
|
|
});
|
|
return;
|
|
}
|
|
|
|
//
|
|
// Get the current user's details from the session
|
|
//
|
|
const clientID = req.session.data.clientID;
|
|
const tokenName = req.swagger.params.body.value.name;
|
|
const clientP = references.getClient(clientID);
|
|
|
|
//
|
|
// Check that we have the feature flag enabled for this, and that we don't
|
|
// already have too many tokens.
|
|
//
|
|
const NOT_ENABLED = 'BRIDGE: Not enabled';
|
|
const TOO_MANY_TOKENS = 'BRIDGE: Too many tokens';
|
|
const enabledP = clientP.then((client) => {
|
|
if (!featureFlags.isEnabled(TOKENS_FEATURE_FLAG, client)) {
|
|
return Q.reject(NOT_ENABLED);
|
|
} else if (
|
|
_.isArray(client.IntegrationTokens) &&
|
|
client.IntegrationTokens.length >= utils.MaxIntegrationTokens
|
|
) {
|
|
return Q.reject(TOO_MANY_TOKENS);
|
|
} else {
|
|
return client; // So we can cascade
|
|
}
|
|
});
|
|
|
|
//
|
|
// Push a random token into the IntegrationsTokens array on the client object
|
|
//
|
|
const token = utils.timeBasedRandomCode();
|
|
const addedP = enabledP.then((client) => {
|
|
const query = {
|
|
_id: client._id
|
|
};
|
|
const update = {
|
|
$push: {
|
|
IntegrationTokens: {
|
|
token: token,
|
|
name: tokenName
|
|
}
|
|
},
|
|
$inc: {
|
|
LastVersion: 1
|
|
},
|
|
$currentDate: {
|
|
LastUpdate: true
|
|
}
|
|
};
|
|
|
|
return mainDB.collectionClient
|
|
.updateOne(query, update)
|
|
.then((res) => {
|
|
if (res.modifiedCount === 1) {
|
|
return Q.resolve();
|
|
} else {
|
|
return Q.reject(DATABASE_UPDATE_FAILED);
|
|
}
|
|
});
|
|
});
|
|
|
|
//
|
|
// Build a JWT based on the token (if it was added correctly)
|
|
//
|
|
const jwtPayload = {
|
|
id: clientID,
|
|
token: token
|
|
};
|
|
const jwtP = addedP.then(
|
|
() => Q.nfcall(jwt.sign, jwtPayload, JWT_SECRET, JWT_OPTIONS)
|
|
.catch(() => Q.reject(FAILED_CREATE_JWT))
|
|
);
|
|
|
|
Q.all([clientP, enabledP, addedP, jwtP])
|
|
.then(
|
|
(results) => {
|
|
const jwt = results[3];
|
|
res.status(httpStatus.OK).json({
|
|
token: jwt
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
debug(' - error creating token', error);
|
|
const responses = [
|
|
[
|
|
'MongoError',
|
|
httpStatus.BAD_GATEWAY, 31111, 'Database Offline', true
|
|
],
|
|
[
|
|
references.ERRORS.INVALID_CLIENT,
|
|
httpStatus.BAD_REQUEST, 31112, 'Client not found', true
|
|
],
|
|
[
|
|
NOT_ENABLED,
|
|
httpStatus.BAD_REQUEST, 31113, 'Tokens not enabled.'
|
|
],
|
|
[
|
|
TOO_MANY_TOKENS,
|
|
httpStatus.CONFLICT, 31116, 'Too many tokens.'
|
|
],
|
|
[
|
|
DATABASE_UPDATE_FAILED,
|
|
httpStatus.BAD_GATEWAY, 31114, 'Failed to store token.'
|
|
],
|
|
[
|
|
FAILED_CREATE_JWT,
|
|
httpStatus.INTERNAL_SERVER_ERROR, 31115, 'Failed to produce final token.'
|
|
]
|
|
];
|
|
const responseHandler = new responsesUtils.ErrorResponses(responses);
|
|
responseHandler.respond(res, error);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Deletes a token that belongs to the current client.
|
|
*
|
|
* @param {Object} req - the request object
|
|
* @param {Object} res - the response object
|
|
*/
|
|
function deleteToken(req, res) {
|
|
//
|
|
// Get the current user's details from the session
|
|
//
|
|
const clientID = req.session.data.clientID;
|
|
const token = req.swagger.params.token.value;
|
|
|
|
//
|
|
// Validate the token
|
|
//
|
|
const DIFFERENT_CLIENT = 'BRIDGE: Token belongs to a different client';
|
|
let validateP = tokenUtils.validateToken(token).then((result) => {
|
|
//
|
|
// The token is valid, but may belong to a different client
|
|
//
|
|
if (result.client.ClientID !== clientID) {
|
|
return Q.reject(DIFFERENT_CLIENT);
|
|
} else {
|
|
return result;
|
|
}
|
|
});
|
|
|
|
//
|
|
// Delete the token from the list
|
|
//
|
|
let deleteP = validateP.then((result) => {
|
|
const query = {
|
|
_id: result.client._id
|
|
};
|
|
const update = {
|
|
$pull: {
|
|
IntegrationTokens: {
|
|
token: result.decoded.token
|
|
}
|
|
},
|
|
$inc: {
|
|
LastVersion: 1
|
|
},
|
|
$currentDate: {
|
|
LastUpdate: true
|
|
}
|
|
};
|
|
|
|
return mainDB.collectionClient
|
|
.updateOne(query, update)
|
|
.then((res) => {
|
|
if (res.modifiedCount === 1) {
|
|
return Q.resolve();
|
|
} else {
|
|
return Q.reject(DATABASE_UPDATE_FAILED);
|
|
}
|
|
});
|
|
});
|
|
|
|
Q.all([validateP, deleteP])
|
|
.then(() => {
|
|
res.status(httpStatus.OK).json();
|
|
})
|
|
.catch((error) => {
|
|
debug(' - error creating token', error);
|
|
const responses = [
|
|
[
|
|
'MongoError',
|
|
httpStatus.BAD_GATEWAY, 31121, 'Database Offline', true
|
|
],
|
|
|
|
//
|
|
// Not that we give a similar error response to a number of cases
|
|
// to reduce the amount of information we return about tokens
|
|
//
|
|
[
|
|
tokenUtils.ERRORS.TOKEN_INVALID,
|
|
httpStatus.BAD_REQUEST, 31122, 'Invalid Token'
|
|
],
|
|
[
|
|
tokenUtils.ERRORS.CLIENT_NOT_FOUND,
|
|
httpStatus.BAD_REQUEST, 31123, 'Invalid Token'
|
|
],
|
|
[
|
|
DIFFERENT_CLIENT,
|
|
httpStatus.BAD_REQUEST, 31124, 'Invalid Token'
|
|
],
|
|
[
|
|
DATABASE_UPDATE_FAILED,
|
|
httpStatus.BAD_GATEWAY, 31125, 'Failed to delete token.'
|
|
]
|
|
];
|
|
const responseHandler = new responsesUtils.ErrorResponses(responses);
|
|
responseHandler.respond(res, error);
|
|
});
|
|
}
|