264 lines
8.3 KiB
JavaScript
264 lines
8.3 KiB
JavaScript
//
|
|
// This file contains utilities related to the web-focused API
|
|
//
|
|
'use strict';
|
|
|
|
const Q = require('q');
|
|
const _ = require('lodash');
|
|
const crypto = require('crypto');
|
|
|
|
const config = require(global.configFile);
|
|
const debug = require('debug')('webconsole-api:api_utils');
|
|
const apiSecurity = require('./api_security.js');
|
|
|
|
const utils = require(global.pathPrefix + 'utils.js');
|
|
const hashUtil = require(global.pathPrefix + '../utils/hashing.js');
|
|
|
|
const ERRORS = {
|
|
SESSION_REGEN_FAILED: 'Session Regen: Failed',
|
|
SESSION_DESTROY_FAILED: 'Session Destroy: Failed'
|
|
};
|
|
|
|
module.exports = {
|
|
initSession,
|
|
initRecoverySession,
|
|
|
|
encodePassword,
|
|
|
|
addPortAndAddress,
|
|
|
|
ERRORS
|
|
};
|
|
|
|
/**
|
|
* Initialises the session for the specified client
|
|
*
|
|
* @param {Object} req - the express request object (for session and swagger)
|
|
* @param {string} email - the email for the client
|
|
* @param {Object} data - the data to store in the session
|
|
*
|
|
* @returns {Promise} - a promise for the completion of this function
|
|
*/
|
|
function initSessionBase(req, email, data) {
|
|
debug(' - initialising session for:', email);
|
|
const defer = Q.defer();
|
|
|
|
if (data.isDeviceSession) {
|
|
//
|
|
// Device sessions are simpler, and initialised every command. Therefore
|
|
// we don't want the session middleware to be saving them to persist them.
|
|
// Therefore we prevent saving by clearing req.sessionID
|
|
//
|
|
req.sessionID = null;
|
|
|
|
//
|
|
// Still store the session data in the standard place if we were persisting
|
|
// sessions.
|
|
//
|
|
req.session.data = data;
|
|
|
|
//
|
|
// And resolve the promise
|
|
//
|
|
defer.resolve({});
|
|
} else {
|
|
//
|
|
// Webconsole sessions are full sessions
|
|
// Reset the session id (for security)
|
|
//
|
|
const dRegen = Q.defer();
|
|
req.session.regenerate((err) => {
|
|
if (err) {
|
|
debug('- failed to regenerate session: ', err);
|
|
dRegen.reject(ERRORS.SESSION_REGEN_FAILED);
|
|
} else {
|
|
//
|
|
// Save the client info into the new session for future use
|
|
//
|
|
req.session.data = data;
|
|
dRegen.resolve(req.session);
|
|
}
|
|
});
|
|
|
|
//
|
|
// Set the XSRF token as a hash of the session token, salted with
|
|
// the users email address, then send it back in the body of the
|
|
// response (as JS can't read a token from a cross site request).
|
|
//
|
|
dRegen.promise
|
|
.then((session) => {
|
|
const xsrfTokenGen = apiSecurity.generateXsrfToken(session.id, email);
|
|
return xsrfTokenGen.on('readable', () => {
|
|
const response = {};
|
|
const securityDef = req.swagger.swaggerObject.securityDefinitions;
|
|
const tokenName = securityDef.bridge_session.name;
|
|
|
|
response[tokenName] = xsrfTokenGen.read();
|
|
|
|
defer.resolve(response);
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
defer.reject(error);
|
|
});
|
|
}
|
|
|
|
return defer.promise;
|
|
}
|
|
|
|
/**
|
|
* Initialises the standard session for the specified client
|
|
*
|
|
* @param {Object} req - the express request object (for session and swagger)
|
|
* @param {Object} client - the client object to build the session for
|
|
* @param {Object?} device - the device object (for device logins only)
|
|
*
|
|
* @returns {Promise} - a promise for the completion of this function
|
|
*/
|
|
function initSession(req, client, device) {
|
|
const email = client.ClientName;
|
|
|
|
//
|
|
// Client is a merchant if any of the items in the Merchant array
|
|
// have MerchantStatus === 1
|
|
// Client is VAT registered if they are both a merchant and have
|
|
// a VAT number specified.
|
|
//
|
|
const isMerchant = _.some(client.Merchant, {
|
|
MerchantStatus: 1
|
|
});
|
|
const isVATRegistered = _.some(client.Merchant, (item) => {
|
|
return item.MerchantStatus && item.VATNo;
|
|
});
|
|
const data = {
|
|
client: client._id,
|
|
clientID: client.ClientID,
|
|
email,
|
|
displayName: client.DisplayName,
|
|
isMerchant,
|
|
isVATRegistered,
|
|
FeatureFlags: client.FeatureFlags, // Case needs to match that expected by flags utils
|
|
clientObj: client,
|
|
deviceObj: device,
|
|
isDeviceSession: !_.isUndefined(device)
|
|
};
|
|
|
|
return initSessionBase(req, email, data)
|
|
.then((response) => {
|
|
//
|
|
// Update the response with the other values
|
|
//
|
|
response.emailConfirmNeeded = !utils.bitsAllSet(
|
|
client.ClientStatus,
|
|
utils.ClientEmailVerifiedMask
|
|
);
|
|
response.isMerchant = data.isMerchant;
|
|
response.isVATRegistered = data.isVATRegistered;
|
|
response.displayName = data.displayName;
|
|
|
|
if (config.EULAVersion !== client.EULAVersionAccepted) {
|
|
response.newEULA = config.EULAVersion;
|
|
}
|
|
|
|
response.featureFlags = client.FeatureFlags;
|
|
|
|
return Q.resolve(response);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialises a session for the recovery process.
|
|
*
|
|
* @param {Object} req - the express request object (for session and swagger)
|
|
* @param {Object} client - the client object to build the session for
|
|
*
|
|
* @returns {Promise} - a promise for the completion of this function
|
|
*/
|
|
function initRecoverySession(req, client) {
|
|
debug('Initialising recoverySession');
|
|
|
|
const email = client.ClientName;
|
|
const data = {
|
|
clientID: client.ClientID,
|
|
email,
|
|
level: apiSecurity.SESSION_TYPES.RECOVERY
|
|
};
|
|
|
|
return initSessionBase(req, email, data);
|
|
}
|
|
|
|
/**
|
|
* Encodes the password to the hash that is stored in the database. This is
|
|
* a 2 step process for the web-api:
|
|
* 1: run a sha-256 hash on the password (to match what the apps do internally
|
|
* 2: use the hashUtils to generate the full hash of this sha-256 hash
|
|
*
|
|
* @param {string} password - the password to hash
|
|
*
|
|
* @returns {promise} - a promise that resolves the hashed value and salt
|
|
*/
|
|
function encodePassword(password) {
|
|
//
|
|
// Hash the password, to match what the mobile clients do internally.
|
|
// Note, the hashing is async, so we use a promises.
|
|
//
|
|
const deferred = Q.defer();
|
|
const promise = deferred.promise;
|
|
|
|
const hasher = crypto.createHash('sha256');
|
|
hasher.setEncoding('hex');
|
|
hasher.end(password, 'utf8');
|
|
|
|
hasher.on('readable', () => {
|
|
const passwordHash = hasher.read();
|
|
deferred.resolve(passwordHash);
|
|
});
|
|
|
|
//
|
|
// Next we need to pass the password to the hashing code to get it in the
|
|
// latest format
|
|
//
|
|
return promise.then((hashedPassword) => {
|
|
return hashUtil.generateHash(Number(config.passwordCryptoVersion), hashedPassword);
|
|
});
|
|
}
|
|
|
|
//
|
|
// Dynamically adds the remoteAddress and protocolPort to the req
|
|
//
|
|
// @param {Object} req - request object, with lots of important information
|
|
// @param {Object} def - definition that we are currently being called for
|
|
//
|
|
// @param {Object} connectionData - connection data that needs to be logged
|
|
//
|
|
function addPortAndAddress(req) {
|
|
/**
|
|
* Different firewall headers depending on the source of the data.
|
|
* To get in to this code the services have been called from a trusted proxy.
|
|
* Technically the protocolPort should always be 'HTTPS:443' if the code has
|
|
* reached here, but it is taken from the headers if available for verification.
|
|
*/
|
|
let remoteAddress;
|
|
let protocolPort;
|
|
switch (global.CURRENT_DEPLOYMENT_ENV) {
|
|
case 'Azure':
|
|
remoteAddress = req.ip.split(':')[0];
|
|
protocolPort = req.protocol + ':' + req.headers['x-forwarded-port'];
|
|
break;
|
|
case 'Bluemix':
|
|
remoteAddress = req.headers.$wsra;
|
|
protocolPort = req.headers.$wssc + ':' + req.headers.$wssp;
|
|
break;
|
|
case 'Flexiion':
|
|
default:
|
|
remoteAddress = req.ip;
|
|
protocolPort = req.protocol + ':443';
|
|
}
|
|
const connectionData = {};
|
|
connectionData.remoteAddress = remoteAddress;
|
|
connectionData.protocolPort = protocolPort;
|
|
|
|
return connectionData;
|
|
}
|
|
|