bridge-node-server/node_server/utils/client/client.js

446 lines
15 KiB
JavaScript
Raw Normal View History

2018-06-24 20:15:03 +00:00
/**
* Support utilities for the clients
*/
'use strict';
const _ = require('lodash');
const Q = require('q');
const debug = require('debug')('utils:client');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var config = require(global.configFile);
var references = require(global.pathPrefix + '../utils/references.js');
var diligence = require(global.pathPrefix + '../utils/diligence/diligence.js');
var adminNotifier = require(global.pathPrefix + '../utils/adminNotifier.js');
const SETKYC_ERRORS = {
INVALID_PARAMETERS: 'BRIDGE: Invalid parameters to setKyc',
DOB_MISMATCH: 'BRIDGE: Date of birth doesnt match in setKyc',
UPDATE_FAILED: 'BRIDGE: Failed to update database in setKyc'
};
const SETKYC_RESPONSES = {
OK: 'BRIDGE: KYC complete',
WARNING_REFER: 'BRIDGE: Additional information required to verify identity',
WARNING_INTERNAL_CHECKS: 'BRIDGE: Additional internal checks required to verify identity'
};
module.exports = {
Client: Client,
generateEmailToken: generateEmailToken,
getCustomerInfo: getCustomerInfo,
setKyc: setKyc,
getDevicesInfo: getDevicesInfo,
SETKYC_ERRORS: SETKYC_ERRORS,
SETKYC_RESPONSES: SETKYC_RESPONSES
};
/**
* Constructs a new client with appropriate default parameters.
* Note that this does not validate parameters - it is expected that the
* caller will have done all necessary validation.
*
* @class
* @param {String} email - email address
* @param {String} passwordHash - the users password, after hashing (as hex)
* @param {String} passwordSalt - the salt used in the hash (as hex)
* @param {String} operator - The account operator
*/
function Client(email, passwordHash, passwordSalt, operator) {
//
// Initialize a blank object
//
Object.assign(this, mainDB.blankClient());
//
// Update that object with the parameters passed in
//
this.ClientName = email;
this.KYC[0].ContactEmail = email;
this.Password = passwordHash;
this.ClientSalt = passwordSalt;
this.OperatorName = operator;
//
// Get the base date that expiry values are based on
//
var baseDate = new Date();
//
// Set up tokens for email validation
// This token will be valid for 7 days
//
var emailToken = utils.randomCode(utils.fullAlphaNumeric, utils.tokenLength);
var emailTokenExpiry = new Date(baseDate.getTime());
emailTokenExpiry.setDate(emailTokenExpiry.getDate() + 7);
var token = generateEmailToken();
this.EMailValidationToken = token.token;
this.EMailValidationTokenExpiry = token.expiry;
//
// Initialize password expiry - 1 year expiry
//
var passwordExpiry = new Date(baseDate.getTime());
passwordExpiry.setDate(passwordExpiry.getDate() + 365);
this.PasswordManagement[0].PasswordExpiry = passwordExpiry;
this.PasswordManagement[0].PasswordLastReset = baseDate;
}
/**
* Generates an email confirmation token and expiry date
*
* @returns {Object} - Returns an object with a token and an epiry
*/
function generateEmailToken() {
var emailToken = utils.randomCode(utils.fullAlphaNumeric, utils.tokenLength);
var emailTokenExpiry = new Date();
emailTokenExpiry.setDate(emailTokenExpiry.getDate() + 7);
return {
token: emailToken,
expiry: emailTokenExpiry
};
}
/**
* Returns the appropriate info from the client to use as the customer in
* a transaction (or similar). The parmeters it returns are CustomerDisplayName,
* CustomerSubDisplayName, CustomerVATNo and CustomerSelfie
*
* @param {string} imageType - The type of image the user is using
* @param {object} client - The client object to get the info from
*
* @returns {Object | null} - the customer details (as above), or null on error
*/
function getCustomerInfo(imageType, client) {
//
// Initialise defaults for items that aren't appropriate
//
var result = {
CustomerDisplayName: '',
CustomerSubDisplayName: '',
CustomerImage: '',
CustomerVATNo: ''
};
//
// Get the correct details depending on the customer's defined image
//
switch (imageType) {
case 'Selfie':
result.CustomerDisplayName = client.DisplayName;
result.CustomerImage = client.Selfie;
break;
case 'defaultSelfie':
result.CustomerDisplayName = client.DisplayName;
result.CustomerImage = config.defaultSelfie;
break;
case 'CompanyLogo0':
result.CustomerDisplayName = client.Merchant[0].CompanyAlias;
result.CustomerSubDisplayName = client.Merchant[0].CompanySubName;
result.CustomerImage = client.Merchant[0].CompanyLogo;
if (client.Merchant[0].VATNo) {
result.CustomerVATNo = client.Merchant[0].VATNo;
}
break;
case 'defaultCompanyLogo0':
result.CustomerDisplayName = client.Merchant[0].CompanyAlias;
result.CustomerSubDisplayName = client.Merchant[0].CompanySubName;
result.CustomerImage = config.defaultCompanyLogo0;
if (client.Merchant[0].VATNo) {
result.CustomerVATNo = client.Merchant[0].VATNo;
}
break;
default:
// Something unknown so return null
return null;
}
return result;
}
/**
* Updates the KYC information for a client, as well as attempting automatic
* identitiy verification from the given information.
*
* @param {Object} client - the client object from the database to update
* @param {Object} updates - the new information to update from.
*
* @returns {Promise} - Promise for the success or otherwise of the update
* Returned values from SETKYC_RESPONSES on success,
* or SETKYC_ERRORS, diligence.ERRORS or references.ERRORS
* on error.
*/
function setKyc(client, updates) {
/**
* Check we've got all the required parameters
*/
if (!validateSetKycParams(client, updates)) {
return Q.reject(SETKYC_ERRORS.INVALID_PARAMETERS);
}
/**
* Validate the client sent the correct DOB unless:
* - they haven't previously set a date of birth OR
* - they are currently in the REFER status of ID verification (i.e. they
* likely got something wrong, which could be DOB)
*/
let kyc = client.KYC[0];
if (
kyc.DateOfBirth !== '' &&
kyc.DateOfBirth !== updates.DateOfBirth &&
!utils.bitsAllSet(client.ClientStatus, utils.ClientRefer)
) {
return Q.reject(SETKYC_ERRORS.DOB_MISMATCH);
}
/**
* All ok, so update with the values from the request.
* We do this manually rather than in a database update because we want
* to verifiy the details before we commit them to the DB.
*/
kyc.Title = updates.Title;
kyc.FirstName = updates.FirstName;
kyc.LastName = updates.LastName;
kyc.DateOfBirth = updates.DateOfBirth;
kyc.ResidentialAddressID = updates.ResidentialAddressID;
kyc.Gender = updates.Gender;
if (updates.hasOwnProperty('MiddleNames')) {
// Set the middlename. Note: convert null into ''
kyc.MiddleNames = updates.MiddleNames || '';
}
/**
* Get the residential address
*/
let addressP = references.isValidAddressRef(
client.ClientID,
updates.ResidentialAddressID,
'client.setKYC'
);
//
// Verify the person's identity with the newly updated data
//
var diligenceP = addressP.then((address) => {
return diligence.verifyIdentity(client, address);
});
//
// Update the record once we have verified the provided identity details
//
var updateP = diligenceP.then((diligenceResult) => {
//
// Build the query. The limits are:
// - Current user must be the owner (for security, to protect
// against Insecure Direct Object References).
// - DateOfBirth must match (or be unspeficied in the database, or the
// client's identity has not been verified)
//
var query = {
ClientID: client.ClientID,
$or: [
{'KYC.0.DateOfBirth': updates.DateOfBirth},
{'KYC.0.DateOfBirth': ''},
{ClientStatus: {$bitsAllSet: utils.ClientRefer}}
]
};
//
// Make sure the diligence result has the required defaults
//
_.defaults(
diligenceResult,
{
SmartScore: 998,
ID: '',
IKey: '',
ProfileURL: ''
}
);
//
// Build the update. This is slightly involved because the KYC is
// an array of subdocuments. We also build a new DisplayName from the
// FirstName + LastName.
//
var newValues = {
$inc: {
LastVersion: 1
},
$set: {
LastUpdate: new Date(),
DisplayName: updates.FirstName + ' ' + updates.LastName,
'KYC.0.Title': updates.Title,
'KYC.0.FirstName': updates.FirstName,
'KYC.0.LastName': updates.LastName,
'KYC.0.DateOfBirth': updates.DateOfBirth,
'KYC.0.ResidentialAddressID': updates.ResidentialAddressID,
'KYC.0.Gender': updates.Gender,
'KYC.0.Smartscore': diligenceResult.Smartscore,
'KYC.0.ID': diligenceResult.ID,
'KYC.0.IKey': diligenceResult.IKey,
'KYC.0.ProfileURL': diligenceResult.ProfileURL
}
};
if (updates.hasOwnProperty('MiddleNames')) {
// Set the middlename. Note: convert null into ''
newValues.$set['KYC.0.MiddleNames'] = updates.MiddleNames || '';
}
//
// Work out the status bits we need to update in the client
//
let status = utils.ClientDetailsMask;
let response = SETKYC_RESPONSES.OK;
if (_.isArray(diligenceResult.Warnings)) {
for (let i = 0; i < diligenceResult.Warnings.length; ++i) {
// Don't report errors with bitwise operations
// jshint -W016
switch (diligenceResult.Warnings[i]) {
case diligence.WARNINGS.REFER:
status |= utils.ClientRefer;
response = SETKYC_RESPONSES.WARNING_REFER;
break;
case diligence.WARNINGS.PEPS:
status |= utils.ClientPeps;
break;
case diligence.WARNINGS.SANCTIONS:
status |= utils.ClientSanctions;
response = SETKYC_RESPONSES.WARNING_INTERNAL_CHECKS;
break;
}
}
}
newValues.$bit = {
ClientStatus: {
or: status
}
};
//
// Build the options
//
var options = {
returnOriginal: false, // Need the updated document
upsert: false // Don't upsert if not found
};
//
// Make the request
//
return Q.ninvoke(
mainDB.collectionClient,
'findOneAndUpdate',
query,
newValues,
options
).then((result) => {
if (!result.ok || !result.value) {
//
// Nothing found - most likely some mistmatch in the search
//
return Q.reject(SETKYC_ERRORS.UPDATE_FAILED);
} else {
//
// Success (or success with warning).
// If the status is not a clean pass, then notify the admin
//
if (status !== utils.ClientDetailsMask) {
adminNotifier.notifyIdentityCheckIssue(result.value);
}
return Q.resolve(response);
}
});
});
/**
* Return the result of the final promise, assuming they all pass
*/
return Q.all([addressP, diligenceP, updateP]).then((responses) => responses[2]);
}
/**
* Validates that the parameters passed in are sufficient to update the KYC
*
* @param {Object} client - the client object to be updated
* @param {Object} updates - the update values
* @return {boolean} - true if the values are valid, false otherwise
*/
function validateSetKycParams(client, updates) {
const required = [
'Title', 'FirstName', 'LastName', 'DateOfBirth', 'Gender', 'ResidentialAddressID'
];
for (let i = 0; i < required.length; ++i) {
if (!_.isString(updates[required[i]])) {
return false;
}
}
if (!_.isObject(client) || !_.isArray(client.KYC) || client.KYC.length <= 0) {
return false;
}
return true;
}
/**
* Gets information about the devices a client has. This function returns a
* promise for an object with two values:
* - hasDevices: does the client have any devices (in any state)
* - hasActiveDevice: does the client have at least one device that is fully active
* i.e. fully registered, not disabled, not barred, etc
*
* @param {string} clientID - the client ID we are interested in
* @returns {Promise<object>} - promise for the status info
*/
function getDevicesInfo(clientID) {
const query = {
ClientID: clientID
};
const projection = {
_id: 0,
DeviceStatus: 1 // Only need device status
};
let result = {
hasDevices: false,
hasActiveDevice: false
};
debug('Getting device info');
//
// Create an async/generator function to simplify looping over the results of find.
// This also lets us end early, and not force loading everything into an array
//
return Q.async(function*() {
let cursor = mainDB.collectionDevice.find(query, projection);
while (yield cursor.hasNext()) {
debug(' -- hasNext');
result.hasDevices = true; // We have at least one device
let device = yield cursor.next();
debug(' -- next:', device);
let status = device.DeviceStatus;
if (
utils.bitsAllSet(status, utils.DeviceFullyRegistered) &&
!utils.bitsAllSet(status, utils.DeviceSuspendedMask) &&
!utils.bitsAllSet(status, utils.DeviceBarredMask)
) {
result.hasActiveDevice = true;
debug(' -- found active device:');
break;
}
}
debug(' - returning result');
return result;
})();
}