Martin Donnelly 57bd6c8e6a init
2018-06-24 21:15:03 +01:00

1878 lines
60 KiB
JavaScript

/**
* Controller to manage the users functions
*/
'use strict';
var _ = require('lodash');
var Q = require('q');
var templates = require(global.pathPrefix + '../utils/templates.js');
var httpStatus = require('http-status-codes');
var mongodb = require('mongodb');
var utils = require(global.pathPrefix + 'utils.js');
var debug = require('debug')('webconsole-api:controllers:users');
var Client = require(global.pathPrefix + '../utils/client/client.js').Client;
var clientUtils = require(global.pathPrefix + '../utils/client/client.js');
var promiseUtil = require(global.pathPrefix + '../utils/promises.js');
var hashUtil = require(global.pathPrefix + '../utils/hashing.js');
var credentialsUtil = require(global.pathPrefix + '../utils/credentials.js');
var swaggerUtils = require(global.pathPrefix + '../utils/swaggerUtils.js');
var anon = require(global.pathPrefix + '../utils/anon.js');
var references = require(global.pathPrefix + '../utils/references.js');
var responsesUtils = require(global.pathPrefix + '../utils/responses.js');
var diligence = require(global.pathPrefix + '../utils/diligence/diligence.js');
const featureFlags = require(global.pathPrefix + '../utils/feature-flags/feature-flags.js');
const apiSecurity = require('../api_security.js');
var apiUtil = require('../api_utils.js');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var mailer = require(global.pathPrefix + 'mailer.js');
var config = require(global.configFile);
module.exports = {
createUser: createUser,
confirmEmail: confirmEmail,
completeRegistration: completeRegistration,
denyEmail: denyEmail,
resendConfirmEmail: resendConfirmEmail,
changeEmail: changeEmail,
revertChangedEmail: revertChangedEmail,
changePassword: changePassword,
getUser: getUser,
getKYC: getKYC,
updateKYC: updateKYC,
getMerchant: getMerchant,
updateMerchant: updateMerchant
};
const VAT_FLAG = 'vat';
/**
* Function to create a user.
* We check that the user doesn't already exist, then add them to the
*
* Note: The controller is called after the validator middleware so we don't
* need to validate the format of the parameters.
*
* @param {Object} req - Express request object, with additional information
* from Swagger. Particularly useful is `req.swagger`
* which contains information on this specific request.
* @param {Object} res - Express response object
*/
function createUser(req, res) {
debug('api/controllers/users/createUser called:');
//
// Get the values from the request
//
var email = req.swagger.params.body.value.email;
var password = req.swagger.params.body.value.password;
var operator = req.swagger.params.body.value.operator;
//
// Encode the password
//
var encodeP = encodePassword(password);
//
// Promise chain for the asynchronous processing of the rest of the steps.
// Errors are handled by adding the requested response to the err then
// using it as the value of the rejected promise. Later error handlers
// check for the existence of that field, and don't change the error if
// a previous error exists.
//
//
// Wait for the password to be hashed, then add the user to the client db.
// Note that we bind in most of the parameters as the promise only
// provides the hashed password from above.
//
encodeP.then(addToDb.bind(undefined, email, operator))
//
// Wait for the insert to be tried. If it worked send the welcome email,
// else report the error.
//
.then(sendWelcomeEmail.bind(undefined, 'webconsole:createUser'), failedAddUser)
//
// Get the result of the email sending. If it worked then report success,
// else report the error
//
.then(returnSuccess.bind(undefined, res), failedSendEmail)
//
// Catch any unknown/unexpected errors
//
.catch(promiseUtil.sendErrorResponse.bind(undefined, res))
//
// Always have to end on done to ensure anything else is caught
//
.done();
}
/**
* Attempts to confirm the users email address by validating the token
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function confirmEmail(req, res) {
var token = req.swagger.params.body.value.emailValidationToken;
var clientId = req.session.data.client;
//
// Query to check the token matches the given one, and the token hasn't
// expired.
//
var confirmValidQuery = {
_id: mongodb.ObjectID(clientId),
EMailValidationToken: token,
EMailValidationTokenExpiry: {$gt: new Date()}
};
//
// If this were to be found, define the updates to set the client's email
// as validated
//
var validateUpdates = {
$set: {
EMailValidationToken: '', // Token cleared
EMailValidationTokenExpiry: '', // No expiry either
LastUpdate: new Date() // Last updated now
},
$bit: {
ClientStatus: {or: utils.ClientEmailVerifiedMask} // Set the flag
},
$inc: {
LastVersion: 1 // Increment the document version
}
};
var validateOptions = {
upsert: false,
multi: false
};
//
// Get the database to query for a record with a matching client id, that
// also matches the unexpired email validation token. If matched, the
// database will update the record to confirm it is validated.
//
var validatePromise = Q.nfcall(
mainDB.updateObject,
mainDB.collectionClient,
confirmValidQuery, // Look for a matching record
validateUpdates, // ...and update it to these values
validateOptions, // don't upsert
false
);
//
// Check the results and return success or failure as appropriate
//
validatePromise
.then(function success(result) {
if (result.result.n === 0) {
//
// No documents matched the criteria.
// This means that one of the following is true:
// - No client with the given id
// - unlikely because we got it from the current session
// - Email token doesn't match
// - user typo, or email already confirmed so no token
// - most likely
// - Email token has expired
// - unlikely, but possible
//
res.status(httpStatus.BAD_REQUEST).json({
code: 38,
info: 'Invalid or expired email validation token'
});
} else {
res.status(httpStatus.OK).json();
}
})
.catch(function fail(error) {
//
// Running the query failed (i.e. it didn't run because of a
// network error etc, NOT that it ran and found nothing.
//
debug('-- error validating email: ', error);
if (
error &&
error.hasOwnProperty('name') &&
error.name === 'MongoError'
) {
//
// Mongo Error
//
res.status(httpStatus.BAD_GATEWAY).json({
code: 40,
info: 'Database Unavailable'
});
} else {
//
// Unknown error
//
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
code: -1,
info: 'Unspecified error'
});
}
})
.done(); // End the promise chain
}
/**
* Completes a partial registration that was initiated through the integrations
* API. This requires the email token, and the client must not have a password
* specified already.
*
* @param {Object} req - express request object
* @param {Object} res - express response object
*/
function completeRegistration(req, res) {
const token = req.swagger.params.body.value.emailValidationToken;
const email = req.swagger.params.body.value.email;
const password = req.swagger.params.body.value.password;
//
// Need to encode the password for saving in the db
//
let encodeP = encodePassword(password);
//
// Find the Client of interest and add the hashed password to them
//
const CLIENT_NOT_FOUND_OR_TOKEN_INVALID = 'BRIDGE: Client not found or token invalid';
let completeP = encodeP.then((pwInfo) => {
//
// To complete a registration, the current client needs to:
// - Exist, based on the email address passed in
// - Have a matching registration token
// - Not have the token be expired
// - Not already have a password
//
const confirmValidQuery = {
ClientName: email,
EMailValidationToken: token,
EMailValidationTokenExpiry: {
$gt: new Date()
},
Password: '',
ClientSalt: ''
};
//
// If this were to be found, define the updates to set the client's email
// as validated
//
const validateUpdates = {
$set: {
EMailValidationToken: '', // Token cleared
EMailValidationTokenExpiry: '', // No expiry either
Password: pwInfo.hash, // Password added
ClientSalt: pwInfo.salt // Password salt added
},
$bit: {
ClientStatus: {
or: utils.ClientEmailVerifiedMask // Set the email verified flag
}
},
$inc: {
LastVersion: 1 // Increment the document version
},
$currentDate: {
LastUpdate: true
}
};
const validateOptions = {
upsert: false,
multi: false
};
//
// Get the database to query for a record with a matching client id, that
// also matches the unexpired email validation token. If matched, the
// database will update the record to confirm it is validated.
//
return Q.nfcall(
mainDB.updateObject,
mainDB.collectionClient,
confirmValidQuery, // Look for a matching record
validateUpdates, // ...and update it to these values
validateOptions, // don't upsert
false
).then((updateResult) => {
if (updateResult.result.n === 0) {
//
// No documents matched the criteria.
// This means that one of the following is true:
// - No client with the given id
// - unlikely because we got it from the current session
// - Email token doesn't match
// - user typo, or email already confirmed so no token
// - most likely
// - Email token has expired
// - unlikely, but possible
// - Password was previously set but the token wasn't cleared
// - very unlikely
//
return Q.reject(CLIENT_NOT_FOUND_OR_TOKEN_INVALID);
} else {
//
// Success
//
return Q.resolve();
}
});
});
//
// Check the results and return success or failure as appropriate
//
Q.all([encodeP, completeP])
.then(() => {
// All good, so send success
res.status(httpStatus.OK).json();
})
.catch((error) => {
debug(' - error updating KYC', error);
const responses = [
[
'MongoError',
httpStatus.BAD_GATEWAY, 999, 'Database Offline', true
],
[
CLIENT_NOT_FOUND_OR_TOKEN_INVALID,
httpStatus.BAD_REQUEST, 999, 'Client not found or invalid token'
],
[
hashUtil.ERRORS.UNKNOWN_ALGO,
httpStatus.INTERNAL_SERVER_ERROR, 999, 'Error encoding passsword'
],
[
hashUtil.ERRORS.HASH_FAILED,
httpStatus.INTERNAL_SERVER_ERROR, 999, 'Error encoding passsword'
],
[
hashUtil.ERRORS.SALT_FAILED,
httpStatus.INTERNAL_SERVER_ERROR, 999, 'Error encoding passsword'
]
];
const responseHandler = new responsesUtils.ErrorResponses(responses);
responseHandler.respond(res, error);
});
}
/**
* Called when the client denies that they have signed up for Bridge. It copies
* the client information to the ClientArchive and then deletes it from Client.
* It then does the same with any deviecs associated with that email.
* NOTE: this is only allowed if they haven't confirmed their email previously.
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function denyEmail(req, res) {
var email = req.swagger.params.body.value.email;
//
// This is a 3 step process:
// Step 1: Find the appropriate client
// Note: they must not have verified their email previously and have
// never logged in via a moble device
//
var query = {
ClientName: email,
ClientStatus: {
$bitsAllClear: utils.ClientEmailVerifiedMask
},
FirstLogin: 1
};
const NOT_FOUND = 'BRIDGE: NOT FOUND';
var findPromise = Q.nfcall(
mainDB.findOneObject,
mainDB.collectionClient,
query,
undefined,
false
).then(function(client) {
if (!client) {
return Q.reject({name: NOT_FOUND});
}
return client;
});
//
// Step 2: Copy the client to the archive
//
var copyPromise = findPromise.then(function(client) {
// Copy the old _id to OldClientID: be aware that ClientID is used for something
// else and should not be overwritten.
client.OldClientID = client._id.toString();
delete client._id;
//
// Remove the Password entirely for future security reasons.
//
client.Password = '';
client.ClientSalt = '';
// Update the LastUpdate
client.LastUpdate = new Date();
return Q.nfcall(
mainDB.addObject,
mainDB.collectionClientArchive,
client,
undefined,
false
);
});
//
// Step 3: delete from the client collection
//
var deletePromise = copyPromise.then(function() {
// We use the same query as before in case there was a race
// condition and the client changed (e.g. confirmed email) in the
// middle of this
return Q.nfcall(
mainDB.removeObject,
mainDB.collectionClient,
query,
undefined,
false);
});
//
// Step 4: find the first device belonging to the client.
// note: you can't add multiple devices until you login (which blocks delete)
// so "first" should be synonymous with "only".
//
var findDeviceP = findPromise.then(function(client) {
let deviceQ = {
ClientID: client.ClientID
};
return Q.nfcall(
mainDB.findOneObject,
mainDB.collectionDevice,
deviceQ,
undefined,
false
);
});
//
// Step 5: archive the device
// note: we wait until the client has been archived before we start
// so we don't risk the client being left, but no devices to
// login with.
//
var archiveDeviceP = Q.all([findDeviceP, deletePromise]).spread(function(device) {
if (!device) {
// Nothing to archive
return Q.resolve();
}
let archiveDevice = _.clone(device);
archiveDevice.DeviceIndex = archiveDevice._id.toString();
delete archiveDevice._id;
archiveDevice.DeviceAuthorisation = '';
archiveDevice.DeviceSalt = '';
archiveDevice.PendingHMAC = '';
archiveDevice.CurrentHMAC = '';
archiveDevice.LastUpdate = new Date();
return Q.nfcall(
mainDB.addObject,
mainDB.collectionDeviceArchive,
archiveDevice,
undefined,
false
);
});
//
// Step 6: delete the device now that it is archived
//
var deleteDeviceP = Q.all([findDeviceP, archiveDeviceP]).spread(function(device) {
if (!device) {
// Nothing to delete
return Q.resolve();
}
let removeQ = {
_id: device._id
};
return Q.nfcall(
mainDB.removeObject,
mainDB.collectionDevice,
removeQ,
undefined,
false
);
});
//
// Run all the steps and check they pass
//
Q.all([findPromise, copyPromise, deletePromise, findDeviceP, archiveDeviceP, deleteDeviceP])
.then(function() {
// All good
res.status(200).json();
})
.catch(function(error) {
debug('-- error denying email: ', error);
if (
error &&
error.hasOwnProperty('name')
) {
switch (error.name) {
case NOT_FOUND:
// No account with that name was found
res.status(httpStatus.NOT_FOUND).json({
code: 58,
info: 'Email address already confirmed, or not found'
});
break;
case 'MongoError':
// Mongo Error
res.status(httpStatus.BAD_GATEWAY).json({
code: 53,
info: 'Database Unavailable'
});
break;
default:
//
// Unknown error
//
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
code: -1,
info: 'Unexpected error'
});
break;
}
} else {
//
// Unknown error
//
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
code: -1,
info: 'Unexpected error'
});
}
})
.done(); // Catch all
}
/**
* Function to request resending of the email address confirmation email.
* This requires that the client is logged in so that we send it to the right
* person.
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function resendConfirmEmail(req, res) {
var email = req.session.data.email;
var id = req.session.data.client;
//
// This is a 3 step process
//
// Step 1: generate a new confirmation code
// This token will be valid for 7 days
//
var emailToken = utils.randomCode(utils.fullAlphaNumeric, utils.tokenLength);
var emailTokenExpiry = new Date();
emailTokenExpiry.setDate(emailTokenExpiry.getDate() + 7);
//
// Step 2: Run an update query to store the new token for the client
// NOTE: this deliberately replaces any existing token so that
// old emails can't be used to validate the account.
//
var query = {
_id: mongodb.ObjectID(id),
ClientStatus: {
$bitsAllClear: utils.ClientEmailVerifiedMask // Must not be verified already
}
};
var update = {
$set: {
EMailValidationToken: emailToken,
EMailValidationTokenExpiry: emailTokenExpiry,
LastUpdate: new Date()
},
$inc: {
LastVersion: 1
}
};
var options = {
upsert: false
};
var updatePromise = Q.nfcall(
mainDB.updateObject,
mainDB.collectionClient,
query,
update,
options,
false
);
//
// Step 3: Send the new email
//
const FAILED_UPDATE = 'BRIDGE: FAILED TO UPDATE TOKEN';
var sendPromise = updatePromise.then(function(results) {
if (results.result.n === 0) {
// Didn't find any accounts to update with the given id that
// aren't already verified
return Q.reject({name: FAILED_UPDATE});
} else {
//
// Call the send email function. It's expecting an array of
// 1 client (based on the other calls to it), so build that.
//
var client = {
ClientName: email,
EMailValidationToken: emailToken
};
return sendWelcomeEmail('webconsole:resendConfirmEmail', [client]);
}
});
//
// Run all the steps and return the results
//
Q.all([updatePromise, sendPromise])
.then(function() {
// Success
res.status(200).json();
})
.catch(function(error) {
debug('-- error adding address: ', error);
if (error && error.hasOwnProperty('name')) {
switch (error.name) {
case FAILED_UPDATE:
// Couldn't find the user in the database to update
// them, or they have already verified their account
res.status(httpStatus.BAD_REQUEST).json({
code: 87,
info: 'Account already verified (or not found)'
});
break;
case 'MongoError':
//
// Mongo Error
//
res.status(httpStatus.BAD_GATEWAY).json({
code: 85,
info: 'Database Unavailable'
});
break;
default:
//
// Unknown error
//
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
code: -1,
info: 'Unexpected error'
});
break;
}
} else {
//
// Unknown error
//
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
code: -1,
info: 'Unexpected error'
});
}
})
.done(); // Catch all
}
/**
* Function to request changing the password. The user must (a) be logged in,
* and (b) re-confirm their password (for security). Thus this only works for
* users who still know their current password
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function changePassword(req, res) {
debug('Changing password');
var email = req.session.data.email;
var currentpw = req.swagger.params.body.value.currentPassword;
var newpw = req.swagger.params.body.value.newPassword;
//
// Step 1. Validate the current pw
//
var validateP = credentialsUtil.validateRawPassword(email, currentpw);
//
// Step 2. encode the new password.
// This can run in parallel with validating the current password
// because we don't do anything with the result until later
//
var encodeP = encodePassword(newpw);
//
// Step 3. If the credentials were valid, and the password encoded
// then send an email to the user to tell them the password is
// being changed.
//
const CANT_SEND_EMAIL = 'Failed to send password change email';
var emailP = Q.all([validateP, encodeP]).then(function() {
debug(' - sending password changed email');
var htmlEmail = templates.render('password-changed-web');
var subject = 'Bridge Password Changed';
//
// Always send emails
//
var mode = 'Live';
return Q.nfcall(
mailer.sendEmail,
mode,
email,
subject,
htmlEmail,
'users/changePassword')
.catch(function(error) {
return Q.reject(CANT_SEND_EMAIL);
});
});
//
// Step 4. Update the password in the database
//
const BRIDGE_UPDATE_FAILED = 'Bridge: Update Failed';
var updateP = Q.all([encodeP, emailP]).then(function(results) {
debug(' - updating database');
var passwordInfo = results[0];
var query = {
ClientName: email
};
var update = {
$set: {
Password: passwordInfo.hash,
ClientSalt: passwordInfo.salt,
LastUpdate: new Date()
},
$inc: {
LastVersion: 1
}
};
var options = {
upsert: false,
returnOriginal: false
};
return Q.ninvoke(
mainDB.collectionClient,
'findOneAndUpdate',
query,
update,
options
).then(function(result) {
if (result.ok !== 1 || !result.value) {
return Q.reject(BRIDGE_UPDATE_FAILED);
} else {
return result.value;
}
});
});
//
// Step 5. Refresh the session so there can't be any accidental use
// of the old session
//
var sessionP = updateP.then(function(client) {
debug(' - refreshing session');
return apiUtil.initSession(req, client).then((response) => {
// Set the level to basic.
req.session.data.level = apiSecurity.SESSION_TYPES.BASIC;
return response;
});
});
//
// Step 6. Wait for all the promises then return the result
//
Q.all([validateP, encodeP, emailP, updateP, sessionP])
.then(function(results) {
debug(' - password update complete');
var response = results[4]; // The sessionP response
res.status(httpStatus.OK).json(response);
})
.catch(function(error) {
debug(' - error updating password', error);
//
// Handle the error appropriately
//
if (error.hasOwnProperty('name') && error.name === 'MongoError') {
// Mongo Error
res.status(httpStatus.BAD_GATEWAY).json({
code: 420,
info: 'Database offline'
});
return;
}
switch (error) {
//
// Credentials verification errors
//
case hashUtil.ERRORS.NO_MATCH:
case credentialsUtil.ERRORS.NOT_FOUND:
// These errors get a generic Login Failed error to avoid
// leaking information about why
res.status(httpStatus.UNAUTHORIZED).json({
code: 106,
info: 'Failed to validate current user and password.'
});
break;
case credentialsUtil.ERRORS.BARRED:
res.status(httpStatus.FORBIDDEN).json({
code: 117,
info: 'Client barred'
});
break;
case credentialsUtil.ERRORS.TOO_MANY_ATTEMPTS:
res.status(httpStatus.FORBIDDEN).json({
code: 410,
info: 'Too many failed password attempts'
});
break;
case credentialsUtil.ERRORS.CANT_UPDATE_ATTEMPTS_SUCCESS:
case credentialsUtil.ERRORS.CANT_UPDATE_ATTEMPTS_FAIL:
res.status(httpStatus.BAD_GATEWAY).json({
code: 401,
info: 'Database offline'
});
break;
case credentialsUtil.ERRORS.CANT_SEND_WARNING_EMAIL:
res.status(httpStatus.BAD_GATEWAY).json({
code: 409,
info: 'Unable to send e-mail.'
});
break;
// Password hash generation errors
case hashUtil.ERRORS.HASH_FAILED:
case hashUtil.ERRORS.UNKNOWN_ALGO:
case hashUtil.ERRORS.SALT_FAILED:
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
code: 419,
info: 'Error encrypting new password'
});
break;
// Cant send email to say password changed
case CANT_SEND_EMAIL:
res.status(httpStatus.BAD_GATEWAY).json({
code: 421,
info: 'Unable to send e-mail.'
});
break;
// Cant update the account (disappeared?)
case BRIDGE_UPDATE_FAILED:
res.status(httpStatus.BAD_REQUEST).json({
code: 106,
info: 'Account not found'
});
break;
// Failed to regenerate the session
case apiUtil.ERRORS.SESSION_REGEN_FAILED:
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
code: 30010,
info: 'Error regenerating session. User must login again.'
});
break;
// Other errors
default:
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
code: -1,
info: 'Unspecified error'
});
}
})
.done();
}
/**
* Function to cheange the email address for this user.
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function changeEmail(req, res) {
debug('Changing email');
const clientID = req.session.data.clientID;
const oldEmail = req.session.data.email;
const newEmail = req.swagger.params.body.value.email;
//
// Step 1. Get the current client and check there isn't already a
// change pending as this would allow a scammer to change the email
// multiple times to overwrite the revert address
//
const RECENTLY_CHANGED = 'BRIDGE: RECENTLY CHANGED';
let clientP = references.getClient(clientID).then(function(client) {
if (
client.PreviousEMailValidationTokenExpiry &&
client.PreviousEMailValidationTokenExpiry > new Date()) {
return Q.reject(RECENTLY_CHANGED);
}
return client;
});
//
// Step 2. Check that the new address is not already in use.
//
const IN_USE = 'BRIDGE: ADDRESS IN USE';
var query = {
ClientName: newEmail
};
var options = {
comment: 'webconsole:revertChangedEmail'
};
let emailNotInUseP = Q.nfcall(
mainDB.findOneObject,
mainDB.collectionClient,
query,
options,
false // Don't suppress errors
).then((client) => client ? Q.reject(IN_USE) : true); // Reject if another client exists
//
// Step 3. Generate a random ID for reverting the email change, and another
// for confirming the new email address.
//
const newEmailToken = clientUtils.generateEmailToken();
const revertEmailToken = clientUtils.generateEmailToken();
//
// Step 4. If the update is allowed, send an email to the old address.
// We want to make sure this at least sends ok, as this may be
// the only warning someone gets of an attempt to take over the account.
//
const CANT_SEND_EMAIL = 'Failed to send email change email';
let emailP = Q.all([clientP, emailNotInUseP]).then(function() {
debug(' - sending email changed email');
return mailer.sendEmailChangedEmails(
oldEmail,
newEmail,
revertEmailToken,
newEmailToken,
'', //Mode: always send
'webconsole.changeEmail'
).catch(function(error) {
return Q.reject(CANT_SEND_EMAIL);
});
});
//
// Step 5. Update the emails etc. in the Client object in the database
//
const BRIDGE_UPDATE_FAILED = 'Bridge: Update Failed';
let updateP = Q.all([clientP, emailP]).then(function(results) {
debug(' - updating database');
var query = {
ClientID: clientID
};
var update = {
$set: {
ClientName: newEmail,
EMailValidationToken: newEmailToken.token,
EMailValidationTokenExpiry: newEmailToken.expiry,
PreviousEmail: oldEmail,
PreviousEMailValidationToken: revertEmailToken.token,
PreviousEMailValidationTokenExpiry: revertEmailToken.expiry
},
$bit: {
// Clear the email verified flag so they have to verify the new email
// Note that we have to ignore JSHint's complaint about the use of xor (~).
ClientStatus: {and: ~utils.ClientEmailVerifiedMask} // jshint ignore:line
},
$inc: {
LastVersion: 1
},
$currentDate: {
LastUpdate: true
}
};
var options = {
upsert: false,
returnOriginal: false
};
return Q.ninvoke(
mainDB.collectionClient,
'findOneAndUpdate',
query,
update,
options
).then(function(result) {
if (result.ok !== 1 || !result.value) {
return Q.reject(BRIDGE_UPDATE_FAILED);
} else {
return result.value;
}
});
});
//
// Step 5. Refresh the sessions so there can't be any accidental use
// of the old session
//
var resetSessionsP = updateP.then((client) => resetSessions(req, client));
//
// Step 6. Wait for all the promises then return the result
// NOTE that we don't wait for the session reset promise because we
// can't really do anything if it fails, and the address has already
// been changed.
//
Q.all([clientP, emailNotInUseP, emailP, updateP])
.then(function(results) {
debug(' - email update complete');
res.status(httpStatus.OK).json();
})
.catch(function(error) {
debug(' - error updating email', error);
const responses = [
[
'MongoError',
httpStatus.BAD_GATEWAY, 30801, 'Database Offline', true
],
[
references.ERRORS.INVALID_CLIENT,
httpStatus.BAD_REQUEST, 30802, 'Client not found', true
],
[
IN_USE,
httpStatus.BAD_REQUEST, 30803, 'Email address already in use.'
],
[
RECENTLY_CHANGED,
httpStatus.CONFLICT, 30804, 'Email address change pending. Try again later.'
],
[
CANT_SEND_EMAIL,
httpStatus.BAD_GATEWAY, 30805, 'Unable to send e-mail.'
],
[
BRIDGE_UPDATE_FAILED,
httpStatus.BAD_REQUEST, 30806, 'Unable to change email address'
]
];
const responseHandler = new responsesUtils.ErrorResponses(responses);
responseHandler.respond(res, error);
})
.done();
}
/**
* Function to revert the email change.
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function revertChangedEmail(req, res) {
debug('Reverting Changed email');
const token = req.swagger.params.body.value.emailValidationToken;
//
// Step 1. Find a client where:
// 1. the token provided is the revert token,
// 2. the token hasn't expired yet.
//
const REVERT_NOT_FOUND = 'BRIDGE: NO REVERT';
var query = {
PreviousEMailValidationToken: token,
PreviousEMailValidationTokenExpiry: {
$gt: new Date()
}
};
var options = {
comment: 'webconsole:revertChangedEmail'
};
let clientP = Q.nfcall(
mainDB.findOneObject,
mainDB.collectionClient,
query,
options,
false // Don't suppress errors
).then((client) => client ? client : Q.reject(REVERT_NOT_FOUND));
//
// Step 2. If we find the client to be reverted, send an email to both address.
// We want to make sure this at least sends ok, as this may be
// the only warning someone gets of an attempt to take over the account.
//
const CANT_SEND_EMAIL = 'Failed to send email change email';
let emailP = clientP.then(function(client) {
debug(' - sending email changed email');
const revertingToEmail = client.PreviousEmail;
const revertingFromEmail = client.ClientName;
return mailer.sendEmailRevertedEmails(
revertingToEmail,
revertingFromEmail,
'', //Mode: always send
'webconsole.revertEmail'
).catch(function(error) {
return Q.reject(CANT_SEND_EMAIL);
});
});
//
// Step 3. Revert the email addresses etc. in the Client object in the database
// NOTE: we don't require re-validation of the revert email address as
// (a) we are reverting to a previously used email, and
// (b) being able to do the revert requires a token from that email address
//
const BRIDGE_UPDATE_FAILED = 'Bridge: Update Failed';
let updateP = Q.all([clientP, emailP]).then(function(results) {
debug(' - updating database');
const client = results[0];
var query = {
ClientID: client.ClientID
};
var update = {
$set: {
ClientName: client.PreviousEmail,
EMailValidationToken: '',
EMailValidationTokenExpiry: ''
},
$unset: {
PreviousEmail: '',
PreviousEMailValidationToken: '',
PreviousEMailValidationTokenExpiry: ''
},
$bit: {
// Set the email verified flag as the original email must have been verified
ClientStatus: {or: utils.ClientEmailVerifiedMask}
},
$inc: {
LastVersion: 1
},
$currentDate: {
LastUpdate: true
}
};
var options = {
upsert: false,
returnOriginal: false
};
return Q.ninvoke(
mainDB.collectionClient,
'findOneAndUpdate',
query,
update,
options
).then(function(result) {
if (result.ok !== 1 || !result.value) {
return Q.reject(BRIDGE_UPDATE_FAILED);
} else {
return result.value;
}
});
});
//
// Step 4. Destroy the session so there can't be any accidental use
// of the old session and they'll have to log in again.
//
var resetSessionsP = updateP.then((client) => resetSessions(req, client));
//
// Step 5. Wait for all the promises then return the result.
// Note: we dpn't wait for the session reset promise as there's
// nothing we can do if it fails.
//
Q.all([clientP, emailP, updateP])
.then(function(results) {
debug(' - email revert complete');
res.status(httpStatus.OK).json();
})
.catch(function(error) {
debug(' - error updating email', error);
const responses = [
[
'MongoError',
httpStatus.BAD_GATEWAY, 30901, 'Database Offline', true
],
[
REVERT_NOT_FOUND,
httpStatus.BAD_REQUEST, 30902, 'Invalid revert token'
],
[
CANT_SEND_EMAIL,
httpStatus.BAD_GATEWAY, 30903, 'Unable to send e-mail.'
],
[
BRIDGE_UPDATE_FAILED,
httpStatus.BAD_REQUEST, 30904, 'Unable to revert email addresses'
]
];
const responseHandler = new responsesUtils.ErrorResponses(responses);
responseHandler.respond(res, error);
})
.done();
}
/**
* Function to get the User information from the current user
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function getUser(req, res) {
//
// Get the current user's id from the session
//
var userId = req.session.data.client;
//
// Build the query. The limits are:
// - Current user must be the owner (for security, to protect
// against Insecure Direct Object References).
//
var query = {
_id: mongodb.ObjectID(userId)
};
//
// Define the projection based on the Swagger definition
//
var projection = swaggerUtils.swaggerToMongoProjection(
req.swagger.operation,
false // we don't want the _id
);
//
// Build the options to encapsulate the projection
//
var options = {
fields: projection,
comment: 'WebConsole:getUser' // For profiler logs use
};
//
// Make the request
//
mainDB.findOneObject(mainDB.collectionClient, query, options, false,
function(err, item) {
if (err) {
debug('- failed to get User', err);
res.status(httpStatus.BAD_GATEWAY).json({
code: 30201,
info: 'Database offline'
});
} else if (item === null) {
//
// Nothing found
//
res.status(httpStatus.NOT_FOUND).json({
code: 30202,
info: 'Not found'
});
} else {
//
// Null any nullable fields
//
swaggerUtils.getAndApplyNullableFields(req.swagger.operation, item);
res.status(httpStatus.OK).json(item);
}
});
}
/**
* Function to get the KYC information from the user
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function getKYC(req, res) {
//
// Get the current user's email from the session
//
var clientID = req.session.data.clientID;
//
// Build the query. The limits are:
// - Current user must be the owner (for security, to protect
// against Insecure Direct Object References).
//
var query = {
ClientID: clientID
};
//
// Define the projection based on the Swagger definition
//
var projection = swaggerUtils.swaggerToMongoProjection(
req.swagger.operation,
false, // we don't want the _id
'KYC' // Fields comes from the KYC subdocument
);
//
// Build the options to encapsulate the projection
//
var options = {
fields: projection,
comment: 'WebConsole:getKYC' // For profiler logs use
};
//
// Make the request
//
mainDB.findOneObject(mainDB.collectionClient, query, options, false,
function(err, item) {
if (err) {
debug('- failed to get KYC', err);
res.status(httpStatus.BAD_GATEWAY).json({
code: 30201,
info: 'Database offline'
});
} else if (item === null || !_.isArray(item.KYC)) {
//
// Nothing found
//
res.status(httpStatus.NOT_FOUND).json({
code: 30202,
info: 'Not found'
});
} else {
//
// The kyc information is an array of subdocuments. We only
// want the first one.
//
var kyc = item.KYC[0];
anon.anonymiseKYC(kyc);
//
// Set a default ResidentialAddressID if none in present
//
if (!kyc.hasOwnProperty('ResidentialAddressID')) {
kyc.ResidentialAddressID = null;
}
//
// Null any nullable fields
//
swaggerUtils.getAndApplyNullableFields(req.swagger.operation, kyc);
res.status(httpStatus.OK).json(kyc);
}
});
}
/**
* Update the KYC information from the user.
* For security reasons the date of birth must always match (unless there is
* no value for the date of birth).
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*
* @return {Promise} - promise for the result of the update
*/
function updateKYC(req, res) {
//
// Get the current user's email from the session
//
const clientID = req.session.data.clientID;
const updates = req.swagger.params.body.value;
//
// To allow the empty string to be returned for unset Gender, we need to
// allow `""` in the enum of valid values. However, we never want to let
// someone //set// the gender to "", so we prevent it here.
//
const INVALID_GENDER = 'Bridge: Invalid Gender';
let genderP = Q.resolve();
if (updates.Gender === '') {
genderP = Q.reject(INVALID_GENDER);
}
var clientP = genderP.then(() => references.getClient(clientID));
var setP = clientP.then((client) => {
return clientUtils.setKyc(client, updates);
});
return Q.all([genderP, clientP, setP])
.then((results) => {
const setResult = results[2];
//
// We may have warnings to respond with
//
const responses = [
[
clientUtils.SETKYC_RESPONSES.OK,
httpStatus.OK, 10059, 'KYC details updated.'
],
[
clientUtils.SETKYC_RESPONSES.WARNING_REFER,
httpStatus.OK, 10079, 'Additional information required.'
],
[
clientUtils.SETKYC_RESPONSES.WARNING_INTERNAL_CHECKS,
httpStatus.OK, 10080, 'Additional internal checks required.'
]
];
const responseHandler = new responsesUtils.ErrorResponses(responses);
responseHandler.respond(res, setResult);
})
.catch((error) => {
debug(' - error updating KYC', error);
const responses = [
[
'MongoError',
httpStatus.BAD_GATEWAY, 423, 'Database Offline', true
],
[
references.ERRORS.INVALID_ADDRESS,
httpStatus.BAD_REQUEST, 532, 'Invalid Address', true
],
[
references.ERRORS.INVALID_CLIENT,
httpStatus.UNAUTHORIZED, 534, 'Client Not Found', true
],
[
diligence.ERRORS.VERIFICATION_FAILED,
httpStatus.BAD_REQUEST, 533, 'Unable to verify id', true
],
[
clientUtils.SETKYC_ERRORS.DOB_MISMATCH,
httpStatus.NOT_FOUND, 426, 'Date of birth mismatch'
],
[
clientUtils.SETKYC_ERRORS.UPDATE_FAILED,
httpStatus.UNAUTHORIZED, 534, 'Client not found during update'
],
[
clientUtils.SETKYC_ERRORS.INVALID_PARAMETERS,
httpStatus.BAD_REQUEST, 535, 'Invalid paramters'
],
[
INVALID_GENDER,
httpStatus.BAD_REQUEST, 30203, 'Invalid Gender. Must not be an empty string.'
]
];
const responseHandler = new responsesUtils.ErrorResponses(responses);
responseHandler.respond(res, error);
})
.done();
}
/**
* Function to get the Merchant information from the user
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function getMerchant(req, res) {
//
// Get the current user's details from the session
//
var clientID = req.session.data.clientID;
//
// Build the query. The limits are:
// - Current user must be the owner (for security, to protect
// against Insecure Direct Object References).
//
var query = {
ClientID: clientID
};
//
// Define the projection based on the Swagger definition
//
var projection = swaggerUtils.swaggerToMongoProjection(
req.swagger.operation,
false, // we don't want the _id
'Merchant' // Fields comes from the Merchant subdocument
);
//
// Build the options to encapsulate the projection
//
var options = {
fields: projection,
comment: 'WebConsole:getMerchant' // For profiler logs use
};
//
// Make the request
//
mainDB.findOneObject(mainDB.collectionClient, query, options, false,
function(err, item) {
if (err) {
debug('- failed to get Merchant', err);
res.status(httpStatus.BAD_GATEWAY).json({
code: 30301,
info: 'Database offline'
});
} else if (item === null || !_.isArray(item.Merchant)) {
//
// Nothing found
//
res.status(httpStatus.NOT_FOUND).json({
code: 30302,
info: 'Not found'
});
} else {
//
// The merchant information is an array of subdocuments. We only
// want the first one.
//
var merchant = item.Merchant[0];
//
// Null any nullable fields
//
swaggerUtils.getAndApplyNullableFields(req.swagger.operation, merchant);
res.status(httpStatus.OK).json(merchant);
}
});
}
/**
* Update the Merchant information from the user.
* This is only available to clients who are already enabled as merchants
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function updateMerchant(req, res) {
//
// Check the client is a merchant
//
if (!req.session.data.isMerchant) {
res.status(httpStatus.FORBIDDEN).json({
code: 30303,
info: 'Client is not a merchant.'
});
return;
}
//
// Get the current user's details from the session
//
var clientID = req.session.data.clientID;
var updates = req.swagger.params.body.value;
//
// Check we have all the required fields (not as nulls)
//
var required = req.swagger.operation.parameters[0].schema.required;
for (var i = 0; i < required.length; ++i) {
if (!_.isString(updates[required[i]])) {
res.status(httpStatus.BAD_REQUEST).json({
code: 30304,
info: 'missing required field'
});
return;
}
}
//
// Check if we are allowed to set a VAT number, and fail if one has been sent
//
if (
updates.VATNo &&
!featureFlags.isEnabled(VAT_FLAG, req.session.data)
) {
res.status(httpStatus.BAD_REQUEST).json({
code: 30307,
info: 'Feature not enabled'
});
return;
}
//
// Build the query. The limits are:
// - Current user must be the owner (for security, to protect
// against Insecure Direct Object References).
//
const query = {
ClientID: clientID
};
//
// Build the update. This is slightly involved because the Merchant is
// an array of subdocuments, but we only want to deal with the first one.
//
var update = {
$inc: {LastVersion: 1},
$set: {
LastUpdate: new Date(),
// Required fields
'Merchant.0.CompanyName': updates.CompanyName,
'Merchant.0.CompanyAlias': updates.CompanyAlias,
// Optional fields so set null = '' (i.e. clear if not sent)
'Merchant.0.VATNo': updates.VATNo || '',
'Merchant.0.CompanySubName': updates.CompanySubName || ''
}
};
//
// Build the options
//
var options = {
upsert: false // Don't upsert if not found
};
//
// Make the request
//
mainDB.updateObject(mainDB.collectionClient, query, update, options, false,
function(err, results) {
if (err) {
debug('- failed to update Merchant', err);
res.status(httpStatus.BAD_GATEWAY).json({
code: 30305,
info: 'Database offline'
});
} else if (results.result.n === 0) {
//
// Nothing found - most likely a date of birth mismatch
//
res.status(httpStatus.NOT_FOUND).json({
code: 30306,
info: 'Client not found'
});
} else {
// All good
res.status(httpStatus.OK).json();
}
});
}
/**
* Revert web and device sessions to ensure all uses of the system have to
* log in again.
*
* @param {Object} req - the express request
* @param {Object} client - the current client object
* @returns {Promise} - a promise for the result of updating the session
*/
function resetSessions(req, client) {
var resetWebSessionP = Q.ninvoke(req.session, 'destroy');
const query = {
ClientID: client.ClientID
};
const update = {
$set: {
SessionToken: '',
SessionTokenExpiry: new Date(0), // Jan 1st 1970
CurrentHMAC: '',
PendingHMAC: utils.randomCode(utils.lowerCaseHex, (config.HMACBytes * 2))
}
};
const options = {
upsert: false
};
var resetDeviceSessionP = Q.ninvoke(
mainDB.collectionDevice,
'updateMany',
query,
update,
options
);
return Q.all([resetWebSessionP, resetDeviceSessionP]);
}
/**
* 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) {
return apiUtil.encodePassword(password)
.catch(function(error) {
debug('---- hashUtil failed: ', error);
//
// Turn any errors into the expected format.
// These are all internal server errors relating to being unable
// to generate hashes, salts, etc. (and are very unlikely)
//
return promiseUtil.returnChainedError(
error,
httpStatus.INTERNAL_SERVER_ERROR,
413,
'Encryption error'
);
});
}
/**
* Adds the user into the client database
*
* @param {String} email - The email address of the client
* @param {String} operator - The account operator (usually 'Comcarde')
* @param {String} passwordInfo - The password hash and salt
*
* @returns {Promise} - a promise for the result of adding to the db
*/
function addToDb(email, operator, passwordInfo) {
debug('- addToDb [%s] [%s]', email, operator);
//
// Create a default formatted client
//
var client = new Client(email, passwordInfo.hash, passwordInfo.salt, operator);
//
// And try and insert it
// - success means we created the user
// - failure means something went wrong (already in the db, db off-line,
// etc.)
//
// Return a promise that will take over from the one we have
//
return Q.ninvoke(
mainDB.collectionClient,
'insert',
client
).then(function(result) {
return result.ops;
});
}
/**
* Sends the welcome email to the client.
*
* @param {String} caller - The caller for logging purposes
* @param {Client[]} newClient - The newly added client. Takes an array for easier calling
*
* @returns {Promise} - A promise for the result of sending the email
*/
function sendWelcomeEmail(caller, newClient) {
var mode = ''; // Never disable
return mailer.sendWelcomeEmail(newClient[0], mode, caller);
}
/**
* Reports successful completion of adding a new client (i.e. 201 Created)
*
* @param {object} res - Express response object
*/
function returnSuccess(res) {
//
// All good
//
debug('- user added successfully');
res
.status(httpStatus.CREATED)
.type('application/json')
.end();
}
/**
* Reports a failure to add a user
*
* @param {Object} err - the error object
*
* @return {Promise} - a rejected promise with the error info
*/
function failedAddUser(err) {
debug(' - failed to add user');
if (promiseUtil.hasChainedError(err)) {
// Resend previous error
return promiseUtil.resendChainedError(err);
} else if (err &&
err.name &&
err.name === 'MongoError' &&
err.code &&
err.code === 11000
) {
// Error code 11000 is MongoDB can't insert duplicate
debug(' -- reason: Email address already in use');
return promiseUtil.returnChainedError(
err,
httpStatus.CONFLICT,
10,
'Email address already in use'
);
} else {
debug(' -- reason: ', err);
return promiseUtil.returnChainedError(
err,
httpStatus.SERVICE_UNAVAILABLE,
6,
'Database temporarily off line'
);
}
}
/**
* Returns the failure code to the caller. This failure code can come
* from a previous error handler, or we will add an 'unknown' error.
*
* @param {Object} err - the error object
*
* @return {Promise} - a rejected promise with the rejected error
*/
function failedSendEmail(err) {
if (promiseUtil.hasChainedError(err)) {
// Ignore previous error
return promiseUtil.resendChainedError(err);
} else {
//
// Couldn't send email
//
debug('- failed to send email: ', err);
return promiseUtil.returnChainedError(
err,
httpStatus.SERVICE_UNAVAILABLE,
11,
'Failed to send email'
);
}
}