1878 lines
60 KiB
JavaScript
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'
|
|
);
|
|
}
|
|
}
|