/** * 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} - 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; })(); }