This commit is contained in:
Martin Donnelly 2018-06-24 21:15:03 +01:00
commit 57bd6c8e6a
398 changed files with 84198 additions and 0 deletions

11
.arcconfig Normal file
View File

@ -0,0 +1,11 @@
{
"phabricator.uri" : "http://10.0.10.242",
"load": [
".arcanist-extensions/tap_test_engine"
],
"unit.engine": "TAPTestEngine",
"unit.engine.tap.command": "gulp --cwd node_server --reporter tap test",
"unit.engine.tap.eol": "\n"
}

18
.arclint Normal file
View File

@ -0,0 +1,18 @@
{
"linters": {
"eslint-regex-based": {
"type": "script-and-regex",
"include": "(\\.js?$)",
"exclude": [],
"script-and-regex.script": "node eslint-for-arc.js",
"script-and-regex.regex": "/^(?P<file>.*): line (?P<line>[0-9]*), col (?P<char>[0-9]*), ((?P<warning>Warning)|(?P<error>Error)) - (?P<message>.*) \\((?P<code>[a-z-\\/]+)\\)$/m"
},
"nsp-regex-based": {
"type": "script-and-regex",
"include": "(package.json$)",
"exclude": [],
"script-and-regex.script": "node nsp-for-arc.js",
"script-and-regex.regex": "/^ (?P<name>\\S* +\\S*) +(?P<original>\\S*) +(?P<other>(?>\\S*(?> > )?)*) +(?P<message>https:\\S*) *$/m"
}
}
}

100
.eslintrc.js Normal file
View File

@ -0,0 +1,100 @@
module.exports = {
"extends": [
"canonical",
"canonical/lodash",
"canonical/mocha"
],
"rules": {
// enable additional rules
//
// A reasonable number of these are because we are stuck on Node 4.
// Moving to later versions of node will allow things like await/async etc.
//
"arrow-body-style": 0,
"jsdoc/require-description-complete-sentence": 0,
"filenames/match-regex": 0,
"func-style": 0,
"id-length": [
1,
{
"exceptions": [
"i",
"j",
"Q",
"P",
"R",
"$",
"_"
],
"max": 50,
"min": 2
}
],
"id-match": [
2,
"(^[$A-Za-z]+(?:[A-Z][a-z]*)*\\d*$)|(^[A-Z]+(_[A-Z]+)*(_\\d$)*$)|(^(_|\\$)$)",
{
"onlyDeclarations": true,
"properties": false
}
],
"indent": [
2,
4,
{
"SwitchCase": 1
}
],
"import/no-commonjs": 0,
"import/no-dynamic-require": 0,
"import/unambiguous": 0,
"import/order": 0,
"linebreak-style": 0,
"lines-around-directive": 0,
"line-comment-position": 0,
"newline-after-var": 0,
"newline-before-return": 0,
"no-extra-parens": 0,
"no-inline-comments": 0,
"no-multi-spaces": [
2,
{
"ignoreEOLComments": true
}
],
"no-param-reassign": 0,
"no-trailing-spaces": [
2,
{
"skipBlankLines": false,
"ignoreComments": false
}
],
"no-use-before-define": [
2,
{
"functions": false
}
],
"object-shorthand": 1,
"promise/prefer-await-to-then": 0,
"promise/prefer-await-to-callbacks": 0,
"sort-keys": 0,
"space-before-function-paren": [
2,
{
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}
],
"strict": 0
},
"settings": {
"jsdoc": {
"additionalTagNames": {
"customTags": ["ngInject"]
}
}
}
};

19
.gitattributes vendored Normal file
View File

@ -0,0 +1,19 @@
# All other files are subjected to the usual algorithm to determine
# whether a file is a binary file or a text file, respecting
# "core.eol" for all files detected as text files.
# "core.autocrlf", if set, will force the conversion to/from CRLF
# automatically as necessary for text files.
* text=auto
# package.json (from NPM) is in lf not crlf
package.json eol=lf
package-lock.json eol=lf
# shell scripts should be LF
*.sh eol=lf
# jade/pug files are lf too
# see: https://github.com/jadejs/jade/issues/1683
*.jade eol=lf
*.pug eol=lf

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
.idea/
node_server/node_modules/
/node_server/portal/
/node_server/docs/
/node_server/temp/
tsconfig.json
devenvtemp.txt
testenvtemp.txt
prodenvtemp.txt
version.txt
/node_modules/
node_server/coverage/
node_server/email_templates/

4
.gitmodules vendored Normal file
View File

@ -0,0 +1,4 @@
[submodule ".arcanist-extensions"]
path = .arcanist-extensions
url = https://github.com/farrago/arcanist-extensions.git
branch = tap-line-endings

28
Dockerfile Normal file
View File

@ -0,0 +1,28 @@
FROM registry.eu-gb.bluemix.net/ibmnode:latest
## Add the node server directory.
ADD ./node_server /node_server
## Standard updates. Note to pin a version use: package-foo=1.3.* \
RUN apt-get update -q && apt-get install -y -q \
curl \
graphicsmagick \
&& apt-get clean -q \
&& rm -rf /var/lib/apt/lists/*
## Enhance default password rules.
RUN sed -i 's/^PASS_MIN_DAYS.*/PASS_MIN_DAYS 1/' /etc/login.defs
## Enhance default password rules.
RUN echo 'Europe/London' > /etc/timezone
RUN dpkg-reconfigure -f noninteractive tzdata
## Expose the appropriate ports.
EXPOSE 80
## Install the node modules.
WORKDIR /node_server
RUN npm install
## Execute the code.
CMD ["node", "node_server.js"]

76
bitbucket-pipelines.yml Normal file
View File

@ -0,0 +1,76 @@
# This is based on the sample build configuration for JavaScript.
# Check our guides at https://confluence.atlassian.com/x/14UWN for more examples.
# Only use spaces to indent your .yml configuration.
# -----
# You can specify a custom docker image from Docker Hub as your build environment.
image: node:8
pipelines:
default:
- step:
#
# Validate swagger definitions against the swagger 2.0 JSON schema spec
# NOTE: we have to download it manually as ajv-cli only supports local
# schemas at present.
#
name: "Swagger schema validation"
caches:
- node
script:
- npm install -g ajv-cli
- TEMPLATEFILE=`mktemp` || exit 1
- wget -q http://json.schemastore.org/swagger-2.0 -O $TEMPLATEFILE
- DEREFEDSWAGGERFILE=`mktemp` || exit 1
- npm install -g json-refs
- json-refs resolve node_server/swagger_api/api_swagger_def.json > $DEREFEDSWAGGERFILE
- ajv test -s $TEMPLATEFILE -d $DEREFEDSWAGGERFILE --valid --errors=json
- ajv test -s $TEMPLATEFILE -d node_server/integration_api/integration_swagger_def.json --valid --errors=json
- step:
#
# Run ESLint against all the JS files that were changed in this branch.
#
# The linting is run using a script as its a little too complex for
# standalone commands.
#
# Note that this will also run on master when changes are merged in, but
# as `HEAD` and `master` will be the same revision there will be no files
# in the list of changes and ESLint won't be run.
#
#
name: "ESLint"
caches:
- node
script:
# Install packages to get our expected version of ESLint and related configs
# pipeline runs as root, but npm doesn't like installing packages as
# root for security reasons. As this is just CI we allow it, using
# unsafe-perm to make npm accept it.
#
- npm install --unsafe-perm
# Run the tests
- chmod +x ./tools/bitbucket-pipeline-scripts/eslint-changes.sh
- ./tools/bitbucket-pipeline-scripts/eslint-changes.sh
- step:
#
# All unit tests are run every time in case a change has an unexpected
# effect on other areas.
#
name: "Unit tests"
caches:
- node
script:
- npm install -g gulp
#
# pipeline runs as root, but npm doesn't like installing packages as
# root for security reasons. As this is just CI we allow it, using
# unsafe-perm to make npm accept it.
#
- npm install --unsafe-perm
#
# As described in the docs, we need to use the mccha-junit-reporter
# to output results in a format pipelines understands, plus set an
# environment variable to put them in a location that it looks in.
# See:
# https://confluence.atlassian.com/bitbucket/test-reporting-in-pipelines-939708543.html
#
- MOCHA_FILE=./test-reports/[hash].xml gulp --cwd node_server test --reporter mocha-junit-reporter

36
eslint-for-arc.js Normal file
View File

@ -0,0 +1,36 @@
/**
* @fileOverview There is a compatibility problem between eslint and arcanist.
* ESLint returns a non-zero code on exit if there are any lint errors
* in the file. But `arc` regex liniters expect to only get an
* non-zero exit code on true errors (e.g. eslint config errors).
*
* So we use this file and the ESLint API to build a version that
* only returns non-zero on actual errors (which will be unhandled
* exceptions, so we don't actually need to do anything to achieve)
*/
const CLIEngine = require('eslint').CLIEngine;
const cli = new CLIEngine();
/**
* Get the file to lint from the command line. The command line is always
* argv[0] - node exe
* argv[1] - this script
* argv[2] - the file passed on the command line
*/
if (process.argv.length !== 3) {
throw new Error('Must pass exactly 1 file on the command line');
}
const filename = process.argv[2];
// Lint the file passed in
const report = cli.executeOnFiles([filename]);
// Get the compact formatter
const formatter = cli.getFormatter('compact');
// Output to stdout so it can be picked up by the arcanist regex
process.stdout.write(formatter(report.results));
// Allow the program to exit normally, which will return code 0

8
node_server/.jscsrc Normal file
View File

@ -0,0 +1,8 @@
{
"excludeFiles": ["node_modules/**", "bower_components/**"],
"preset": "google",
"validateIndentation": 4,
"maximumLineLength": 140,
"maxErrors": 1000,
"requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties"
}

58
node_server/.jshintrc Normal file
View File

@ -0,0 +1,58 @@
{
"bitwise": true,
"camelcase": true,
"curly": true,
"eqeqeq": true,
"es3": false,
"forin": true,
"freeze": true,
"immed": true,
"indent": 4,
"latedef": "nofunc",
"newcap": true,
"noarg": true,
"noempty": true,
"nonbsp": true,
"nonew": true,
"plusplus": false,
"quotmark": "single",
"undef": true,
"unused": false,
"strict": false,
"maxparams": 10,
"maxdepth": 5,
"maxstatements": 50,
"maxcomplexity": 8,
"maxlen": 140,
"asi": false,
"boss": false,
"debug": false,
"eqnull": true,
"esnext": true,
"evil": false,
"expr": false,
"funcscope": false,
"globalstrict": false,
"iterator": false,
"lastsemic": false,
"laxbreak": false,
"laxcomma": false,
"loopfunc": true,
"maxerr": 50,
"moz": false,
"multistr": false,
"notypeof": false,
"proto": false,
"scripturl": false,
"shadow": false,
"sub": true,
"supernew": false,
"validthis": false,
"noyield": false,
"node": true,
"globals": {
}
}

View File

@ -0,0 +1,21 @@
/**
* @file This file wraps the functions in auth.js with promises for simpler
* use in promises and async/await
*/
const Q = require('q');
const auth = require('./auth.js');
module.exports = {
validSession: (...args) => Q.nfapply(auth.validSession, args),
validateCurrentSession: (...args) => Q.nfapply(auth.validateCurrentSession, args),
checkHMAC: (...args) => Q.nfapply(auth.checkHMAC, args),
checkClientPassword: (...args) => Q.nfapply(auth.checkClientPassword, args),
/**
* Non-promise functions for compatibility
*/
respond: auth.respond,
checkClientStatus: auth.checkClientStatus,
checkDeviceStatus: auth.checkDeviceStatus
};

View File

@ -0,0 +1,941 @@
/**
* @fileOverview Node.js Authorisation Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var valid = require(global.pathPrefix + 'valid.js');
var mailer = require(global.pathPrefix + 'mailer.js');
var log = require(global.pathPrefix + 'log.js');
var config = require(global.configFile);
var templates = require(global.pathPrefix + '../utils/templates.js');
var formattingUtils = require(global.pathPrefix + '../utils/formatting.js');
var crypto = require('crypto');
var async = require('async');
var moment = require('moment');
/**
* This function checks the client status for any blocking flags. It will return an error if the following are true:
* 1) The client is not verified.
* 2) The client is barred.
*
* @type {function} checkClientStatus
* @param {!string} ClientStatus - The ClientStatus flag from the client document.
*/
exports.checkClientStatus = function(ClientStatus) {
/**
* Valid session. Check client status.
*/
if (!utils.bitsAllSet(ClientStatus, utils.ClientEmailVerifiedMask)) {
return utils.createError(114, 'Client e-mail has not been verified - please click the link.');
}
if (utils.bitsAllSet(ClientStatus, utils.ClientBarredMask)) {
return utils.createError(117, 'Client barred by Comcarde.');
}
return null;
};
/**
* This function checks the device status for any blocking flags. It will return an error if the following are true:
* 1) The device is not verified.
* 2) The device is not authorised.
* 3) The device is suspended.
* 4) The device is barred.
*
* @type {function} checkDeviceStatus
* @param {!string} DeviceStatus - The DeviceStatus flag from the device document.
*/
exports.checkDeviceStatus = function(DeviceStatus) {
/**
* Valid session. Check device status.
*/
if (!utils.bitsAllSet(DeviceStatus, utils.DeviceRegister2Mask)) {
return utils.createError(109, 'Device not verified - SMS not confirmed.');
}
if (!utils.bitsAllSet(DeviceStatus, utils.DeviceRegister3Mask)) {
return utils.createError(110, 'Device not authorised - PIN not set.');
}
if (utils.bitsAllSet(DeviceStatus, utils.DeviceSuspendedMask)) {
return utils.createError(111, 'Device suspended by the user.');
}
if (utils.bitsAllSet(DeviceStatus, utils.DeviceBarredMask)) {
return utils.createError(112, 'Device barred by Comcarde.');
}
return null;
};
/**
* This function needs to be called with all server requests. It checks the user is currently logged in
* and if they are, it returns their client and device details. It requires two parameters:
*
* @type {function} validateCurrentSession
* @param {!string} DeviceToken - The token assigned to the device at registration.
* @param {!string} SessionToken - The token returned at login. Note that this is valid for ~5 minutes only.
* Calling this function extends the SessionToken's life.
* @param {!function} next - Not optional and should contain the code to be subsequently executed.
*/
exports.validateCurrentSession = function(DeviceToken, SessionToken, next) {
/**
* Valid input. Check to see if the database is online.
* Cyclomatic complexity is known to be high for this function.
*/
//jshint -W074
mainDB.findOneObject(mainDB.collectionDevice, {DeviceToken: DeviceToken}, undefined, false, function(err, existingDevice) {
if (err) {
next(utils.createError(104, 'Database offline.'), null, null);
return;
}
/**
* No information returned from database.
*/
if (existingDevice === null) {
next(utils.createError(103, 'Cannot find device token.'), null, null);
return;
}
/**
* Device found. Now check the token.
*/
if (SessionToken !== existingDevice.SessionToken) {
// Session token invalid.
next(utils.createError(107, 'Invalid session token.'), null, null);
return;
}
/**
* Check the session token expiry.
*/
var timestamp = new Date();
var expiry = existingDevice.SessionTokenExpiry;
if (timestamp >= expiry) {
// Session token invalid.
next(utils.createError(108, 'Session token expired.'), null, null);
return;
}
/**
* Check device status.
*/
var currentDeviceStatus = exports.checkDeviceStatus(existingDevice.DeviceStatus);
if (currentDeviceStatus) {
next(currentDeviceStatus, null, null);
return;
}
/**
* Through device checks. Pull the client.
*/
mainDB.findOneObject(mainDB.collectionClient, {ClientID: existingDevice.ClientID}, undefined, false,
function(err, existingClient) {
/**
* Check for an error.
*/
if (err) {
// Database is not working.
next(utils.createError(105, 'Database offline.'), null, null);
return;
}
/**
* If null then there is no Client account.
*/
if (existingClient === null) {
// Callback.
next(utils.createError(106, 'Cannot find account.'), null, null);
return;
}
/**
* Check client status.
*/
var currentClientStatus = exports.checkClientStatus(existingClient.ClientStatus);
if (currentClientStatus) {
next(currentClientStatus, null, null);
return;
}
/**
* Great. All active. Extend token validity.
*/
var newExpiry = new Date(timestamp);
newExpiry.setMinutes(newExpiry.getMinutes() + utils.sessionTimeout);
mainDB.updateObject(mainDB.collectionDevice, {DeviceToken: DeviceToken}, {
$set: {
LastUpdate: timestamp,
SessionTokenExpiry: newExpiry
}
},
{upsert: false}, false, function(err) {
if (err) {
next(utils.createError(119, 'Database offline.'), null, null);
return;
}
/**
* Success!
*/
next(null, existingDevice, existingClient);
});
});
});
//jshint +W074
};
/**
* This function needs to be called with all server requests. It checks the user is currently logged in, esures the hmac is OK,
* and if so, it returns their client and device details. It requires multiple parameters:
*
* @type {function} validSession
* @param {!object} res - Response object for returning information. This function will respond directly on error.
* @param {!string} DeviceToken - The token assigned to the device at registration.
* @param {!string} SessionToken - The token returned at login. Note that this is valid for ~5 minutes only.
* Calling this function extends the SessionToken's life.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} hmacData - HMAC information from incoming packet.
* @param {!function} next - Not optional and should contain the code to be subsequently executed.
*/
exports.validSession = function(res, DeviceToken, SessionToken, functionInfo, hmacData, next) {
/**
* First check the session.
*/
exports.validateCurrentSession(DeviceToken, SessionToken, function(err, existingDevice, existingClient) {
if (err) {
res.status(200).json({
code: ('' + err.code),
info: err.message
});
log.system(
'WARNING',
err.message,
functionInfo.name,
err.code,
('AF [SessionToken ' + SessionToken + ' (DeviceToken ' + DeviceToken + ')]'),
(functionInfo.remote + ' (' + functionInfo.port + ')'));
/**
* Call back passing the error.
*/
next(err, null, null);
return;
}
/**
* Update the hmacData to store the ClientName from the existingClient as that
* is required for the HMAC generation and validation
*/
hmacData.ClientName = existingClient.ClientName;
/**
* Check the HMAC is fine.
*/
exports.checkHMAC(existingDevice, hmacData, functionInfo.name, function(err) {
if (err) {
res.status(200).json({
code: ('' + err.code),
info: err.message
});
log.system(
'WARNING',
err.message,
functionInfo.name,
err.code,
(existingDevice.ClientID + ' (' + existingDevice.DeviceNumber + ')'),
(functionInfo.remote + ' (' + functionInfo.port + ')'));
/**
* Call back passing the error.
*/
next(err, null, null);
return;
}
/**
* All valid. Proceed.
*/
next(null, existingDevice, existingClient);
});
});
};
/**
* Checks the PIN received from the device. This function will return an error object as the
* first parameter if something went wrong.
*
* @type {function} checkDevicePIN
* @param {!string} deviceAuthorisation - SHA256 of PIN as received from device.
* @param {!object} existingDevice - Existing object in database.
* @param {!object} timestamp - Reference time when clock was pulled.
* @param {!function} next - Function to call when verification complete.
*/
exports.checkDevicePIN = function(deviceAuthorisation, existingDevice, timestamp, next) {
/**
* Check for a locked device.
*/
if (existingDevice.LoginAttempts >= utils.PINLockout) {
next(utils.createError(399, 'Device locked. Please use PIN Reset.'));
return;
}
/**
* Split up the existing PIN and update if necessary.
*/
var receivedDeviceAuth;
var databaseDeviceAuth;
var authArray = existingDevice.DeviceAuthorisation.split('::');
async.series([
function(callback) {
/**
* Find the salt or create a new one if one doesn't exist.
*/
if (authArray[0] === '2') {
/**
* PIN encrypted using PBKDF2.
*/
crypto.pbkdf2(deviceAuthorisation, existingDevice.DeviceSalt,
config.encryptPBKDF2Rounds, config.encryptPBKDF2Bytes, config.encryptPBKDF2Protocol,
function(err, newHash) {
if (err) {
callback(err);
} else {
/**
* Update the database.
*/
receivedDeviceAuth = newHash.toString('hex');
databaseDeviceAuth = authArray[1];
callback(null);
}
});
} else {
/**
* Problem with the encryption string.
*/
callback('Unknown encryption type.');
}
}
],
/**
* Final clause which is executed after everything else or when an error is detected.
*/
function(err) {
if (err) {
next(utils.createError(400, ('Error when checking PIN: ' + err)));
return;
}
/**
* Check that the PIN matches.
*/
if (receivedDeviceAuth !== databaseDeviceAuth) {
/**
* Wrong PIN. Increase the fail count.
*/
mainDB.updateObject(mainDB.collectionDevice, {DeviceToken: existingDevice.DeviceToken}, {
$set: {LastUpdate: timestamp},
$inc: {LoginAttempts: 1}
},
{upsert: false}, false, function(err) {
if (err) {
next(utils.createError(401, 'Database offline.'));
return;
}
/**
* Check for maximum number of retries; if so, lock the account.
*/
if (existingDevice.LoginAttempts === (utils.PINLockout - 1)) {
/**
* Send warning e-mail.
*/
const suspendUrl = formattingUtils.formatPortalUrl('personal/devices');
var htmlEmail = templates.render('device-locked', {
DeviceNumber: existingDevice.DeviceNumber,
suspendDeviceUrl: suspendUrl
});
mailer.sendEmailByID(null, existingDevice.ClientID, 'Bridge Device Locked', htmlEmail, 'auth.checkDevicePIN',
function(err) {
if (err) {
next(utils.createError(402, 'Unable to send e-mail.'));
return;
}
/**
* Tell the user that the device has been locked.
*/
next(utils.createError(403, 'Wrong PIN. ' + utils.PINLockout +
' failed attempts have locked this device.'));
});
return;
}
/**
* Wrong PIN - more attempts left.
*/
next(utils.createError(404, 'Wrong PIN.'));
});
return;
}
/**
* PIN matched successfully.
*/
mainDB.updateObject(mainDB.collectionDevice, {DeviceToken: existingDevice.DeviceToken}, {
$set: {
LastUpdate: timestamp,
LoginAttempts: 0
}
},
{upsert: false}, false, function(err) {
if (err) {
next(utils.createError(405, 'Database offline.'));
return;
}
/**
* Success!
*/
next(null);
});
});
};
/**
* Checks the client password. This function will return an error object as the first parameter if something went wrong.
*
* @type {function} checkClientPassword
* @param {!string} password - SHA256 of password as received from device.
* @param {!object} existingClient - Existing object in database.
* @param {!object} timestamp - Reference time when clock was pulled.
* @param {!function} next - Function to call when verification complete.
*/
exports.checkClientPassword = function(password, existingClient, timestamp, next) {
/**
* Check for a locked account.
*/
if (existingClient.LoginAttempts >= utils.passwordLockout) {
next(utils.createError(406, 'Attempted login to locked account. Please contact Comcarde.'));
return;
}
/**
* Split up the existing password and update if necessary.
*/
var receivedPassword;
var databasePassword;
var passArray = existingClient.Password.split('::');
async.series([
function(callback) {
/**
* Find the salt or create a new one if one doesn't exist.
*/
if (passArray[0] === '2') {
/**
* Password encrypted using PBKDF2.
*/
crypto.pbkdf2(password, existingClient.ClientSalt,
config.encryptPBKDF2Rounds, config.encryptPBKDF2Bytes, config.encryptPBKDF2Protocol,
function(err, newHash) {
if (err) {
callback(err);
} else {
/**
* Update the database.
*/
receivedPassword = newHash.toString('hex');
databasePassword = passArray[1];
callback(null);
}
});
} else {
/**
* Problem with the encryption string.
*/
callback('Unknown encryption type.');
}
}
],
/**
* Final clause which is executed after everything else or when an error is detected.
*/
function(err) {
if (err) {
next(utils.createError(407, ('Error when checking password: ' + err)));
return;
}
/**
* Check that the password matches.
*/
if (receivedPassword !== databasePassword) {
/**
* Wrong password. Increase the fail count.
*/
mainDB.updateObject(mainDB.collectionClient, {ClientID: existingClient.ClientID}, {
$set: {LastUpdate: timestamp},
$inc: {LoginAttempts: 1}
},
{upsert: false}, false, function(err) {
if (err) {
next(utils.createError(408, 'Database offline.'));
return;
}
/**
* Check for maximum number of retries; if so, lock the account.
*/
if (existingClient.LoginAttempts === (utils.passwordLockout - 1)) {
/**
* Send warning e-mail.
*/
var htmlEmail = templates.render('account-locked', {
ClientName: existingClient.ClientName
});
mailer.sendEmail(null, existingClient.ClientName, 'Bridge Account Locked',
htmlEmail, 'auth.checkClientPassword',
function(err) {
if (err) {
next(utils.createError(409, 'Unable to send e-mail.'));
return;
}
/**
* Tell the user that the client account has been locked.
*/
next(utils.createError(410, 'Wrong password. ' + utils.passwordLockout +
' failed attempts have locked the client account.'));
});
return;
}
/**
* Wrong password - more attempts left.
*/
next(utils.createError(411, 'Wrong password.'));
});
return;
}
/**
* Password matched successfully. Reset login attempts.
*/
mainDB.updateObject(mainDB.collectionClient, {ClientID: existingClient.ClientID}, {
$set: {
LastUpdate: timestamp,
LoginAttempts: 0
}
},
{upsert: false}, false, function(err) {
if (err) {
next(utils.createError(412, 'Database offline.'));
return;
}
/**
* Success!
*/
next(null);
});
});
};
/**
* Creates a new salt and encrypts the password hash using PBKDF2.
* This function will return an error object as the first parameter if something went wrong.
*
* @type {function} encryptPBKDF2
* @param {!string} input - SHA256 of input to be encrypted using PBKDF2.
* @param {!function} next - Function to call when verification complete.
* @param {!object} next.err - Error object. null on success.
* @param {!string} next.newSalt - Random salt for encoding.
* @param {!string} next.newHash - Hashed input using new salt.
*/
exports.encryptPBKDF2 = function(input, next) {
/**
* Create a new salt.
*/
crypto.randomBytes(config.encryptPBKDF2Bytes, function(err, salt) {
if (err) {
next(err, null, null);
return;
}
/**
* Success. Encrypt the password.
*/
var newSalt = salt.toString('hex');
crypto.pbkdf2(input, newSalt, config.encryptPBKDF2Rounds, config.encryptPBKDF2Bytes, config.encryptPBKDF2Protocol,
function(err, hash) {
if (err) {
next(err, null, null);
return;
}
/**
* All done. Convert the hash and call back with the new values.
*/
var newHash = hash.toString('hex');
next(null, newSalt, newHash);
});
});
};
/**
* Reponds with an HTML page.
*
* @type {function} respond
* @param {!object} res - response object. End will be called by this function.
* @param {!int} responseCode - HTML response code.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {!string} code - The code associated with the response, e.g. '235', '10014'.
* @param {!string} fileName - Name of the Pug file that is the HTML source: e.g. 'templates/39_expired_token.pug'.
* @param {!object} data - Parameters used to render the HTML file.
* @param {!string} logType - The log entry type to be recorded - e.g. 'INFO', 'WARNING' etc.
* Omit if no system log entry is needed such as in 'Database offline'.
* @param {!string} infoString - If logType above is present, infoString is will be used as the information to be logged.
* @param {!string} altUser - If logType above is present, altUser can be used to log different user name. If not, 'UU' wil be used.
*/
exports.respondHTML = function(res, responseCode, functionInfo, code, fileName, data, logType, infoString, altUser) {
/**
* Respond to the request.
*/
var toReturn = templates.render(fileName, data);
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(toReturn);
/**
* Log what has happened if required.
*/
var logUser = '';
if (altUser) {
logUser = altUser;
} else {
logUser = 'UU';
}
if (logType) {
log.system(
logType,
infoString,
functionInfo.name,
code,
logUser,
(functionInfo.remote + ' (' + functionInfo.port + ')'));
}
/**
* Add HTML page generation details regardless.
*/
log.system(
'PAGE',
('Generated file returned [' + fileName + '].'),
functionInfo.name,
code,
logUser,
(functionInfo.remote + ' (' + functionInfo.port + ')'));
};
/**
* Uses a passed HMAC key to generate the data packet.
*
* @type {function} respond
* @param {!object} res - response object. End will be called by this function.
* @param {!int} responseCode - HTML response code.
* @param {!object} existingDevice - Existing object in database. If set to null the function works as a normal res.status() call.
* @param {!object} hmacData - hmac information {!address, !method, !body, !ClientName, ?timestamp, ?hmac}
* This function can be used to respond to non hmac calls by adding null in here.
* Typically set existingDevice and hmacData to null together.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {!object} data - Body of the packet as a JSON object. Note that 'info' and 'code' must be present.
* @param {!string} logType - The log entry type to be recorded - e.g. 'INFO', 'WARNING' etc.
* Omit if no system log entry is needed such as in 'Database offline'.
* @param {!string} altString - If logType above is present, altString can be used to log different data than the user receives.
* @param {!string} altUser - If logType above is present, altUser can be used to log different user name. If not, 'UU' wil be used.
* This parameter is ignored if hmacData and existingDevice are present.
*
* Cyclomatic complexity error disabled as it seems unnecessary.
*/
// jshint -W074
exports.respond = function(res, responseCode, existingDevice, hmacData, functionInfo, data, logType, altString, altUser) {
/**
* Ensure that the function is getting the correct data to process.
* Checking disabled for speed outwith the development environment.
*/
if (config.isDevEnv) {
if (typeof data !== 'object') {
throw new Error('auth.respond received bad data from ' + functionInfo.name + ': data is not an object.');
}
if (!('code' in data)) {
throw new Error('auth.respond received bad data from ' + functionInfo.name + ': data is missing code field.');
}
if (!('info' in data)) {
throw new Error('auth.respond received bad data from ' + functionInfo.name + ': data is missing info field.');
}
if (typeof data.code !== 'string') {
throw new Error('auth.respond received bad data from ' + functionInfo.name + ': data.code is not a string.');
}
if (typeof data.info !== 'string') {
throw new Error('auth.respond received bad data from ' + functionInfo.name + ': data.info is not a string.');
}
}
/**
* Set up variables for an HMAC based return.
*/
var key = '';
var DeviceUuid = '';
if (existingDevice) {
if (functionInfo.name === 'RotateHMAC.process') {
key = existingDevice.PendingHMAC;
DeviceUuid = existingDevice.DeviceUuid;
} else {
key = existingDevice.CurrentHMAC;
}
}
/**
* If no key is available then there is no HMAC. We do not need to sign.
*/
if ((hmacData === null) || (key === '')) {
/**
* Respond to the request.
*/
res.status(responseCode).json(data);
/**
* Log what has happened if required.
*/
if (logType) {
var logString = '';
var logUser = '';
if (altString) {
logString = altString;
} else {
logString = data.info;
}
if (altUser) {
logUser = altUser;
} else {
logUser = 'UU';
}
log.system(
logType,
logString,
functionInfo.name,
data.code,
logUser,
(functionInfo.remote + ' (' + functionInfo.port + ')'));
}
return;
}
/**
* Session token exception for Login1 due to the signing token not yet being saved.
*/
var sessionToken = '';
if (functionInfo.name === 'Login1.process') {
sessionToken = data.SessionToken;
} else {
sessionToken = existingDevice.SessionToken;
}
/**
* Process the data and create the hmac.
*/
var timestamp = moment().utc().format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z';
var text = JSON.stringify(data);
var fullText = hmacData.address + hmacData.method + timestamp + hmacData.ClientName + sessionToken + DeviceUuid + text;
var newkey = new Buffer(key, 'hex'); // Re-encode the key for use with the hmac.
var hmac = crypto.createHmac('sha256', newkey); // Create the HMAC object.
hmac.setEncoding('hex'); // Set encoding.
/**
* Note that the callback is attached as listener to stream's finish event.
*/
hmac.end(fullText, function() {
/**
* Read the HMAC and respond.
*/
var hash = hmac.read();
res.writeHead(responseCode, {
'bridge-hmac': hash,
'bridge-timestamp': timestamp,
'Content-Type': 'application/json; charset=utf-8' // Return that this is JSON
});
res.end(text);
/**
* Log what has happened if required.
*/
if (logType) {
var logString = '';
if (altString) {
logString = altString;
} else {
logString = data.info;
}
log.system(
logType,
logString,
functionInfo.name,
data.code,
(existingDevice.ClientID + ' (' + existingDevice.DeviceNumber + ')'),
(functionInfo.remote + ' (' + functionInfo.port + ')'));
}
});
};
// jshint +W074
/**
* Checks an incoming HMAC.
*
* @type {function} checkHMAC
* @param {!object} existingDevice - Existing object in database.
* @param {!object} hmacData - hmac information {!address, !method, !body, !ClientName, ?timestamp, ?hmac}
* @param {!string} functionName - The function that called the validation process: e.g. 'PayCodeRequest.process'.
* @param {!function} next - Function that should be called once processing is complete.
*
* Cyclomatic complexity error disabled as it seems unnecessary.
*/
// jshint -W074
exports.checkHMAC = function(existingDevice, hmacData, functionName, next) {
/**
* Check for HMAC problems first.
*/
if (existingDevice.HMACAttempts >= config.maxHMACAttempts) {
next(utils.createError(458, 'HMAC error: too many failed HMAC attempts.'));
return;
}
/**
* Only Login1 and RotateHMAC are valid calls if there is a PendingHMAC.
*/
if (existingDevice.PendingHMAC !== '') {
if ((functionName !== 'RotateHMAC.process') && (functionName !== 'Login1.process')) {
next(utils.createError(459, 'HMAC error: HMAC must be rotated using RotateHMAC.'));
return;
}
}
var key = '';
var DeviceUuid = '';
if (functionName === 'RotateHMAC.process') {
key = existingDevice.PendingHMAC;
DeviceUuid = existingDevice.DeviceUuid;
} else {
key = existingDevice.CurrentHMAC;
}
/**
* If the HMAC key is blank then no HMAC has been issued - re-register the device.
* Note there is one exception and that is on Login1 where there is a PendingHMAC.
*/
if (key === '') {
if ((functionName === 'Login1.process') && (existingDevice.PendingHMAC !== '')) {
next(null);
} else {
next(utils.createError(462, 'HMAC error: No valid HMAC key - please re-register the device.'));
}
return;
}
/**
* Look for timestamp errors.
*/
var output = '';
if (!('timestamp' in hmacData)) {
next(utils.createError(446, 'HMAC error: \"bridge-timestamp\" not present.'));
return;
} else {
output = valid.validateFieldTimeStamp(hmacData.timestamp);
if (output) {
next(utils.createError(449, output));
return;
}
/**
* Check for desync.
*/
var upperTimestamp = new Date();
upperTimestamp.setSeconds(upperTimestamp.getSeconds() + config.HMACDesyncThreshold);
if (hmacData.timestamp > upperTimestamp) {
next(utils.createError(451, 'HMAC error: timestamp is in the future.'));
return;
}
var lowerTimestamp = new Date();
lowerTimestamp.setSeconds(lowerTimestamp.getSeconds() - config.HMACDesyncThreshold);
if (lowerTimestamp > hmacData.timestamp) {
next(utils.createError(452, 'HMAC error: timestamp has expired.'));
return;
}
}
/**
* Look for hmac errors.
*/
if (!('hmac' in hmacData)) {
next(utils.createError(447, 'HMAC error: \"bridge-hmac\" not present.'));
return;
} else {
output = valid.validateFieldHMAC(hmacData.hmac);
if (output) {
next(utils.createError(450, output));
return;
}
}
/**
* Assemble the HMAC.
*/
var fullText = hmacData.address + hmacData.method + hmacData.timestamp + hmacData.ClientName + DeviceUuid + hmacData.body;
var newkey = new Buffer(key, 'hex'); // Re-encode the key for use with the hmac.
var hmac = crypto.createHmac('sha256', newkey); // Create the HMAC object.
hmac.setEncoding('hex'); // Set encoding.
/**
* Note that the callback is attached as listener to stream's finish event.
*/
hmac.end(fullText, function() {
/**
* Read the HMAC and respond.
*/
var hash = hmac.read();
if (hash !== hmacData.hmac) {
/**
* HMAC error. Tick up HMAC attempts or bar the device if there have been too many problems.
*/
var timestamp = new Date();
var toUpdate = {
$set: {LastUpdate: timestamp},
$inc: {HMACAttempts: 1}
};
if (existingDevice.HMACAttempts >= (config.maxHMACAttempts - 1)) {
toUpdate.$bit = {DeviceStatus: {or: utils.DeviceBarredMask}};
}
/**
* Write this information to the correct device.
*/
mainDB.updateObject(mainDB.collectionDevice, {DeviceToken: existingDevice.DeviceToken}, toUpdate,
{upsert: false}, false, function(err) {
if (err) {
next(utils.createError(460, ('HMAC error: database offline.')));
return;
}
/**
* Inform the device of the error.
*/
if (existingDevice.HMACAttempts >= (config.maxHMACAttempts - 1)) {
next(utils.createError(461, ('HMAC error: security check failed and device barred.')));
} else {
next(utils.createError(448, ('HMAC error: security check failed.')));
}
});
} else {
next(null);
}
});
};
// jshint +W074

View File

@ -0,0 +1,262 @@
/* eslint-disable no-process-env */
/* eslint-disable no-process-exit */
/* eslint-disable no-console */
/**
* @fileOverview Node.js Bridge Server Config for Bridge Pay
* @preserve Copyright 2014-2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*/
/* eslint-disable no-process-env, no-process-exit, no-console */
/**
* Includes needed for this module.
*/
const crypto = require('crypto');
const path = require('path');
const exitCodes = require('../exitcodes.js');
const packageJson = require('../package.json');
/**
* Version information.
*/
exports.EULAVersion = '1.0';
exports.CCServerReleaseType = 'Beta'; // Options include Alpha, Beta etc.
exports.useHTTPS = true;
/**
* Ensure NODE_ENV is either 'development' or 'production'.
*/
exports.isDevEnv = (process.env.NODE_ENV === 'development');
exports.isProdEnv = (process.env.NODE_ENV === 'production');
if (!exports.isDevEnv && !exports.isProdEnv) {
console.log('NODE_ENV environment variable missing or invalid: must be either \'development\' or \'production\'.');
process.exit(exitCodes.EXIT_CODE_NO_NODE_ENV);
}
/**
* Ensure the acquisition server is either 'test' or 'live'.
*/
exports.isTestEnv = (process.env.acquirer === 'test');
exports.isLiveEnv = (process.env.acquirer === 'live');
if (!exports.isTestEnv && !exports.isLiveEnv) {
console.log('Acquisition server environment variable missing or invalid: must be either \'test\' or \'live\'.');
process.exit(exitCodes.EXIT_CODE_NO_ACQUISITION_SERVER);
}
/**
* Checks that the appropriate environment variable is present and sets the variable if so.
* Error constants. These need to be in config as other code depends on the environment variables being loaded first.
*
* @param {!string} keyName - The key to check for.
* @param {!string} destName - The key to be written in exports.
* @param {varies} [defaultValue] - Default value if one is not provided in the environment
*/
exports.readENVVariable = function(keyName, destName, defaultValue) {
let value = null;
if (process.env.hasOwnProperty(keyName)) {
value = process.env[keyName];
} else if (defaultValue === undefined) {
console.log('Missing environment variable: ' + keyName + '\nPlease set the variables to launch the server.');
process.exit(exitCodes.EXIT_CODE_NO_ENVIRONMENT_VARS);
} else {
value = defaultValue;
}
/**
* Set the system variable if it has not been set. Do not overwrite.
*/
if (!(destName in exports)) {
exports[destName] = value;
}
};
/**
* Read in all the environment variables.
* First key is the environment varable, the second variable is the export name.
*/
exports.readENVVariable('AESKey', 'AESKey'); // e.g. kJq5fW4m/lLG6oLTcM+fPFmlHL9FU9=N
exports.readENVVariable('ServerCommit', 'ServerCommit'); // e.g. 7d33f90
exports.readENVVariable('PortalCommit', 'PortalCommit'); // e.g. 7d33f90
exports.readENVVariable('uuid', 'CCUUID'); // e.g. 2478b89c-c165-445b-9ff6-eb0beec60d12
exports.readENVVariable('HOSTNAME', 'CCServerName'); // e.g. Virtual machine name or ID
exports.readENVVariable('sgroup_name', 'CCServerGroup'); // e.g. Name of the group deployment.
exports.readENVVariable('loadbalancer_vip', 'CCServerIP'); // e.g. 172.31.0.2
exports.readENVVariable('webAddress', 'CCWebsiteAddress'); // e.g. dev.bridgepay.uk
exports.readENVVariable('forceHTTPS', 'forceHTTPS'); // e.g. 'true' or 'false'
if (exports.forceHTTPS === 'false') {
exports.useHTTPS = false;
}
/**
* API Service
*/
exports.readENVVariable('serverHttpPort', 'serverHttpPort', 80);
/**
* Encryption keys.
*/
exports.hashedAESKey = crypto.createHash('sha256').update(exports.AESKey).digest('hex'); // Hashed version of the above key.
exports.pinCryptoVersion = '2';
exports.passwordCryptoVersion = '2';
exports.encryptPBKDF2Rounds = 10000;
exports.encryptPBKDF2Bytes = 32;
exports.encryptPBKDF2Protocol = 'sha256';
exports.HMACBytes = 32;
exports.HMACDesyncThreshold = 120; // Seconds.
exports.maxHMACAttempts = 3;
/**
* Payments Setup.
* The verification provider can either be Worldpay or Credorax.
* Optionally, a card can be added with zero checking if 'None' is used.
* To shut down verification completely please use a blank string ''.
*/
exports.verificationProvider = 'Worldpay';
exports.demoCardPAN = '4917610000000000003';
/**
* Credorax setup.
*/
exports.readENVVariable('credoraxKey', 'comcardeCipherKey');
exports.readENVVariable('credoraxMerchantID', 'comcardeMerchantID');
if (exports.isTestEnv) {
exports.credoraxPrimaryGateway = 'https://intconsole.credorax.com/intenv/service/gateway'; // Normal gateway.
exports.credoraxSecondaryGateway = 'https://intconsole.credorax.com/intenv/service/gateway'; // Failover gateway.
} else {
exports.credoraxPrimaryGateway = 'https://comcarde-eu1.gate.credorax.net/crax_gate/service/gateway'; // Normal gateway.
exports.credoraxSecondaryGateway = 'https://comcarde-na1.gate.credorax.net/crax_gate/service/gateway'; // Failover gateway.
}
exports.credoraxCurrentGateway = exports.credoraxPrimaryGateway;
exports.credoraxChangeoverThreshold = 3; // Number of failed transactions before switchover.
exports.credoraxChangeRate = 1; // Number of failed comms reduced every 15 minutes.
exports.credoraxPrimaryGatewayFailure = 'https://' + exports.CCWebsiteAddress +
': The primary Credorax gateway has failed. Secondary is now active.';
/**
* Worldpay setup.
*/
exports.readENVVariable('worldpayMerchantID', 'worldpayMerchantID');
exports.readENVVariable('worldpayServiceKey', 'worldpayServiceKey');
exports.readENVVariable('worldpayClientKey', 'worldpayClientKey');
exports.worldpayPrimaryGateway = 'https://api.worldpay.com/v1/'; // Normal and test gateways are the same.
exports.worldpayNotificationThreshold = 3; // Number of failed transactions before notification.
exports.worldpayChangeRate = 1; // Number of failed comms reduced every 15 minutes.
exports.worldpayPrimaryGatewayFailure = 'https://' + exports.CCWebsiteAddress + ': The primary Worldpay gateway has failed.';
/**
* Database configuration.
*/
exports.readENVVariable('mongoUser', 'mongoUser');
exports.readENVVariable('mongoPassword', 'mongoPassword');
exports.readENVVariable('mongoDBAddress', 'mongoDBAddress');
exports.readENVVariable('mongoUseSSL', 'mongoUseSSL', 'true'); // Env vars are always strings.
exports.mongoUseSSL = (exports.mongoUseSSL !== 'false'); // Coerce to a bool as env is all strings.
if (exports.mongoUseSSL) {
exports.readENVVariable('mongoCACertBase64', 'mongoCACertBase64');
exports.mongoCA = [Buffer.from(exports.mongoCACertBase64, 'base64')];
}
exports.externaldbAddress = 'mongodb://' + exports.mongoUser + ':' + exports.mongoPassword +
exports.mongoDBAddress + '/MDB?connectTimeoutMS=5000&authMechanism=SCRAM-SHA-1&authSource=MDB';
if (exports.mongoUseSSL) {
exports.externaldbAddress += '&ssl=true';
}
exports.internaldbAddress = exports.externaldbAddress; // Currently one and the same as there is no internal route.
exports.databaseUpdate = false; // Automatically updates the database to the latest version.
exports.databaseUpdateWrite = true; // Normally true. When false, updates are not actually written ot the database. For testing.
exports.databaseIntegrityCheck = true; // Integrity checking is an option only if databaseUpdate is enabled.
exports.databaseArchiveTransactions = true; // Moves incomplete transactions to the TransactionArchive.
exports.databaseArchiveAccounts = true; // Removes deleted accounts that never made any transactions.
/**
* Selfie location.
*/
exports.defaultSelfie = 'defaultSelfie';
exports.defaultCompanyLogo0 = 'defaultCompanyLogo0';
exports.defaultSelfieData = '';
exports.defaultCompanyLogo0Data = '';
/**
* File system configuration.
*/
exports.temporaryDirectory = path.normalize(global.rootPath + 'temp/'); // Default swap directory.
/**
* LexisNexis Tracesmart IDU-AML values
* @see https://developer.tracesmart.co.uk/idu-aml
*/
exports.readENVVariable('tracesmartIduAmlUrl', 'tracesmartIduAmlUrl');
exports.readENVVariable('tracesmartIduAmlUsername', 'tracesmartIduAmlUsername');
exports.readENVVariable('tracesmartIduAmlPassword', 'tracesmartIduAmlPassword');
/**
* Ideal Postcodes API
* @see https://ideal-postcodes.co.uk/
*/
exports.readENVVariable('idealPostcodesKey', 'idealPostcodesKey');
/**
* General Defines.
*/
exports.maxAddresses = 25;
exports.maxRegTokenAttempts = 3;
exports.maxItems = 100;
exports.maxInvoiceNumberAttempts = 3;
exports.callTimeout = 30000; // Number of miliseconds that a server will hold a port open for before it is shut down.
/**
* Web Console host
* - Used for links in registration emails etc.
*/
exports.readENVVariable('cookieSecret', 'cookieSecret');
exports.webconsole = {
host: exports.CCWebsiteAddress,
path: '/portal/',
cookieSecret: exports.cookieSecret
};
/**
* Rate Limits for express-rate-limit.
* Note that the identifier for the "same" client depends on the type of call.
* It defaults to IP, but some of the APIs can be more specific based on session
* tokens or device ids.
* @see {@link https://github.com/nfriedly/express-rate-limit}
*/
exports.rateLimits = {
/* APIs */
api: {
windowMs: 15 * 60 * 1000, // 15 min window
max: 900, // Allow 900 requests per 15 min (~10 / second avg)
delayAfter: 0, // Never delay responses, only succeed or fail
delayMs: 0 // Never delay responses, only succeed or fail
},
/* Portal static files. */
portalStatic: {
windowMs: 15 * 60 * 1000, // 15 min window
max: 900, // Allow 900 requests per 15 minute, then fail (429)
delayAfter: 25, // Slow down after the first 25
delayMs: 10 // Delay by (10ms * (requests-delayAfter)) per request
},
/* Anything else - 404 page, selfies and other such things that haven't been removed */
fallback: {
windowMs: 1 * 60 * 60 * 1000, // 1 hour window
max: 100, // Allow 100 requests per hour, then fail (429)
delayAfter: 1, // Slow down after the first request
delayMs: 10 // Delay by 10ms * (requests-delayAfter) per request
}
};
/**
* Secret for creation and validation of JSON Web Tokens for the integrations API
* @see https://github.com/auth0/node-jsonwebtoken
*/
exports.readENVVariable('integrationsTokenSecret', 'integrationsTokenSecret');
/**
* The commit hash that the server was built from.
*/
exports.readENVVariable('commitHash', 'commitHash');
exports.CCServerVersion = packageJson.version + '-' + exports.commitHash;

View File

@ -0,0 +1,288 @@
/**
* @fileOverview Node.js Credorax Acquiring Code
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Integrates with Credorax and interprets any results received.
*/
var config = require(global.configFile);
var querystring = require('querystring');
var crypto = require('crypto');
var request = require('request');
var log = require(global.pathPrefix + 'log.js');
var sms = require(global.pathPrefix + 'sms.js');
/**
* Control defines.
*/
exports.useMCC6012 = 0; // Add [jx] fields. Support seems patchy.
exports.useAVS = 0; // Add [c4-c10] fields. Support seems patchy.
exports.credoraxPostData = 1; // Shows the info sent to Credorax.
exports.primaryFailedComms = 0; // Ticks up every time communications fail with Credorax's primary server.
exports.credoraxSMSAlertSent = 0; // Designed to prevent multiple SMS calls.
exports.credoraxTimeout = 25000; // Apps use 30 seconds as we need a little time to respond to them.
/**
* List of Appendix D errors so the system interprets a meaningful result.
*/
exports.processingResponseReason = [
{'00': 'Approved or completed successfully'},
{'01': 'Refer to card issuer'},
{'02': 'Refer to card issuer special condition'},
{'03': 'Invalid merchant'},
{'04': 'Pick up card'},
{'05': 'Do not Honour'},
{'06': 'Invalid Transaction for Terminal'},
{'07': 'Pick up card special condition'},
{'08': 'Time-Out'},
{'09': 'No Original'},
{'10': 'Approved for partial amount'},
{'11': 'Partial Approval'},
{'12': 'Invalid transaction card / issuer / acquirer'},
{'13': 'Invalid amount'},
{'14': 'Invalid card number'},
{'17': 'Invalid Capture date (terminal business date)'},
{'19': 'System Error; Re-enter transaction'},
{'20': 'No From Account'},
{'21': 'No To Account'},
{'22': 'No Checking Account'},
{'23': 'No Saving Account'},
{'24': 'No Credit Account'},
{'30': 'Format error'},
{'34': 'Implausible card data'},
{'39': 'Transaction Not Allowed'},
{'41': 'Lost Card, Pickup'},
{'42': 'Special Pickup'},
{'43': 'Hot Card, Pickup (if possible)'},
{'44': 'Pickup Card'},
{'51': 'Not sufficient funds'},
{'52': 'No checking Account'},
{'53': 'No savings account'},
{'54': 'Expired card'},
{'55': 'Pin incorrect'},
{'57': 'Transaction not allowed for cardholder'},
{'58': 'Transaction not allowed for merchant'},
{'59': 'Suspected Fraud'},
{'61': 'Exceeds withdrawal amount limit'},
{'62': 'Restricted card'},
{'63': 'MAC Key Error'},
{'65': 'Activity count limit exceeded'},
{'66': 'Exceeds Acquirer Limit'},
{'67': 'Retain Card; no reason specified'},
{'68': 'Response received too late.'},
{'75': 'Pin tries exceeded'},
{'76': 'Invalid Account'},
{'77': 'Issuer Does Not Participate In The Service'},
{'78': 'Function Not Available'},
{'79': 'Key Validation Error'},
{'80': 'Approval for Purchase Amount Only'},
{'81': 'Unable to Verify PIN'},
{'82': 'Time out at issuer system'},
{'83': 'Not declined (Valid for all zero amount transactions)'},
{'84': 'Invalid Life Cycle of transaction'},
{'85': 'Not declined'},
{'86': 'Cannot verify pin'},
{'87': 'Purchase amount only, no cashback allowed'},
{'88': 'MAC sync Error'},
{'89': 'Security Violation'},
{'91': 'Issuer not available'},
{'92': 'Unable to route at acquirer Module'},
{'93': 'Transaction cannot be completed'},
{'94': 'Duplicate transaction'},
{'95': 'Contact Acquirer'},
{'96': 'System malfunction'},
{'97': 'No Funds Transfer'},
{'98': 'Duplicate Reversal'},
{'99': 'Duplicate Transaction'},
{'N3': 'Cash Service Not Available'},
{'N4': 'Cash Back Request Exceeds Issuer Limit'},
{'N7': '(Visa) decline; CVV2 failure.'},
{'R0': 'Stop Payment Order'},
{'R1': 'Revocation of Authorisation Order'},
{'R3': 'Revocation of all Authorisations Order'}
];
/**
* Credorax main API function call.
*
* @type {function} CredoraxFunction
* @param {!object} credorax - JSON stucture with filled in data that represents the call.
* @param {!object} M - Merchant ID.
* @param {!object} cipherKey - Key issued to acquiring merchant.
* @param {!function} callback - Call back that returns the result.
*/
exports.CredoraxFunction = function(credorax, M, cipherKey, callback) {
/**
* Local variables.
*/
var cryptoString = M;
/**
* Parse all keys.
*/
Object.keys(credorax).forEach(function(key) { cryptoString += credorax[key]; });
/**
* Create hash.
*/
cryptoString += cipherKey;
var hash = crypto.createHash('MD5').update(cryptoString).digest('hex');
/**
* Create query string.
*/
var postData = querystring.stringify({'K': hash, 'M': M});
postData += '&' + querystring.stringify(credorax);
postData = postData.replace('*', '%2A'); // stringify contains an error that does not process the asterisk correctly.
/**
* Show the data to be posted. Never post this information on the live server.
*/
if (exports.credoraxPostData && config.isDevEnv) {
log.system(
'INFO',
('[OUT] parameters: ' + JSON.stringify(postData)),
'credorax.CredoraxFunction',
'',
'System',
'127.0.0.1');
}
/**
* Process the data by submitting it to Credorax.
*/
if (postData.length > 0) {
/**
* Set the headers
*/
var headers = {
'User-Agent': 'Super Agent/0.0.1',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Content-Length': postData.length
};
/**
* Configure the request
*/
var options = {
url: config.credoraxCurrentGateway,
method: 'POST',
headers: headers,
form: postData,
timeout: exports.credoraxTimeout
};
/**
* Start the request.
*/
request(options, function(error, response, body) {
if (!error && response.statusCode === 200) {
/**
* A response was received. This does not mean the operation was successful.
*/
var credoraxResult = querystring.parse(body);
return callback(null, credoraxResult);
} else if (!error && response.statusCode !== 200) {
/**
* HTTP error.
*/
return callback('Error: HTTP Code: ' + response.statusCode);
} else {
/**
* Return a request error.
*/
return callback('Error: ' + error);
}
});
} else {
/**
* No data to post.
*/
return callback('No data to process.');
}
};
/**
* Credorax z6 code interpretation.
*
* @type {function} interpretZ6
* @param {!string} z6 - Error code.
* @return {!string} Interpreted error code or 'Unknown error code' if it is not found.
*/
exports.interpretZ6 = function(z6) {
var reason = 'Unknown error code';
if (exports.processingResponseReason.hasOwnProperty(z6)) {
reason = exports.processingResponseReason[z6];
}
return reason;
};
/**
* This function deals with Credorax communication failures and gateway switchover if necessary.
*
* @type {function} commsFailure
* @param {!string} source - The function where the error was called from e.g. 'AddCard.process'.
*/
exports.commsFailure = function(source) {
/**
* General error - usually indicates Credorax is down.
* First check the current gateway and switch if necessary.
*/
if (config.credoraxCurrentGateway === config.credoraxPrimaryGateway) {
/**
* Still on primary gateway.
*/
if (exports.primaryFailedComms >= (config.credoraxChangeoverThreshold - 1)) {
/**
* Too many failures. Switching to secondary gateway.
*/
config.credoraxCurrentGateway = config.credoraxSecondaryGateway;
log.system(
'CRITICAL',
'Credorax primary gateway down. Moving to secondary gateway.',
source,
'',
'System',
'127.0.0.1');
/**
* Inform admins.
*/
if (exports.credoraxSMSAlertSent === 0) {
/**
* Block multiple SMS messages and send a single one to the admin(s).
* Note that SMS is blocked before we know it has been sent; the callback structure means that this could
* be initialised hundreds of times on a loaded system before the first one returned.
*/
exports.credoraxSMSAlertSent = 1;
sms.sendSMS(null, (sms.adminMobile + ',' + sms.backupMobile),
config.credoraxPrimaryGatewayFailure, function(err, smsBalance) {
if (err) {
log.system(
'ERROR',
'Unable to send SMS.',
source,
'',
'System',
'127.0.0.1');
return;
}
/**
* Success.
*/
log.system(
'INFO',
('Credorax primary gateway failure. SMS sent to admins (SMS balance now ' + smsBalance + ').'),
source,
'',
'System',
'127.0.0.1');
});
}
} else {
exports.primaryFailedComms += 1;
}
}
};

View File

@ -0,0 +1,518 @@
/* eslint-disable no-var, no-unused-vars, vars-on-top */
/* eslint-disable spaced-comment, global-require, lines-around-comment, comma-spacing */
/* eslint-disable no-use-before-define, no-useless-escape, brace-style, padded-blocks */
/* eslint-disable prefer-arrow-callback, promise/always-return, unicorn/catch-error-name */
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Comcarde Node.js JSON Handler
// Provides -Bridge- pay functionality.
// Copyright 2014-2015 Comcarde
// Written by Keith Symington
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Includes
var moment = require('moment');
var querystring = require('querystring');
var crypto = require('crypto');
var mongodb = require('mongodb');
var path = require('path');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var log = require(global.pathPrefix + 'log.js');
var sms = require(global.pathPrefix + 'sms.js');
var mailer = require(global.pathPrefix + 'mailer.js');
var auth = require(global.pathPrefix + 'auth.js');
var credorax = require(global.pathPrefix + 'credorax.js');
var config = require(global.configFile);
var SendReport = require(global.pathPrefix + 'hJSON/SendReport');
var validator = require(global.pathPrefix + '../schemas/validator');
// Local variables.
exports.JSONServed = 0;
exports.showPackets = true; // Use to show the incoming packet detail. This is a debug mode.
exports.REST = 1;
exports.form = 2;
/**
* List of commands that are supported by this function. Commands added to
* this list MUST:
* 1. Have a matching handler in 'ComServe/hJSON/<name>.js''
* 2. Have a matching schema for the body in 'schemas/<name>.json'
* 3. Have the approprite errorcode added to the list
*
* WARNING: due to the change in direction, ALL commands are now unsupported!
* They have been moved to `UNSUPPORTED_COMMANDS` for easy reverting if needed.
*/
const SUPPORTED_COMMANDS = {};
const UNSUPPORTED_COMMANDS = {
/**
* Login and Authorisation Commands
*/
AcceptEULA: {validationErrorCode: 292},
Authorise2FARequest: {validationErrorCode: 455},
Get2FARequest: {validationErrorCode: 453},
KeepAlive: {validationErrorCode: 123},
LogOut1: {validationErrorCode: 123},
Login1: {validationErrorCode: 149},
PINReset: {validationErrorCode: 128},
RotateHMAC: {validationErrorCode: 444},
ElevateSession: {validationErrorCode: 558},
/**
* Account Commands
*/
AddAddress: {validationErrorCode: 378},
AddCard: {validationErrorCode: 122},
ChangePIN: {validationErrorCode: 415},
ChangePassword: {validationErrorCode: 418},
DeleteAccount: {validationErrorCode: 151},
DeleteAddress: {validationErrorCode: 384},
GetTransactionDetail: {validationErrorCode: 190},
GetTransactionHistory: {validationErrorCode: 186},
ListAccounts: {validationErrorCode: 519},
ListDeletedAccounts: {
validationErrorCode: 519,
commandHandler: 'ListAccounts'
},
ListAddresses: {validationErrorCode: 376},
SetAccountAddress: {validationErrorCode: 391},
SetDefaultAccount: {validationErrorCode: 300},
/**
* Image Commands
*/
AddImage: {validationErrorCode: 217},
GetImage: {validationErrorCode: 220},
IconCache: {validationErrorCode: -1}, // Has no body, so can't fail validation
ImageCache: {validationErrorCode: 520},
ReportImage: {validationErrorCode: 223},
/**
* Invoice Commands
*/
ConfirmInvoice: {validationErrorCode: 493},
GetInvoice: {validationErrorCode: 513},
ListInvoices: {validationErrorCode: 511},
RejectInvoice: {validationErrorCode: 516},
/**
* Merchant Commands
*/
ListItems: {validationErrorCode: 471},
/**
* Message Commands
*/
DeleteMessage: {validationErrorCode: 485},
GetMessage: {validationErrorCode: 479},
ListMessages: {validationErrorCode: 477},
MarkMessage: {validationErrorCode: 482},
/**
* Payment Commands
*/
CancelPaymentRequest: {validationErrorCode: 161},
ConfirmTransaction: {validationErrorCode: 181},
GetTransactionUpdate: {validationErrorCode: 170},
PayCodeRequest: {validationErrorCode: 150},
RedeemPayCode: {validationErrorCode: 174},
RefundTransaction: {validationErrorCode: 227},
/**
* Registration Commands
*/
AddDevice: {validationErrorCode: 330},
DeleteDevice: {validationErrorCode: 363},
GetClientDetails: {validationErrorCode: 424},
ListDevices: {validationErrorCode: 361},
Register1: {validationErrorCode: 2},
Register2: {validationErrorCode: 124},
Register3: {validationErrorCode: 125},
Register4: {validationErrorCode: 126},
Register6: {validationErrorCode: 140},
Register7: {
validationErrorCode: 141,
paramsValidator: 'Register7.params'
},
Register8: {validationErrorCode: 142},
ResumeDevice: {validationErrorCode: 432},
SetClientDetails: {validationErrorCode: 422},
SetDeviceName: {validationErrorCode: 438},
SuspendDevice: {validationErrorCode: 427},
/**
* Utils functions
*/
PostCodeLookup: {validationErrorCode: 530}
};
/**
* Define where the schemas should be loaded from
*/
const SCHEMA_DIR = path.join(global.pathPrefix, '..', 'schemas');
/**
* Define where the command handlers should be loaded from
*/
const COMMAND_HANDLER_DIR = path.join(global.pathPrefix, 'hJSON');
/**
* Stores the command handlers that are loaded based on the SUPPORTED_COMMANDS
*/
var commandHandlers = {};
/**
* Loads the command handlers base on the list of supported commands
*/
function loadCommandHandlers() {
/**
* Pull in all the command handlers
*/
var commands = Object.keys(SUPPORTED_COMMANDS);
var paramsSchemas = ['defaultCommandOnly.params']; // Default unless otherwise specified
for (var i = 0; i < commands.length; ++i) {
/**
* Default command name is the same as the key
*/
var command = commands[i];
var commandHandler = command;
/**
* Some commands need a custom command handler name.
* e.g. `ListDeletedAccounts` uses `ListAccounts`
*/
var commandInfo = SUPPORTED_COMMANDS[command];
if (commandInfo && commandInfo.commandHandler) {
commandHandler = commandInfo.commandHandler;
}
/**
* `require` the command handler
*/
commandHandlers[command] = require(
path.join(COMMAND_HANDLER_DIR, commandHandler)
);
/**
* Some commands have custom parameters validators. If so, add them
* to the additional schemas list.
*/
if (commandInfo && commandInfo.paramsValidator) {
paramsSchemas.push(commandInfo.paramsValidator);
}
}
/**
* Initialise the validator with the same list of supported commands
*/
var schemas = commands.concat(paramsSchemas);
validator.initialise(schemas, config.isDevEnv, SCHEMA_DIR);
}
/**
* Call loadCommandHandlers immediately to register all the handlers we have
*/
loadCommandHandlers();
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Reads the JSON data out of the packet.
// req and res are the request and response packets.
// remoteAddress: is the source address of the incoming link.
// protocolPort is 'HTTPS:443' for example. This is a text string only that is put to the logs.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
exports.handleJSONRequest = function(req, res, remoteAddress, protocolPort, parameters, type) {
// Local objects.
var serverData = '';
var receivedObject = {};
// Reset number of JSON requests served.
exports.JSONServed++;
////////////////////////////////////////////////////////////////////////////////////////////////////
// Receipt of 'data' function.
////////////////////////////////////////////////////////////////////////////////////////////////////
var dataReceived = function(chunk) {
// Check that the maximum packet size is not above the current limit.
if (serverData.length > utils.maxPacketSize) {
// Too much data. Shut down receive methods.
req.removeListener('data', dataReceived);
req.removeListener('end', requestEnd);
// Return an error code.
res.writeHead(413, {'Content-Type': 'application/json'});
res.end(
'{\"code\":\"280\",' +
'\"info\":\"Packet too large.\"}');
log.system(
'ATTACK',
'Packet too large.',
'hJSON.handleJSONRequest',
'280',
'UU',
(remoteAddress + ' (' + protocolPort + ')'));
} else { // Overflow limit not reached. Add the data.
serverData += chunk;
}
};
////////////////////////////////////////////////////////////////////////////////////////////////////
// 'End' of data stream.
////////////////////////////////////////////////////////////////////////////////////////////////////
//jshint -W074
var requestEnd = function() {
// Protect against unhandled exceptions.
try {
// Try to parse the data to see if it is JSON.
try {
// Parse the data received. Some commands are pure rest so skip null data.
if (serverData !== '') {
if (type === exports.REST) {
receivedObject = JSON.parse(serverData);
} else if (type === exports.form) {
receivedObject = querystring.parse(serverData);
}
}
}
catch (err) {
// Unable to process querystring or body JSON.
res.writeHead(200, {'Content-Type': 'application/json'});
if (type === exports.REST) {
res.end(
'{\"code\":\"324\",' +
'\"info\":\"Invalid JSON in packet.\"}');
log.system(
'WARNING',
('Invalid JSON in packet. ' + err.name + ' (' + err.message + ') Message not logged for security reasons.'),
'hJSON.handleJSONRequest',
'324',
'UU',
(remoteAddress + ' (' + protocolPort + ')'));
} else if (type === exports.form) {
res.end(
'{\"code\":\"325\",' +
'\"info\":\"Invalid querystring.\"}');
log.system(
'WARNING',
('Invalid querystring. ' + err.name + ' (' + err.message + ') Message not logged for security reasons.'),
'hJSON.handleJSONRequest',
'325',
'UU',
(remoteAddress + ' (' + protocolPort + ')'));
}
// Return after error.
return;
}
// Detailed logging of input/output for debug purposes. Never show in the live environment.
if (exports.showPackets && config.isDevEnv) {
if (type === exports.REST) {
log.system(
'INFO',
('[IN] parameters: ' + JSON.stringify(parameters) + ' [REST IN] parsed data: ' +
JSON.stringify(receivedObject)),
'hJSON.showPackets',
'',
'UU',
(remoteAddress + ' (' + protocolPort + ')'));
} else if (type === exports.form) {
log.system(
'INFO',
('[IN] parameters: ' + JSON.stringify(parameters) + ' [FORM IN] parsed data: ' +
JSON.stringify(receivedObject)),
'hJSON.showPackets',
'',
'UU',
(remoteAddress + ' (' + protocolPort + ')'));
}
}
/**
* Create hmac object.
*/
var hmacData = {};
hmacData.address = 'https://' + req.headers.host + req.url;
hmacData.method = req.method;
hmacData.body = serverData;
if ('bridge-timestamp' in req.headers) {
hmacData.timestamp = req.headers['bridge-timestamp'];
}
if ('bridge-hmac' in req.headers) {
hmacData.hmac = req.headers['bridge-hmac'];
}
/**
* Check for unknown packets. Note there are a couple of exceptions where commands
* can be generated by non-apps.
*/
if (!(('user-agent' in req.headers) && ((req.headers['user-agent'].substr(0,6)) === 'Bridge'))) {
if ((parameters.Command !== 'Register7') &&
(parameters.Command !== 'SendReport')) {
res.status(403).json({
code: '464',
info: 'Forbidden.'
});
log.system(
'WARNING',
'Request forbidden: user-agent is not Bridge.',
'hJSON.handleJSONRequest',
'464',
'UU',
(remoteAddress + ' (' + protocolPort + ')'));
return;
}
}
/**
* Build function info.
*/
var functionInfo = {};
functionInfo.name = parameters.Command + '.process';
functionInfo.remote = remoteAddress;
functionInfo.port = protocolPort;
/**
* Check if this is a command that is handled from the dynamically
* loaded and validated commands.
*/
if (commandHandlers.hasOwnProperty(parameters.Command)) {
/**
* It is a dynamically created function so handle it here
*/
doCommand(res, functionInfo, parameters, receivedObject, hmacData);
} else {
/**
* All of the hard-coded JSON commands are examined here. The most common should be put at the top.
* Once all commands have been converted over to using JSON Schema validation we can
* remove this case.
*/
switch (parameters.Command) {
case 'SendReport':
SendReport.process(res, functionInfo, parameters);
break;
////////////////////////////////////////////////////////////////////////////////////////////////////
// Error condition - unknown commands.
////////////////////////////////////////////////////////////////////////////////////////////////////
default:
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(
'{\"code\":\"0\",' +
'\"info\":\"Unknown Command.\"}');
log.system(
'WARNING',
('Unknown \"Command:\" in url (' + parameters.Command + ')'),
'hJSON.handleJSONRequest',
'0',
'UU',
(remoteAddress + ' (' + protocolPort + ')'));
}
}
}
catch (err) {
// Processing error. Now actual error returned in message
var responseObj = {
code: '4',
info: 'Unhandled Exception - ' + err.message
};
res.status(200).json(responseObj);
log.system(
'CRITICAL',
('Unhandled Exception - ' + err.name + ' (' + err.message + ')'),
'hJSON.handleJSONRequest',
'4',
'UU',
(remoteAddress + ' (' + protocolPort + ')'));
}
};
////////////////////////////////////////////////////////////////////////////////////////////////////
// Bind events to functions.
////////////////////////////////////////////////////////////////////////////////////////////////////
req.on('data', dataReceived); // Data indicates that information is still arriving.
req.on('end', requestEnd); // End indicates that everything has been read.
};
/**
* Runs the command specified by `parameters.Command`.
* It first validates the body using JSON Schema, then calls the command handler
*
* @param {!Object} res - Response object for returning information.
* @param {!Object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?Object} parameters - Input parameters posted on link.
* @param {?Object} receivedObject - Input parameters in message body.
* @param {!Object} hmacData - hmac information {!address, !method, !body, ?timestamp, ?hmac}
*/
function doCommand(res, functionInfo, parameters, receivedObject, hmacData) {
var commandName = parameters.Command;
var commandInfo = SUPPORTED_COMMANDS[commandName];
var paramsSchema = commandInfo.paramsValidator || 'defaultCommandOnly.params';
var bodyValidatorP = validator.validate(commandName, receivedObject);
var paramsValidatorP = validator.validate(paramsSchema, parameters);
Promise.all([bodyValidatorP, paramsValidatorP])
.then(function onValidationSucceeded() {
/**
* Validated, so run the command
*/
try {
commandHandlers[commandName].process(
res,
functionInfo,
parameters,
receivedObject,
hmacData
);
} catch (err) {
/* Processing error. Return it to the caller */
var responseObj = {
code: '4',
info: 'Unhandled exception - ' + err.message
};
auth.respond(
res,
200,
null, // Don't know what device was used
null, // Don't pass in HMAC data as we don't have a device.
functionInfo,
responseObj,
'CRITICAL'
);
}
})
.catch(function onValidationFailed(err) {
var hasErrorDetail = Array.isArray(err.errors) && err.errors.length > 0;
/**
* Failed validation, so return an error
*/
var responseObj = {
code: commandInfo.validationErrorCode ?
commandInfo.validationErrorCode.toString() :
'-1',
info: 'Invalid body in request'
};
/* Add a little more detail if we have it */
if (hasErrorDetail) {
responseObj.info =
'Invalid body' +
err.errors[0].dataPath;
}
/* If we are in dev, also return the detailed error info from the validator */
if (config.isDevEnv && hasErrorDetail) {
responseObj.info += ': ' + err.errors[0].message;
responseObj.devOnlyErrorDetail = err;
}
auth.respond(
res,
200,
null, // Don't know what device was used
null, // Don't pass in HMAC data as we don't have a device.
functionInfo,
responseObj,
'WARNING'
);
});
}

View File

@ -0,0 +1,72 @@
/**
* @fileOverview Node.js AcceptEULA Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Confirms that the user has accepted the EULA. JSON version.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/login_auth/accepteula/}
*/
/**
* Includes
*/
var auth = require(global.pathPrefix + 'auth.js');
var log = require(global.pathPrefix + 'log.js');
var utils = require(global.pathPrefix + 'utils.js');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var config = require(global.configFile);
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* New version accepted.
*/
var newLastUpdate = new Date();
mainDB.updateObject(mainDB.collectionClient, {ClientID: existingClient.ClientID}, {
$set: {
EULAVersionAccepted: config.EULAVersion,
LastUpdate: newLastUpdate
},
$inc: {LastVersion: 1}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '290',
info: 'Database offline.'
});
return;
}
/**
* Updated.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10044',
info: 'EULA acceptance confirmed.'
},
'INFO',
('V' + config.EULAVersion + ' EULA acceptance confirmed.'));
});
});
};

View File

@ -0,0 +1,180 @@
/**
* @fileOverview Node.js Add Address Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Adds an address which will be associated with the current client.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/account_commands/addaddress/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var config = require(global.configFile);
var utils = require(global.pathPrefix + 'utils.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Search for the requested addresses.
* Cyclomatic complexity warning disabled.
*/
//jshint -W074
mainDB.collectionAddresses.find({ClientID: existingClient.ClientID},
{
_id: 1,
AddressDescription: 1
}
).toArray(function(err, addresses) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '379',
info: 'Database offline.'
});
return;
}
/**
* Check for maximum number of entries. GTE used to trap problem cases or changes
* in config.maxAddresses.
*/
if (addresses.length >= config.maxAddresses) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '380',
info: 'Maximum number of addresses reached.'
},
'WARNING');
return;
}
/**
* Check that the description does not already exist.
*/
var counter;
for (counter = 0; counter < addresses.length; counter++) {
if (addresses[counter].AddressDescription === receivedObject.AddressDescription) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '381',
info: 'AddressDescription already exists.'
},
'WARNING');
return;
}
}
/**
* Create and populate the new address.
*/
var timestamp = new Date();
var newAddress = mainDB.blankAddress();
newAddress.ClientID = existingClient.ClientID;
newAddress.AddressDescription = receivedObject.AddressDescription;
if (receivedObject.BuildingNameFlat) {
newAddress.BuildingNameFlat = receivedObject.BuildingNameFlat;
}
newAddress.Address1 = receivedObject.Address1;
if (receivedObject.Address2) {
newAddress.Address2 = receivedObject.Address2;
}
newAddress.Town = receivedObject.Town;
if (receivedObject.County) {
newAddress.County = receivedObject.County;
}
newAddress.PostCode = receivedObject.PostCode;
newAddress.Country = receivedObject.Country;
if (receivedObject.PhoneNumber) {
newAddress.PhoneNumber = receivedObject.PhoneNumber;
}
newAddress.DateAdded = timestamp;
newAddress.LastUpdate = timestamp;
/**
* Add the object to the addresses database.
*/
mainDB.addObject(mainDB.collectionAddresses, newAddress, undefined, false, function(err, objectAdded) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '382',
info: 'Database offline.'
});
return;
}
/**
* Address successfully added.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10053',
info: 'Address added.',
AddressID: objectAdded[0]._id
},
'INFO');
/**
* Check KYC flag and update if necessary.
* note that this is a valid bitwise comparison and manipulation.
*/
//jshint -W016
if (!(existingClient.ClientStatus & utils.ClientAddressMask)) {
var newClientStatus = existingClient.ClientStatus | utils.ClientAddressMask;
//jshint +W016
mainDB.updateObject(mainDB.collectionClient, {ClientID: existingDevice.ClientID},
{
$set: {
ClientStatus: newClientStatus,
LastUpdate: timestamp
},
$inc: {LastVersion: 1}
},
{upsert: false}, false, function(err) {
if (err) {
log.system(
'ERROR',
'Could not set address mask flag set (KYC2) as database was offline.',
'AddAddress.process',
'',
(existingDevice.ClientID + ' (' + existingDevice.DeviceNumber + ')'),
(functionInfo.remote + ' (' + functionInfo.port + ')'));
return;
}
/**
* System note that the KYC flag has been set.
*/
log.system(
'INFO',
'Client address mask flag set (KYC2).',
'AddAddress.process',
'',
(existingDevice.ClientID + ' (' + existingDevice.DeviceNumber + ')'),
(functionInfo.remote + ' (' + functionInfo.port + ')'));
});
}
});
});
//jshint +W074
});
};

View File

@ -0,0 +1,336 @@
/**
* @fileOverview Node.js AddCard handling code for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Adds a card to the account table associated to the supplied user.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/account_commands/addcard/}
*/
'use strict';
/**
* Includes needed for this module.
*/
const _ = require('lodash');
var moment = require('moment');
var mongodb = require('mongodb');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var config = require(global.configFile);
var worldpay = require(global.pathPrefix + 'worldpay.js');
var sms = require(global.pathPrefix + 'sms.js');
var anon = require(global.pathPrefix + '../utils/anon.js');
const acquirers = require(global.pathPrefix + '../utils/acquirers/acquirer.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Ensure that the client details are set.
*/
if (!(utils.bitsAllSet(existingClient.ClientStatus, utils.ClientDetailsMask))) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '521',
info: 'No client details set.'
},
'INFO');
return;
}
/**
* Retrieve the address from the database.
*/
mainDB.findOneObject(mainDB.collectionAddresses,
{
_id: mongodb.ObjectID(receivedObject.BillingAddress),
ClientID: existingClient.ClientID
},
undefined,
false,
function(err, existingAddress) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '522',
info: 'Database offline.'
});
return;
}
/**
* Check if any addresses match the search query.
*/
if (!existingAddress) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '523',
info: 'Invalid billing address.'
},
'WARNING');
return;
}
/**
* Create a new Account and fill in as a card.
*/
var newCard = mainDB.blankAccount();
newCard.ClientID = existingClient.ClientID;
newCard.ClientAccountName = receivedObject.ClientAccountName;
newCard.UserImage = receivedObject.UserImage;
newCard.NameOnAccount = receivedObject.NameOnAccount;
/**
* Encrypt and store the card details.
*/
var temp;
/**
* CardPAN
*/
temp = utils.encryptDataV3(receivedObject.CardPAN, receivedObject.ClientKey, existingClient._id.toString());
if (typeof temp !== 'string') {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '524',
info: 'Error when encrypting CardPAN.'
},
'WARNING',
('Encryption V3 error: CardPAN, Code: ' + temp.code.toString() + ' (' + temp.message + ').'));
return;
}
newCard.CardPAN = anon.anonymiseCardPAN(receivedObject.CardPAN);
newCard.CardPANEncrypted = temp;
/**
* CardExpiry
*/
temp = utils.encryptDataV3(receivedObject.CardExpiry, receivedObject.ClientKey, existingClient._id.toString());
if (typeof temp !== 'string') {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '524',
info: 'Error when encrypting CardExpiry.'
},
'WARNING',
('Encryption V3 error: CardExpiry, Code: ' + temp.code.toString() + ' (' + temp.message + ').'));
return;
}
newCard.CardExpiryEncrypted = temp;
/**
* CardValidFrom
*/
if (receivedObject.CardValidFrom) {
temp = utils.encryptDataV3(receivedObject.CardValidFrom, receivedObject.ClientKey, existingClient._id.toString());
if (typeof temp !== 'string') {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '524',
info: 'Error when encrypting CardValidFrom.'
},
'WARNING',
('Encryption V3 error: CardValidFrom, Code: ' + temp.code.toString() + ' (' + temp.message + ').'));
return;
}
newCard.CardValidFromEncrypted = temp;
}
/**
* IssueNumber
*/
if (receivedObject.IssueNumber) {
temp = utils.encryptDataV3(receivedObject.IssueNumber, receivedObject.ClientKey, existingClient._id.toString());
if (typeof temp !== 'string') {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '524',
info: 'Error when encrypting IssueNumber.'
},
'WARNING',
('Encryption V3 error: IssueNumber, Code: ' + temp.code.toString() + ' (' + temp.message + ').'));
return;
}
newCard.IssueNumberEncrypted = temp;
}
newCard.AccountType = 'Credit/Debit Payment Card';
newCard.VendorAccountName = 'Credit/Debit Card';
newCard.BillingAddress = receivedObject.BillingAddress;
var cardDetails = utils.identifyCard(receivedObject.CardPAN);
newCard.VendorID = cardDetails.type;
newCard.IconLocation = cardDetails.icon;
newCard.ReceivingAccount = 0;
newCard.PaymentsAccount = 1;
newCard.BalanceAvailable = 0;
newCard.Balance = null;
newCard.LastUpdate = new Date();
/**
* Tokenise the card to check it is valid. The default method can be set in config.
* If the default method is 'None' then the details will simply be stored; the assumption is that no checking need
* be done at this stage.
* Entry of a demo card will show the verification provider as 'Demo'.
*/
var provider = config.verificationProvider;
if (receivedObject.CardPAN === config.demoCardPAN) {
provider = 'Demo';
}
newCard.AcquirerName = provider;
/**
* Use Worldpay to tokenise the card.
*/
if (provider === 'Worldpay') {
/**
* Build the card details for the acquirer to tokenise
* This is just an extract from the receivedObject
*/
const tokeniseDetails = _.pick(
receivedObject,
[
'NameOnAccount',
'CardPAN',
'CVV',
'CardExpiry',
'CardValidFrom',
'IssueNumber'
]
);
/**
* make the request to tokenise
*/
acquirers.tokeniseCard(
provider,
tokeniseDetails,
receivedObject.ClientKey,
existingClient._id.toString()
).then((cardDetails) => {
/**
* Update the account object with the new details
*/
const updatedCard = _.assign(
{},
newCard,
cardDetails
);
/**
* Add the card to the Account collection.
*/
mainDB.addObject(mainDB.collectionAccount, updatedCard, undefined, false, function(err, objectAdded) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '529',
info: 'Database offline.'
});
return;
}
/**
* Respond to client.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10012',
info: 'Card added (tokenised with Worldpay).',
AccountID: objectAdded[0]._id
},
'INFO',
('New card account ID ' + objectAdded[0]._id + ' added (Worldpay ID: ' + ').'));
});
}).catch((err) => {
/**
* Report an appropriate error to the acquirer not tokenising properly
*/
if (err.name === acquirers.ERRORS.ACQUIRER_DOWN) {
worldpay.commsFailure('AddCard.process');
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '527',
info: 'Cannot connect to verifying bank (Worldpay); card addition failed.'
},
'ERROR');
return;
} else if (err.name === acquirers.ERRORS.TOKEN_ENCRYPTION_FAILED) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '524',
info: 'Error when encrypting Token.'
},
'WARNING',
('Encryption V3 error when encrypting token. ' + err.info));
return;
} else {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '528',
info: 'Card tokenisation failed.'
},
'WARNING',
('Cannot tokenise card. ' + err.name + ':' + err.message));
return;
}
});
}
/**
* Do not tokenise the card - simply store the details.
* This can either be where the
*/
else if ((provider === 'None') || (provider === 'Demo')) {
/**
* Data is simply stored; either a demo card or verification disabled.
*/
if (provider === 'Demo') {
newCard.ClientAccountName += ' (Demo)';
}
/**
* Add the card to the Account collection.
*/
mainDB.addObject(mainDB.collectionAccount, newCard, undefined, false, function(err, objectAdded) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '526',
info: 'Database offline.'
});
return;
}
/**
* Respond to client.
*/
var variedInfo = '';
if (provider === 'Demo') {
variedInfo = '(demo account)';
} else {
variedInfo = '(not tokenised)';
}
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10012',
info: 'Card added ' + variedInfo + '.',
AccountID: objectAdded[0]._id
},
'INFO',
('New card account ID ' + objectAdded[0]._id + ' added ' + variedInfo + '.'));
});
}
/**
* Default case - system disabled.
*/
else {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '525',
info: 'Card addition disabled.'
},
'WARNING');
}
});
});
};

View File

@ -0,0 +1,341 @@
/**
* @fileOverview Node.js AddImage Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Adds an image to the image database.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/image_commands/addimage/}
*/
// Legacy code line length disable.
// jscs:disable maximumLineLength
//jshint -W101
/**
* Includes
*/
var moment = require('moment');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var log = require(global.pathPrefix + 'log.js');
var mailer = require(global.pathPrefix + 'mailer.js');
var auth = require(global.pathPrefix + 'auth.js');
var credorax = require(global.pathPrefix + 'credorax.js');
var mongodb = require('mongodb');
var gm = require('gm');
var fs = require('fs');
var config = require(global.configFile);
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Create a buffer and write the image to a temporary disk file.
*/
var buf = new Buffer(receivedObject.ImageFile, 'base64');
var tempFileName = moment().format('YYYYMMDDTHHmmssSSS') + utils.randomCode(utils.fullAlphaNumeric, 14);
fs.writeFile((config.temporaryDirectory + tempFileName), buf, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '284',
info: 'Internal server error.'
},
'CRITICAL',
('File creation error: ' + err.message));
return;
}
/**
* Check this is a valid image file.
*/
gm((config.temporaryDirectory + tempFileName)).format(function(err, fileFormat) {
if ((err) || ((fileFormat !== 'PNG') && (fileFormat !== 'JPEG')) ||
((fileFormat === 'PNG') && (receivedObject.FileType !== 'PNG')) ||
((fileFormat === 'JPEG') && (receivedObject.FileType !== 'JPG'))) {
/**
* Add more detail.
*/
var furtherInfo = '';
if (err) {
furtherInfo = err;
} else if ((fileFormat !== 'PNG') && (fileFormat !== 'JPEG')) {
furtherInfo = 'Not a PNG or JPEG file';
} else {
furtherInfo = 'File format is not that specified in FileType';
}
/**
* Let the user know the image file is invalid.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '285',
info: 'Invalid image file.'
},
'WARNING',
('Invalid image file (' + furtherInfo + ').'));
return;
}
/**
* Check that the file size is reasonable.
*/
gm((config.temporaryDirectory + tempFileName)).size(function(err, fileSize) {
if ((err) || (fileSize.width > utils.ImageWidthMax) ||
(fileSize.height > utils.ImageHeightMax) ||
(fileSize.width !== fileSize.height)) {
/**
* Add more detail.
*/
var furtherInfo = '';
if (err) {
furtherInfo = err;
} else if (fileSize.width !== fileSize.height) {
furtherInfo = 'The image is not square';
} else if (fileSize.width > utils.ImageWidthMax) {
furtherInfo = 'File is over ' + utils.ImageWidthMax + ' pixels wide';
} else {
furtherInfo = 'File is over ' + utils.ImageHeightMax + ' pixels high';
}
/**
* Let the user know the image file is invalid.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '286',
info: 'Image dimensions invalid.'
},
'WARNING',
('Image dimensions invalid (' + furtherInfo + ').'));
return;
}
/**
* Image is valid. Push image to Bluemix.
*/
config.bluemixContainer.createObject(tempFileName, receivedObject.ImageFile)
.then(function() {
/**
* Create new database entry.
*/
var timestamp = new Date();
var newImage = {};
newImage.ClientID = existingClient.ClientID;
newImage.ImageFile = tempFileName;
newImage.ImageType = receivedObject.ImageType;
newImage.FileType = receivedObject.FileType;
newImage.ImageReported = 0;
newImage.LastUpdate = timestamp;
/**
* Write the image and get the object ID back.
*/
mainDB.addObject(mainDB.collectionImages, newImage, undefined, false, function(err, imageAdded) {
if (err) { // Unable to store info.
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '218',
info: 'Database offline.'
});
return;
}
/**
* Image successfully added. Update the client record.
*/
var ImageID = mongodb.ObjectID(imageAdded[0]._id).toString();
var Selfie = existingClient.Selfie;
if (receivedObject.ImageType === 'Selfie') {
Selfie = ImageID;
}
var CompanyLogo = existingClient.Merchant[0].CompanyLogo;
if (receivedObject.ImageType === 'CompanyLogo0') {
CompanyLogo = ImageID;
}
var newLastVersion = existingClient.LastVersion + 1;
/**
* Update the client record.
*/
mainDB.updateObject(mainDB.collectionClient,
{ClientID: existingClient.ClientID}, {
$set: {
Selfie: Selfie,
'Merchant.0.CompanyLogo': CompanyLogo,
LastUpdate: timestamp,
LastVersion: newLastVersion
}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '219',
info: 'Database offline.'
});
return;
}
/**
* Delete the temporary file.
*/
fs.unlink((config.temporaryDirectory + tempFileName), function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '287',
info: 'Internal server error.'
},
'CRITICAL',
('File deletion error: ' + err.message));
return;
}
/**
* Return code to user.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10031',
info: 'Image added.',
ImageRef: ImageID
},
'INFO',
('New ' + receivedObject.FileType + ' image added (' + fileSize.width +
'x' + fileSize.height + ', ID ' + ImageID + ', S3 ' + tempFileName + ').'));
/**
* Launch async cleanup task to remove old image if it was never used.
* This can be slow so is launched independently of image addition.
*/
var searchFor = '';
if (receivedObject.ImageType === 'Selfie') {
searchFor = existingClient.Selfie;
} else {
searchFor = existingClient.Merchant[0].CompanyLogo;
}
/**
* Protect the default images.
*/
if ((searchFor === config.defaultSelfie) || (searchFor === config.defaultCompanyLogo0)) {
log.system(
'INFO',
'Default image detected (not deleted).',
'AddImage.process',
'',
'System',
'127.0.0.1');
return;
}
mainDB.findOneObject(mainDB.collectionTransactionHistory, {OtherImage: searchFor},
undefined,
false,
function(err, oldTransaction) {
if (!err) {
/**
* Database module writes its own errors.
*/
if (oldTransaction) {
log.system(
'INFO',
('Old ' + receivedObject.FileType + ' image in use (ID ' + searchFor +
'). Not deleted.'),
'AddImage.process',
'',
'System',
'127.0.0.1');
return;
}
/**
* Image not used in a transaction.
* Pull the database reference.
*/
mainDB.findOneObject(mainDB.collectionImages, {_id: mongodb.ObjectID(searchFor)},
undefined,
false,
function(err, oldImage) {
if (!err) {
// Database module writes its own errors.
if (!oldImage) {
log.system(
'WARNING',
('Cannot find Image in database (ID ' + searchFor + ').'),
'AddImage.process',
'',
'System',
'127.0.0.1');
return;
}
/**
* Image is present. Remove from IBM OS.
*/
config.bluemixContainer.deleteObject(oldImage.ImageFile)
.then(function() {
/**
* Remove the Images database entry.
*/
mainDB.removeObject(mainDB.collectionImages, {_id: mongodb.ObjectID(searchFor)}, undefined, false, function(err) {
if (!err) {
log.system(
'INFO',
('Unused ' + receivedObject.FileType + ' deleted from IBM OS (ID ' + searchFor +
', OS ' + oldImage.ImageFile + ').'),
'AddImage.process',
'',
'System',
'127.0.0.1');
}
});
})
.catch(function(err) {
log.system(
'WARNING',
(receivedObject.FileType + ' image not deleted from IBM OS (ID ' + searchFor + ', OS ' +
oldImage.ImageFile + '). ' + err.message),
'AddImage.process',
'',
'System',
'127.0.0.1');
});
}
});
}
});
});
});
});
})
.catch(function(err) {
/**
* Error putting the file on S3.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '463',
info: 'Could not store image.'
},
'WARNING',
('Error uploading image to Bluemix Object Storage (' + err.message + ').'));
});
});
});
});
});
};

View File

@ -0,0 +1,130 @@
/**
* @fileOverview Node.js handler to authorise an outstanding 2FA request for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Richard Taylor
* @see #bridge_server-core
*
* Authorises the identified 2FA request from a web console login request
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/login_auth/authorise2farequest/}
*/
/**
* Includes
*/
var Q = require('q');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.js');
/**
* This authorises an outstanding 2-factor authentication request arising from
* an attempt to log into the web console. The RequestID for this request
* comes from a previous request to Get2FARequest.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Authorise the identified request, if it can be found.
*
* Build the query. The limits are:
* - TargetAccount must match the id of the current client
* - RequestID must match the RequestID
* - Request must not be expired
* - Request must not be already authorised (i.e. no authorised date)
*/
var timestamp = new Date();
var query = {
TargetAccount: existingClient.ClientID,
RequestID: receivedObject.RequestID,
RequestExpiry: {$gt: timestamp},
AuthorisedDate: {$type: 10} // Must exist and be exactly `null` (BSON Type 10)
};
/**
* Define the update
*/
var newExpiry = new Date(timestamp);
newExpiry.setSeconds(
timestamp.getSeconds() + utils.twoFactorRequestExpiry
);
var update = {
$set: {
AuthorisedDate: timestamp,
AuthorisingDeviceID: existingDevice.DeviceUuid,
RequestExpiry: newExpiry,
LastUpdate: timestamp
}
};
/**
* Build the options.
*/
var options = {
upsert: false,
multi: false,
comment: 'authorise2FARequest' // For profiler logs use
};
/**
* Request the object
*/
Q.nfcall(
mainDB.updateObject,
mainDB.collectionTwoFARequests,
query,
update,
options,
false
).then(function(result) {
/**
* Successful query (though it may not have found anything to update)
*/
if (result.result.n === 1) {
/**
* A document was updated, so this is total success
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10067',
info: 'Authorisation successful.'
},
'INFO',
('Authorise2FARequest successful: ' + receivedObject.RequestID));
} else {
/**
* The request ran, but didn't find any documents
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '457',
info: 'Invalid or expired request ID.'
},
'WARNING',
('Invalid or expired request ID: ' + receivedObject.RequestID));
}
}).catch(function() {
/**
* Query failed, most likely from the database
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '456',
info: 'Database offline.'
});
});
}); // End auth.validSession callback
};

View File

@ -0,0 +1,198 @@
/**
* @fileOverview Node.js CancelPaymentRequest Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Cancels a payment request, sets TransactionStatus appropriately.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/payment_commands/cancelpaymentrequest/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var mongodb = require('mongodb');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice) {
if (err) {
return;
}
/**
* Either the payer or payee can cancel the transaction.
*/
//jshint -W074
mainDB.findOneObject(mainDB.collectionTransaction, {_id: mongodb.ObjectID(receivedObject.TransactionID)},
undefined,
false,
function(err, existingTransaction) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '162',
info: 'Database offline.'
});
return;
}
/**
* Check to see if the transaction exists.
*/
if (existingTransaction === null) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '163',
info: 'Cannot find transaction.'
},
'WARNING');
return;
}
/**
* Ensure that it's either the customer or merchant requesting cancellation.
* If this is true it indicated a session timeout.
*/
if (!(((receivedObject.DeviceToken === existingTransaction.CustomerDeviceToken) &&
(receivedObject.SessionToken === existingTransaction.CustomerSessionToken)) ||
((receivedObject.DeviceToken === existingTransaction.MerchantDeviceToken) &&
(receivedObject.SessionToken === existingTransaction.MerchantSessionToken)))) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '164',
info: 'Session timed out.'
},
'WARNING');
return;
}
/**
* User is allowed to cancel the transaction. Default is to update the transaction.
*/
var updateTransaction = false;
var newTransactionStatus;
var newStatusInfo;
switch (existingTransaction.TransactionStatus) {
case 0:
/**
* Just issued.
*/
newTransactionStatus = 10;
newStatusInfo = 'Paycode cancelled before use by customer.';
updateTransaction = true;
break;
case 1:
/**
* Code claimed.
*/
newTransactionStatus = 11;
if (receivedObject.DeviceToken === existingTransaction.CustomerDeviceToken) {
newStatusInfo = 'Payment process cancelled by customer.';
} else {
newStatusInfo = 'Payment process cancelled by merchant.';
}
updateTransaction = true;
break;
case 2:
/**
* Payment underway.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '165',
info: 'Transfer underway.'
},
'WARNING');
break;
case 3:
/**
* Payment complete.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '166',
info: 'Payment complete.'
},
'WARNING');
break;
default:
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '167',
info: 'Payment already cancelled.'
},
'WARNING');
break;
}
/**
* Update the transaction if needed.
*/
if (updateTransaction) {
var newLastUpdate = new Date();
var newLastVersion = existingTransaction.LastVersion + 1;
mainDB.updateObject(mainDB.collectionTransaction,
{_id: mongodb.ObjectID(receivedObject.TransactionID)}, {
$set: {
TransactionStatus: newTransactionStatus,
StatusInfo: newStatusInfo,
LastUpdate: newLastUpdate,
LastVersion: newLastVersion
}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '168',
info: 'Database offline.'
});
return;
}
/**
* Delete PayCode so it cannot be matched again.
*/
mainDB.removeObject(mainDB.collectionPayCode,
{_id: existingTransaction.PayCodeID}, undefined, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '169',
info: 'Database offline.'
});
return;
}
/**
* Payment successfully cancelled.
*/
let cancelledBy = '';
if (receivedObject.DeviceToken === existingTransaction.CustomerDeviceToken) {
cancelledBy = 'payer.';
} else {
cancelledBy = 'payee.';
}
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10018',
info: ('Transaction cancelled by ' + cancelledBy)
},
'INFO',
('PayCode ' + existingTransaction.PayCode + ' cancelled by ' + cancelledBy));
});
});
}
});
});
};

View File

@ -0,0 +1,101 @@
/**
* @fileOverview Node.js ChangePIN Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Gets a list of all devices associated with a particular client.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/account_commands/changepin/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var config = require(global.configFile);
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice) {
if (err) {
return;
}
/**
* Check the current PIN.
*/
var timestamp = new Date();
auth.checkDevicePIN(receivedObject.DeviceAuthorisation, existingDevice, timestamp, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: err.code.toString(),
info: err.message
},
'WARNING');
return;
}
/**
* Encrypt and store the new PIN. Create a new salt as well.
*/
auth.encryptPBKDF2(receivedObject.NewAuthorisation, function(err, newSalt, newHash) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '416',
info: ('Error encrypting new PIN: ' + err)
},
'WARNING');
return;
}
/**
* Success. Update the database with the new PIN.
*/
var newDeviceAuthorisation = config.pinCryptoVersion + '::' + newHash;
mainDB.updateObject(mainDB.collectionDevice, {DeviceToken: existingDevice.DeviceToken},
{
$set: {
DeviceSalt: newSalt,
DeviceAuthorisation: newDeviceAuthorisation,
LastUpdate: timestamp
},
$inc: {LastVersion: 1}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '417',
info: 'Database offline.'
});
return;
}
/**
* Success!
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10057',
info: 'PIN changed.'
},
'INFO');
});
});
});
});
};

View File

@ -0,0 +1,121 @@
/**
* @fileOverview Node.js ChangePassword Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Allows the user to change their password if they know the old one.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/account_commands/changepassword/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var mailer = require(global.pathPrefix + 'mailer.js');
var config = require(global.configFile);
var templates = require(global.pathPrefix + '../utils/templates.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Check the current Password.
*/
var timestamp = new Date();
auth.checkClientPassword(receivedObject.Password, existingClient, timestamp, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: err.code.toString(),
info: err.message
},
'WARNING');
return;
}
/**
* Encrypt and store the new Password. Create a new salt as well.
*/
auth.encryptPBKDF2(receivedObject.NewPassword, function(err, newSalt, newHash) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '419',
info: ('Error encrypting new Password: ' + err)
},
'WARNING');
return;
}
/**
* Tell the user that a new device is being added - send an e-mail.
*/
var htmlEmail = templates.render('password-changed', {
DeviceNumber: existingDevice.DeviceNumber
});
mailer.sendEmail(null, existingClient.ClientName, 'Bridge Password Changed',
htmlEmail, 'ChangePassword.process', function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '421',
info: 'Unable to send e-mail.'
},
'ERROR');
return;
}
/**
* Success. Update the database with the new Password.
*/
var newPassword = config.passwordCryptoVersion + '::' + newHash;
mainDB.updateObject(mainDB.collectionClient, {ClientID: existingClient.ClientID},
{
$set: {
ClientSalt: newSalt,
Password: newPassword,
LastUpdate: timestamp
},
$inc: {LastVersion: 1}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '420',
info: 'Database offline.'
});
return;
}
/**
* Success!
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10058',
info: 'Password changed.'
},
'INFO');
});
});
});
});
});
};

View File

@ -0,0 +1,243 @@
/**
* @fileOverview Node.js Confirm Invoice Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Richard Taylor
* @see #bridge_server-core
*
* Confirms the transaction for the customer.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/merchant_commands/confirm_invoice/}
*/
'use strict';
/**
* Includes.
*/
const debug = require('debug')('app:ConfirmInvoice');
const auth = require(global.pathPrefix + 'auth.js');
const utils = require(global.pathPrefix + 'utils.js');
const impl = require(global.pathPrefix + '../impl/confirm_transaction.js');
const acqErrors = require(global.pathPrefix + '../utils/acquirers/acquirer_errors.js');
const responsesUtils = require(global.pathPrefix + '../utils/responses.js');
/**
* The customer confirms the invoice, and pays it using their selected account.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Collect the data we need
*/
const data = {
TransactionID: receivedObject.InvoiceID,
TipAmount: 0,
ClientKey: receivedObject.ClientKey,
initialStatus: utils.TransactionStatus.PENDING_INVOICE,
ipAddress: functionInfo.remote,
Latitude: receivedObject.Latitude,
Longitude: receivedObject.Longitude,
AccountID: receivedObject.AccountID
};
/**
* Call the base implementation
*/
impl.confirmTransaction(existingClient, existingDevice, data).then(() => {
auth.respond(
res,
200,
existingDevice,
hmacData,
functionInfo,
{
code: '10074',
info: 'Invoice confirmed.',
TransactionID: receivedObject.InvoiceID
},
'INFO',
'Invoice ID <' + receivedObject.InvoiceID + '> confirmed by acquirer.'
);
}).catch((error) => {
debug('Error:', error);
//
// Define the responses
//
const responses = [
//
// Errors when reading from the database
//
[
'MongoError',
200, 510, 'Database Offline', true
],
//
// Errors from the main implementation
//
[
impl.ERRORS.TRANSACTION_NOT_FOUND,
200, 496, 'Invalid InvoiceID'
],
[
impl.ERRORS.MERCHANT_NOT_FOUND,
200, 551, 'Merchant information not found'
],
[
impl.ERRORS.CLIENT_DETAILS_NOT_SET,
200, 552, 'User details not set'
],
[
impl.ERRORS.MERCHANT_DETAILS_NOT_SET,
200, 553, 'Merchant details not set'
],
[
impl.ERRORS.CLIENT_KYC_INCOMPLETE,
200, 554, 'Additional user information required'
],
[
impl.ERRORS.MERCHANT_KYC_INCOMPLETE,
200, 555, 'Additional merchant information required'
],
[
impl.ERRORS.TRANSACTION_TOTAL_TOO_HIGH,
200, 310, ('Total above current limit of ' + utils.transactionMaxText)
],
[
impl.ERRORS.TRANSACTION_TOTAL_TOO_LOW,
200, 311, ('Total below current limit of ' + utils.transactionMinText)
],
[
impl.ERRORS.FAILED_SET_CONFIRMED,
200, 510, 'Database offline'
],
[
impl.ERRORS.FAILED_SET_COMPLETE,
200, 506, 'Database offline'
],
[
impl.ERRORS.FAILED_ADD_HISTORY,
200, 507, 'Database offline'
],
[
impl.ERRORS.FAILED_UPDATE_CUSTOMER_BALANCE,
200, 508, 'Database offline'
],
[
impl.ERRORS.FAILED_UPDATE_MERCHANT_BALANCE,
200, 509, 'Database offline'
],
[
impl.ERRORS.MERCHANT_ACCOUNT_NOT_FOUND,
200, 497, 'Invalid Merchant AccountID'
],
[
impl.ERRORS.CUSTOMER_ACCOUNT_NOT_FOUND,
200, 494, 'Invalid Customer AccountID'
],
[
impl.ERRORS.MERCHANT_ACCOUNT_NOT_RECEIVING,
200, 498, 'Not a receiving account'
],
[
impl.ERRORS.CUSTOMER_ACCOUNT_NOT_PAYMENTS,
200, 495, 'Not a payments account'
],
//
// Errors from the acquirer
//
[
acqErrors.UNKNOWN_ACQUIRER,
200, 532, 'Merchant acquirer unknown',
true
],
[
acqErrors.INVALID_COMBINATION,
200, 536, 'Invalid payment type',
true
],
[
acqErrors.ACQUIRER_DOWN,
200, 533, 'Cannot connect to acquirer',
true
],
[
acqErrors.INVALID_MERCHANT_NAME,
200, 534, 'Invalid Merchant account details.',
true
],
[
acqErrors.INVALID_MERCHANT_ACCOUNT_DETAILS,
200, 535, 'Receiving account information unreadable',
true
],
[
acqErrors.INVALID_CARD_DETAILS,
200, 536, 'Payment account information unreadable',
true
],
[
acqErrors.ACQUIRER_UNKNOWN_ERROR,
200, 537, 'Error processing payment',
true
],
[
acqErrors.ACQUIRER_BAD_REQUEST,
200, 538, 'Error processing payment',
true
],
[
acqErrors.ACQUIRER_INVALID_PAYMENT_DETAILS,
200, 540, 'Invalid payment details',
true
],
[
acqErrors.ACQUIRER_UNAUTHORIZED,
200, 541, 'Merchant account unauthorized with acquirer',
true
],
[
acqErrors.ACQUIRER_MERCHANT_DISABLED,
200, 542, 'Merchant account disabled with acquirer',
true
],
[
acqErrors.ACQUIRER_INTERNAL_SERVER_ERROR,
200, 543, 'Error processing payment',
true
],
[
acqErrors.CARD_EXPIRED,
200, 544, 'Card has expired',
true
],
[
acqErrors.PAYMENT_FAILED_UNSPECIFIED,
200, 545, 'Unspecified error',
true
]
];
const responseHandler = new responsesUtils.ErrorResponses(responses);
responseHandler.respondAuth(
res, error, existingDevice, hmacData, functionInfo, 'INFO'
);
});
});
};

View File

@ -0,0 +1,241 @@
/**
* @fileOverview Node.js ConfirmTransaction Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Confirms the transaction for the customer.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/payment_commands/confirmtransaction/}
*/
'use strict';
// Legacy code line length disable.
// jscs:disable maximumLineLength
//jshint -W101
/**
* Includes.
*/
const log = require(global.pathPrefix + 'log.js');
const auth = require(global.pathPrefix + 'auth.js');
const utils = require(global.pathPrefix + 'utils.js');
const impl = require(global.pathPrefix + '../impl/confirm_transaction.js');
const acqErrors = require(global.pathPrefix + '../utils/acquirers/acquirer_errors.js');
const responsesUtils = require(global.pathPrefix + '../utils/responses.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
const data = {
TransactionID: receivedObject.TransactionID,
TipAmount: receivedObject.TipAmount,
ClientKey: receivedObject.ClientKey,
initialStatus: utils.TransactionStatus.CLAIMED,
ipAddress: functionInfo.remote
};
/**
* Call the base implementation
*/
impl.confirmTransaction(existingClient, existingDevice, data).then(() => {
auth.respond(
res,
200,
existingDevice,
hmacData,
functionInfo,
{
code: '10023',
info: 'Transaction confirmed by acquirer.'
},
'INFO',
'Transaction ID <' + receivedObject.TransactionID + '> confirmed by acquirer.'
);
}).catch((error) => {
//
// Define the responses
//
const responses = [
//
// Errors when reading from the database
//
[
'MongoError',
200, 182, 'Database Offline', true
],
//
// Errors from the main implementation
//
[
impl.ERRORS.TRANSACTION_NOT_FOUND,
200, 183, 'Invalid TransactionID'
],
[
impl.ERRORS.MERCHANT_NOT_FOUND,
200, 546, 'Merchant information not found'
],
[
impl.ERRORS.CLIENT_DETAILS_NOT_SET,
200, 547, 'User details not set'
],
[
impl.ERRORS.MERCHANT_DETAILS_NOT_SET,
200, 548, 'Merchant\'s user details not set'
],
[
impl.ERRORS.CLIENT_KYC_INCOMPLETE,
200, 549, 'Additional user information required'
],
[
impl.ERRORS.MERCHANT_KYC_INCOMPLETE,
200, 550, 'Additional user information for merchant required'
],
[
impl.ERRORS.TRANSACTION_TOTAL_TOO_HIGH,
200, 310, ('Total above current limit of ' + utils.transactionMaxText)
],
[
impl.ERRORS.TRANSACTION_TOTAL_TOO_LOW,
200, 311, ('Total below current limit of ' + utils.transactionMinText)
],
[
impl.ERRORS.FAILED_SET_CONFIRMED,
200, 185, 'Database offline'
],
[
impl.ERRORS.FAILED_SET_COMPLETE,
200, 185, 'Database offline'
],
[
impl.ERRORS.FAILED_ADD_HISTORY,
200, 188, 'Database offline'
],
[
impl.ERRORS.FAILED_UPDATE_CUSTOMER_BALANCE,
200, 239, 'Database offline'
],
[
impl.ERRORS.FAILED_UPDATE_MERCHANT_BALANCE,
200, 240, 'Database offline'
],
[
impl.ERRORS.MERCHANT_ACCOUNT_NOT_FOUND,
200, 209, 'Database offline'
],
[
impl.ERRORS.CUSTOMER_ACCOUNT_NOT_FOUND,
200, 208, 'Database offline'
],
[
impl.ERRORS.MERCHANT_ACCOUNT_NOT_RECEIVING,
200, 211, 'Invalid merchant AccountID'
],
[
impl.ERRORS.CUSTOMER_ACCOUNT_NOT_PAYMENTS,
200, 210, 'Invalid customer AccountID'
],
//
// Errors from the acquirer
//
[
acqErrors.UNKNOWN_ACQUIRER,
200, 532, 'Merchant acquirer unknown',
true
],
[
acqErrors.INVALID_COMBINATION,
200, 539, 'Invalid payment type',
true
],
[
acqErrors.ACQUIRER_DOWN,
200, 533, 'Cannot connect to acquirer',
true
],
[
acqErrors.INVALID_MERCHANT_NAME,
200, 534, 'Invalid merchant details',
true
],
[
acqErrors.INVALID_MERCHANT_ACCOUNT_DETAILS,
200, 535, 'Receiving account information unreadable',
true
],
[
acqErrors.INVALID_CARD_DETAILS,
200, 536, 'Payment account information unreadable',
true
],
[
acqErrors.ACQUIRER_UNKNOWN_ERROR,
200, 537, 'Error processing payment',
true
],
[
acqErrors.ACQUIRER_BAD_REQUEST,
200, 538, 'Error processing payment',
true
],
[
acqErrors.ACQUIRER_INVALID_PAYMENT_DETAILS,
200, 540, 'Invalid payment type for merchant acquirer',
true
],
[
acqErrors.ACQUIRER_UNAUTHORIZED,
200, 541, 'Merchant account unauthorized with acquirer',
true
],
[
acqErrors.ACQUIRER_MERCHANT_DISABLED,
200, 542, 'Merchant account disabled with acquirer',
true
],
[
acqErrors.ACQUIRER_INTERNAL_SERVER_ERROR,
200, 543, 'Error processing payment',
true
],
[
acqErrors.CARD_EXPIRED,
200, 544, 'Card Expired',
true
],
[
acqErrors.PAYMENT_FAILED_UNSPECIFIED,
200, 545, 'Error processing payment',
true
]
];
const responseHandler = new responsesUtils.ErrorResponses(responses);
responseHandler.respondAuth(
res, error, existingDevice, hmacData, functionInfo, 'INFO'
);
});
});
};

View File

@ -0,0 +1,97 @@
/**
* @fileOverview Node.js DeleteAccount Handler for Bridge Pay
* @preserve Copyright 2017 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Deletes the account ID sent through.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/account_commands/deleteaccount/}
*/
/**
* Includes
*/
const auth = require(global.pathPrefix + 'auth.js');
const acquirerUtils = require(global.pathPrefix + '../utils/acquirers/acquirer.js');
const responsesUtils = require(global.pathPrefix + '../utils/responses.js');
const deleteAccountImpl = require(global.pathPrefix + '../impl/delete_account.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - Detail on the calling function {!name, !remote, !port}.
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
//
// Call the implementation and return the properly formatted
//
return deleteAccountImpl.deleteAccount(existingClient.ClientID, receivedObject.AccountID)
.then(() => {
/**
* Success!
*/
return auth.respond(res, 200, existingDevice, hmacData, functionInfo,
{
code: '10016',
info: 'Account successfully deleted.'
},
'INFO',
('Account deleted (ID ' + receivedObject.AccountID + ').')
);
})
.catch((error) => {
const responses = [
[
deleteAccountImpl.ERRORS.RELATED_INVOICES,
200, 30108, 'Account can\'t be deleted while related active invoices exist', true
],
[
// AccountID is not valid (or doesn't belong to *me*)
deleteAccountImpl.ERRORS.NOT_FOUND,
200, 153, 'No account match.', true
],
[
// AccountID is not valid (or doesn't belong to *me*)
deleteAccountImpl.ERRORS.FAILED_UPDATE,
200, 153, 'No account match.', true
],
[
deleteAccountImpl.ERRORS.LOCKED,
200, 243, 'Account locked.', true
],
[
acquirerUtils.ERRORS.UNKNOWN_ACQUIRER,
200, 241, 'Invalid VendorID or AcquirerName.', true
],
[
acquirerUtils.ERRORS.ACQUIRER_DOWN,
200, 244, 'Cannot connect to acquiring bank', true
],
[
'MongoError',
200, 152, 'Database offline.'
]
];
const responseHandler = new responsesUtils.ErrorResponses(responses);
return responseHandler.respondAuth(
res, error, existingDevice, hmacData, functionInfo, 'INFO'
);
})
.done();
});
};

View File

@ -0,0 +1,158 @@
/**
* @fileOverview Node.js Delete Address Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Deletes an address which is associated with the current client.
* This call will fail if the address is used with any accounts.
*
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/account_commands/deleteaddress/}
*/
/**
* Includes
*/
var mongodb = require('mongodb');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Check to see if this address is in use.
*/
mainDB.collectionAccount.find(
{
ClientID: existingClient.ClientID,
BillingAddress: receivedObject.AddressID,
AccountStatus: {$bitsAllClear: utils.AccountDeleted}
},
{
_id: 1,
ClientID: 1
}
).toArray(function(err, accounts) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '385',
info: 'Database offline.'
});
return;
}
/**
* Check if any addresses match the search query.
*/
if (accounts.length > 0) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '386',
info: 'Address still in use.'
},
'WARNING',
('Address still in use in ' + accounts.length + ' account(s).'));
return;
}
/**
* Get the full address detail to back up.
*/
mainDB.findOneObject(mainDB.collectionAddresses,
{
_id: mongodb.ObjectID(receivedObject.AddressID),
ClientID: existingClient.ClientID
},
undefined,
false,
function(err, existingAddress) {
/**
* Check for errors.
*/
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '387',
info: 'Database offline.'
});
return;
}
/**
* Check if any addresses match the search query.
*/
if (!existingAddress) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '388',
info: 'Cannot find address to delete.'
},
'WARNING');
return;
}
/**
* Clean up address.
*/
existingAddress.AddressID = existingAddress._id.toString();
delete existingAddress._id;
existingAddress.LastUpdate = new Date();
existingAddress.LastVersion += 1;
/**
* Store to AddressArchive database.
*/
mainDB.addObject(mainDB.collectionAddressArchive, existingAddress, undefined, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '389',
info: 'Database offline.'
});
return;
}
/**
* Delete from Address database.
*/
mainDB.removeObject(mainDB.collectionAddresses, {_id: mongodb.ObjectID(receivedObject.AddressID)},
undefined, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '390',
info: 'Database offline.'
});
return;
}
/**
* Address successfully deleted.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10054',
info: 'Address deleted.'
},
'INFO');
});
});
});
});
});
};

View File

@ -0,0 +1,162 @@
/**
* @fileOverview Node.js Delete Device Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Deletes a particular device if it belongs to the current client.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/registration_commands/deletedevice/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.js');
var mongodb = require('mongodb');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Local variables
*/
var timestamp = new Date();
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Check that the user has permission to delete the device.
*/
auth.checkClientPassword(receivedObject.Password, existingClient, timestamp, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: err.code.toString(),
info: err.message
},
'WARNING');
return;
}
/**
* Find the device.
*/
mainDB.findOneObject(mainDB.collectionDevice,
{
_id: mongodb.ObjectId(receivedObject.DeviceIndex),
ClientID: existingClient.ClientID
},
undefined,
false,
function(err, device) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '369',
info: 'Database offline.'
});
return;
}
/**
* No hits from database.
*/
if (!device) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '370',
info: 'Invalid device or device does not belong to client.'
},
'WARNING');
return;
}
/**
* Check that the selected device has not been locked by Comcarde.
*/
if (utils.bitsAllSet(device.DeviceStatus, utils.DeviceBarredMask)) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '371',
info: 'The device has been put on hold by Comcarde.'
},
'WARNING');
return;
}
/**
* Cannot delete the device that is being used.
*/
if (device.DeviceNumber === existingDevice.DeviceNumber) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '374',
info: 'Cannot delete the device currently in use.'
},
'WARNING');
return;
}
/**
* Store the old object _id as DeviceIndex
*/
device.DeviceIndex = device._id.toString();
delete device._id;
device.DeviceAuthorisation = '';
device.DeviceSalt = '';
device.CurrentHMAC = '';
device.PendingHMAC = '';
device.LastUpdate = timestamp;
/**
* Write the object to the Archive.
*/
mainDB.addObject(mainDB.collectionDeviceArchive, device, undefined, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '372',
info: 'Database offline.'
});
return;
}
/**
* The device can be safely deleted.
*/
mainDB.removeObject(mainDB.collectionDevice,
{_id: mongodb.ObjectId(receivedObject.DeviceIndex)}, undefined, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '373',
info: 'Database offline.'
});
return;
}
/**
* Success.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10051',
info: 'Device deleted.'
},
'INFO');
});
});
});
});
});
};

View File

@ -0,0 +1,132 @@
/**
* @fileOverview Node.js Delete Message Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Deletes a particular message if it belongs to the current client.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/registration_commands/deletemessage/}
*/
/**
* Includes
*/
var _ = require('lodash');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.js');
var mongodb = require('mongodb');
/**
* Deletes a Message by moving it to the MessageArchive collection
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Find the message, confirming it belongs to this client
*/
var findQuery = {
_id: mongodb.ObjectId(receivedObject.MessageID),
ClientID: existingClient.ClientID
};
mainDB.findOneObject(mainDB.collectionMessages,
findQuery,
undefined,
false,
function(err, message) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '486',
info: 'Database offline.'
});
return;
}
/**
* No hits from database.
*/
if (!message) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '489',
info: 'Invalid message or message does not belong to client.'
},
'WARNING');
return;
}
/**
* Clone the message for copying to the archive.
* Also copy _id to MessageID, remove _id, and updated the
* LastUpdate time.
*/
var messageClone = _.clone(message);
messageClone.MessageID = message._id.toHexString();
delete messageClone._id;
messageClone.LastUpdate = new Date();
/**
* Write the object to the Archive.
*/
mainDB.addObject(
mainDB.collectionMessagesArchive,
messageClone,
undefined,
false,
function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '487',
info: 'Database offline.'
});
return;
}
/**
* The message can be safely deleted.
*/
mainDB.removeObject(
mainDB.collectionMessages,
findQuery,
undefined,
false,
function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '488',
info: 'Database offline.'
});
return;
}
/**
* Success.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo,
{
code: '10073',
info: 'Message deleted.'
},
'INFO');
});
});
});
});
};

View File

@ -0,0 +1,102 @@
/**
* @fileOverview Elevate a session with the email and password.
*
* Requires that you are already logged in on a device.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/login_auth/elevatesession/}
*/
/**
* Includes
*/
const _ = require('lodash');
const Q = require('q');
const authP = require(global.pathPrefix + 'auth-promises.js');
const utils = require(global.pathPrefix + 'utils.js');
/**
* Check the email and password is correct. It also requires that you are logged
* in normally, so we also verify that the given details match those for the
* normal device login.
*
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = async function(res, functionInfo, parameters, receivedObject, hmacData) {
let authDetails = null;
try {
/**
* Check the device session is valid
*/
authDetails = await authP.validSession(
res,
receivedObject.DeviceToken,
receivedObject.SessionToken,
functionInfo,
hmacData
).then((result) => {
return {
existingDevice: result[0],
existingClient: result[1]
};
}).catch(() => Q.reject()); // Error has been handled in auth.ValidSession
/**
* Check the email address sent to us is the same one as the one signed in to the device
*/
if (authDetails.existingClient.ClientName !== receivedObject.ClientName) {
throw utils.createError(559, 'Invalid ClientName.', 'ElevateSesession');
}
/**
* Check the password is correct for the current user
*/
const timestamp = new Date();
await authP.checkClientPassword(
receivedObject.Password,
authDetails.existingClient,
timestamp
);
/**
* All good so return success
*/
authP.respond(
res,
200,
authDetails.existingDevice,
hmacData,
functionInfo,
{
code: '10079',
info: 'Session Elevated.'
},
'INFO',
'ElevateSession successful'
);
} catch (error) {
/**
* If we have an error then return.
* NOTE: we won't have an error if it was validSession() that failed as
* it handles responding internally.
*/
if (error) {
authP.respond(
res,
200,
_.get(authDetails, 'existingDevice', null),
hmacData,
functionInfo,
{
code: String(_.get(error, 'code', -1)),
info: _.get(error, 'message', 'Unknown error')
},
'ERROR',
'ElevateSession failed'
);
}
}
};

View File

@ -0,0 +1,112 @@
/**
* @fileOverview Node.js Get outstanding 2-factor requests handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Richard Taylor
* @see #bridge_server-core
*
* Gets the client's outstanding 2 factor requests for access to the web console
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/login_auth/get2farequest/}
*/
/**
* Includes
*/
var Q = require('q');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.js');
/**
* Gets any outstanding 2-factor requests that are created when a client tries
* to log in to the web console. These should be responded to by the apps with
* a call to Authorise2FARequest.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Request the latest outstanding 2FA request from the table
*
* Build the query. The limits are:
* - TargetAccount must match the id of the current client
* - Request must not be expired
* - Request must not be already authorised (i.e. no authorised date)
*/
var timestamp = new Date();
var query = {
TargetAccount: existingClient.ClientID,
RequestExpiry: {$gt: timestamp},
AuthorisedDate: {$type: 10} // Must exist and be exactly `null` (BSON Type 10)
};
/**
* Define the projection
*/
var projection = {
_id: 0,
RequestID: 1,
RequestDate: 1,
RequestExpiry: 1,
RequesterDisplayName: 1,
RequesterClientID: 1
};
/**
*
* Build the options. Importantly, this includes a sort by RequestDate
* so that we always get the request that was made most recently (as
* there could be multiple requests that are still active).
*/
var options = {
fields: projection,
sort: {RequestDate: -1},
comment: 'get2FARequest' // For profiler logs use
};
/**
* Request the object
*/
Q.nfcall(
mainDB.findOneObject,
mainDB.collectionTwoFARequests,
query,
options,
false
).then(function(result) {
/**
* Successful query (though it may not have found anything)
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10066',
info: 'Request returned.',
Request: result // Will be null if no requests outstanding
},
'INFO',
('Get2FARequest successful - ' + (result ? 'request pending' : 'no requests')));
}).catch(function() {
/**
* Query failed, most likely from the database
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '454',
info: 'Database offline.'
});
});
}); // End auth.validSession callback
};

View File

@ -0,0 +1,67 @@
/**
* @fileOverview Node.js Get Client Details Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Gets the client's personal details from the client record.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/registration_commands/getclientdetails/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Ensure that the client details are set.
*/
if (!(utils.bitsAllSet(existingClient.ClientStatus, utils.ClientDetailsMask))) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '425',
info: 'No details set.'
},
'WARNING');
return;
}
/**
* Success!
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10060',
info: 'Client details retrieved.',
Title: existingClient.KYC[0].Title,
FirstName: existingClient.KYC[0].FirstName,
LastName: existingClient.KYC[0].LastName,
MiddleNames: existingClient.KYC[0].MiddleNames,
ResidentialAddressID: existingClient.KYC[0].ResidentialAddressID || '',
Gender: existingClient.KYC[0].Gender || ''
},
'INFO');
});
};

View File

@ -0,0 +1,141 @@
/**
* @fileOverview Node.js GetImage Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Retrieves an image from the image database.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/image_commands/getimage/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var config = require(global.configFile);
var mongodb = require('mongodb');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice) {
if (err) {
return;
}
/**
* Check for default selfie.
*/
if (receivedObject.ImageRef === config.defaultSelfie) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10032',
info: 'Image found.',
FileType: 'PNG',
ImageFile: config.defaultSelfieData,
ImageReported: 0
},
'INFO',
('Default image sent (defaultSelfie).'));
return;
}
/**
* Check for default Company Logo.
*/
if (receivedObject.ImageRef === config.defaultCompanyLogo0) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10032',
info: 'Image found.',
FileType: 'PNG',
ImageFile: config.defaultCompanyLogo0Data,
ImageReported: 0
},
'INFO',
('Default image sent (defaultCompanyLogo0).'));
return;
}
/**
* The image needs to be retrieved form the object store. First, get the details from MongoDB.
*/
mainDB.findOneObject(mainDB.collectionImages,
{_id: mongodb.ObjectID(receivedObject.ImageRef)}, undefined, false, function(err, existingImage) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '221',
info: 'Database offline.'
});
return;
}
/**
* Check to see if the image exists.
*/
if (!existingImage) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '222',
info: 'Invalid ImageRef.'
},
'WARNING',
'Client requested an invalid ImageRef.');
return;
}
/**
* Check to see if it has been reported.
*/
var newImageReported = 0;
if (existingImage.ImageReported !== 0) {
newImageReported = 1;
}
/**
* Set up Bluemix transaction.
*/
config.bluemixContainer.getObject(existingImage.ImageFile)
.then(function(object) {
object.load(false)
.then(function(content) {
/**
* All good. Return it to the user.
*/
var fileString = content.toString();
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10032',
info: 'Image found.',
FileType: existingImage.FileType,
ImageFile: fileString,
ImageReported: newImageReported
},
'INFO',
('Image sent (ID ' + receivedObject.ImageRef + ', IBM OS ' + existingImage.ImageFile + ', IR' +
existingImage.ImageReported + ').'));
});
})
.catch(function(err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '289',
info: 'Cannot access image.'
},
'ERROR',
('Cannot get image from IBM OS (' + err.message + ').'));
});
});
});
};

View File

@ -0,0 +1,148 @@
/**
* @fileOverview Returns full details for the selected invoice
* @preserve Copyright 2016 Comcarde Ltd.
* @author Richard Taylor
* @see #bridge_server-core
*
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/merchant_commands/get_invoice/}
*/
/*
* Includes
*/
var _ = require('lodash');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var apiHelpers = require(global.pathPrefix + '../utils/api_helpers.js');
var mongodb = require('mongodb');
/**
* Return the full details for the selected invoice. This only returns invoices
* where the client is the *customer*.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in invoice body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Request the invoice from the database
*
* Build the query. The limits are:
* - ClientID of the invoice must match the current client
* - _id must match the given InvoiceID
*/
var query = {
_id: mongodb.ObjectID(receivedObject.InvoiceID),
CustomerClientID: existingClient.ClientID
};
/**
* Define the projection
*/
var projection = {
_id: 1,
MerchantDisplayName: 1,
MerchantSubDisplayName: 1,
MerchantImage: 1,
TransactionStatus: 1,
MerchantInvoice: 1,
MerchantComment: 1,
MerchantVATNo: 1,
CustomerComment: 1,
RequestAmount: 1,
DueDate: 1,
LastUpdate: 1,
MerchantInvoiceNumber: 1
};
var options = {
fields: projection,
comment: 'GetInvoice' // For profiler logs use
};
/**
* Find the invoice
*/
mainDB.findOneObject(
mainDB.collectionTransaction,
query,
options,
false, // Don't suppress errors
function(err, existingInvoice) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '514',
info: 'Database offline.'
});
return;
}
/**
* Check for no invoices.
*/
if (!existingInvoice) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '515',
info: 'Invalid InvoiceID.'
},
'WARNING');
return;
}
/**
* Add the creation date and rename the fields
*/
existingInvoice.CreationDate = existingInvoice._id.getTimestamp();
var renames = {
_id: 'InvoiceID',
MerchantDisplayName: 'OtherDisplayName',
MerchantSubDisplayName: 'OtherSubDisplayName',
MerchantImage: 'OtherImage'
};
apiHelpers.renameFields(existingInvoice, renames);
//
// Copy just the InvoiceNumber up to the top level
//
if (!_.isUndefined(existingInvoice.MerchantInvoiceNumber)) {
existingInvoice.MerchantInvoiceNumber =
existingInvoice.MerchantInvoiceNumber.InvoiceNumber;
}
//
// Set a value for any missing dates
if (_.isUndefined(existingInvoice.DueDate)) {
existingInvoice.DueDate = '1970-01-01T00:00:00.000Z';
}
auth.respond(
res,
200,
existingDevice,
hmacData,
functionInfo, {
code: '10076',
info: 'Invoice returned.',
InvoiceDetail: existingInvoice
},
'INFO',
'Invoice detail returned (InvoiceID ' + receivedObject.InvoiceID + ').'
);
});
});
};

View File

@ -0,0 +1,121 @@
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Comcarde GetMessage Handler.
//
// Command = GetMessage
// Gets more information on a particular message associated with Client.
// Checked 25/5/2015 KJS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Includes
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var mongodb = require('mongodb');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Request the messages from the database
*
* Build the query. The limits are:
* - ClientID of the message must match the current client
* - _id must match the given MessageID
* - TimeFilter must be before <now>
* - Type must match the request type, if any
*/
var query = {
_id: mongodb.ObjectID(receivedObject.MessageID),
ClientID: existingClient.ClientID,
TimeFilter: {$lte: new Date()}
};
/**
* Define the projection
*/
var projection = {
_id: 1,
Type: 1,
Read: 1,
Reference: 1,
Info: 1,
InfoObject: 1,
LastUpdate: 1
};
var options = {
fields: projection,
comment: 'GetMessage' // For profiler logs use
};
/**
* Find the message
*/
mainDB.findOneObject(
mainDB.collectionMessages,
query,
options,
false,
function(err, existingMessage) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '480',
info: 'Database offline.'
});
return;
}
/**
* Check for no messages.
*/
if (!existingMessage) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '481',
info: 'Invalid MessageID.'
},
'WARNING');
return;
}
/**
* Move _id to MessageID, add the creation date
*/
existingMessage.MessageID = existingMessage._id;
existingMessage.CreationDate = existingMessage._id.getTimestamp();
delete existingMessage._id;
auth.respond(
res,
200,
existingDevice,
hmacData,
functionInfo, {
code: '10071',
info: 'Message returned.',
Message: existingMessage
},
'INFO',
'Message detail returned (MessageID ' + receivedObject.MessageID + ').'
);
});
});
};

View File

@ -0,0 +1,145 @@
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Comcarde GetTransactionDetail Handler.
//
// Command = GetTransactionDetail
// Gets more information on a particular transaction associated with Client.
// Checked 25/5/2015 KJS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Includes
var _ = require('lodash');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var mongodb = require('mongodb');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* If there is no timestamp object, create one.
*/
mainDB.findOneObject(mainDB.collectionTransaction,
{_id: mongodb.ObjectID(receivedObject.TransactionID)}, undefined, false, function(err, existingTransaction) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '191',
info: 'Database offline.'
});
return;
}
/**
* Check for no transactions.
*/
if (!existingTransaction) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '192',
info: 'Invalid TransactionID.'
},
'WARNING');
return;
}
/**
* Customer.
*/
var toReturn = {};
if (existingTransaction.CustomerClientID === existingClient.ClientID) {
toReturn.OtherDisplayName = existingTransaction.MerchantDisplayName;
toReturn.OtherSubDisplayName = existingTransaction.MerchantSubDisplayName;
toReturn.OtherImage = existingTransaction.MerchantImage;
toReturn.TransactionStatus = existingTransaction.TransactionStatus;
toReturn.StatusInfo = existingTransaction.StatusInfo;
toReturn.MerchantInvoice = existingTransaction.MerchantInvoice;
toReturn.MerchantComment = existingTransaction.MerchantComment;
toReturn.MerchantVATNo = existingTransaction.MerchantVATNo;
toReturn.RequestAmount = existingTransaction.RequestAmount;
toReturn.TipAmount = existingTransaction.TipAmount;
toReturn.TotalAmount = existingTransaction.TotalAmount;
toReturn.MyLocation = existingTransaction.CustomerLocation;
toReturn.SaleTime = existingTransaction.SaleTime;
//
// Copy just the InvoiceNumber up to the top level
//
if (!_.isUndefined(existingTransaction.MerchantInvoiceNumber)) {
toReturn.MerchantInvoiceNumber =
existingTransaction.MerchantInvoiceNumber.InvoiceNumber;
}
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10026',
info: 'Detail returned.',
TransactionDetail: toReturn
},
'INFO',
('Transaction detail returned (TransactionID ' + mongodb.ObjectID(existingTransaction._id).toString() + ').'));
return;
}
/**
* Merchant.
*/
if (existingTransaction.MerchantClientID === existingClient.ClientID) {
toReturn.OtherDisplayName = existingTransaction.CustomerDisplayName;
toReturn.OtherSubDisplayName = existingTransaction.CustomerSubDisplayName;
toReturn.OtherImage = existingTransaction.CustomerImage;
toReturn.TransactionStatus = existingTransaction.TransactionStatus;
toReturn.StatusInfo = existingTransaction.StatusInfo;
toReturn.MerchantInvoice = existingTransaction.MerchantInvoice;
toReturn.MerchantComment = existingTransaction.MerchantComment;
toReturn.MerchantVATNo = existingTransaction.MerchantVATNo;
toReturn.RequestAmount = existingTransaction.RequestAmount;
toReturn.TipAmount = existingTransaction.TipAmount;
toReturn.TotalAmount = existingTransaction.TotalAmount;
toReturn.MyLocation = existingTransaction.MerchantLocation;
toReturn.SaleTime = existingTransaction.SaleTime;
//
// Copy just the InvoiceNumber up to the top level
//
if (!_.isUndefined(existingTransaction.MerchantInvoiceNumber)) {
toReturn.MerchantInvoiceNumber =
existingTransaction.MerchantInvoiceNumber.InvoiceNumber;
}
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10026',
info: 'Detail returned.',
TransactionDetail: toReturn
},
'INFO',
('Transaction detail returned (TransactionID ' + mongodb.ObjectID(existingTransaction._id).toString() + ').'));
return;
}
/**
* Didn't find the client name.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '193',
info: 'Invalid ClientID.'
},
'WARNING');
});
});
};

View File

@ -0,0 +1,115 @@
/**
* @fileOverview Node.js GetTransactionHistory Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Gets a high level list of all the transactions associated with a Client.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/account_commands/gettransactionhistory/}
*/
/**
* Includes
*/
var _ = require('lodash');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* If there is no timestamp object, create one.
*/
var timestamp;
if (receivedObject.Timestamp) {
timestamp = new Date(receivedObject.Timestamp);
} else {
timestamp = new Date();
}
/**
* Create the account filter if needed.
*/
var accountFilter = '';
if (receivedObject.AccountID) {
accountFilter = receivedObject.AccountID;
} else {
accountFilter = '';
}
/**
* Search for the requested items.
*/
mainDB.collectionTransactionHistory.find(
{
ClientID: existingClient.ClientID,
AccountID: {$regex: accountFilter},
SaleTime: {$lt: timestamp}
},
{
_id: 0,
TransactionID: 1,
TransactionType: 1,
AccountID: 1,
OtherDisplayName: 1,
OtherSubDisplayName: 1,
OtherImage: 1,
MyLocation: 1,
TotalAmount: 1,
SaleTime: 1,
MerchantInvoiceNumber: 1
}
).skip(receivedObject.Skip).limit(receivedObject.Number).sort({'SaleTime': -1}).toArray(function(err, items) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '187',
info: 'Database offline.'
});
return;
}
//
// Copy just the InvoiceNumber up to the top level
//
for (var i = 0; i < items.length; ++i) {
if (!_.isUndefined(items[i].MerchantInvoiceNumber)) {
items[i].MerchantInvoiceNumber =
items[i].MerchantInvoiceNumber.InvoiceNumber;
}
}
/**
* Success!
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10025',
info: 'History returned.',
count: items.length,
TransactionHistory: items
},
'INFO',
('Transaction history requested from ' + (receivedObject.Skip + 1) + ' to ' +
(receivedObject.Skip + receivedObject.Number) + ' (' + items.length + ' items found).'));
});
});
};

View File

@ -0,0 +1,50 @@
/**
* @fileOverview Node.js GetTransactionUpdate Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Allows a user to check a transaction to see what's happening.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/payment_commands/gettransactionupdate/}
*/
/**
* Includes
*/
var auth = require(global.pathPrefix + 'auth.js');
var impl = require(global.pathPrefix + '../impl/get_transaction_update.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice) {
if (err) {
return;
}
impl.getTransactionUpdate(receivedObject, function(err, result) {
//
// The error or result will be the correct body to send back
// to the caller
//
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, err);
} else {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, result);
}
});
});
};

View File

@ -0,0 +1,61 @@
/**
* @fileOverview Node.js IconCache Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Returns current icon versions.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/image_commands/iconcache/}
*/
/**
* Includes
*/
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var cardIconCache = [
{ImageName: 'AMEX.png', VersionNo: '1'},
{ImageName: 'bridge-card.png', VersionNo: '2'},
{ImageName: 'BRIDGE_MERCHANT.png', VersionNo: '1'},
{ImageName: 'CARTEBLEUE.png', VersionNo: '1'},
{ImageName: 'credorax-account.png', VersionNo: '3'},
{ImageName: 'Dankort.png', VersionNo: '1'},
{ImageName: 'DINERS.png', VersionNo: '1'},
{ImageName: 'Diners-Generic.png', VersionNo: '1'},
{ImageName: 'Discover-card.png', VersionNo: '2'},
{ImageName: 'Electron.png', VersionNo: '1'},
{ImageName: 'Generic-card.png', VersionNo: '1'},
{ImageName: 'JCB.png', VersionNo: '1'},
{ImageName: 'LloydsTSB.png', VersionNo: '2'},
{ImageName: 'MAESTRO.png', VersionNo: '2'},
{ImageName: 'MASTERCARD_CORPORATE_CREDIT.png', VersionNo: '1'},
{ImageName: 'MASTERCARD_CORPORATE_DEBIT.png', VersionNo: '1'},
{ImageName: 'MASTERCARD_CREDIT.png', VersionNo: '1'},
{ImageName: 'MASTERCARD_DEBIT.png', VersionNo: '1'},
{ImageName: 'MIR.png', VersionNo: '1'},
{ImageName: 'RBS.png', VersionNo: '2'},
{ImageName: 'VISA_CORPORATE_CREDIT.png', VersionNo: '1'},
{ImageName: 'VISA_CORPORATE_DEBIT.png', VersionNo: '1'},
{ImageName: 'VISA_CREDIT.png', VersionNo: '1'},
{ImageName: 'VISA_DEBIT.png', VersionNo: '1'},
{ImageName: 'worldpay-account.png', VersionNo: '2'}
];
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
*/
exports.process = function(res, functionInfo) {
auth.respond(res, 200, null, null, functionInfo, {
code: '10017',
info: 'IconCache check successful.',
cache: cardIconCache
},
'INFO',
'Icon cache sent.');
};

View File

@ -0,0 +1,78 @@
/**
* @fileOverview Node.js Image Cache Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Retrieves an image from the image database.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/image_commands/imagecache/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var config = require(global.configFile);
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Build the image cache.
*/
var imageCache = [];
imageCache.push({
ImageType: 'defaultSelfie',
ImageRef: config.defaultSelfie
});
imageCache.push({
ImageType: 'Selfie',
ImageRef: existingClient.Selfie
});
/**
* Add Merchant logos if applicable.
*/
if (existingClient.Merchant[0].MerchantStatus === 1) {
imageCache.push({
ImageType: 'defaultCompanyLogo0',
ImageRef: config.defaultCompanyLogo0
});
imageCache.push({
ImageType: 'CompanyLogo0',
ImageRef: existingClient.Merchant[0].CompanyLogo
});
}
/**
* Return current images from the cache.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10033',
info: 'ImageCache check successful.',
Images: imageCache
},
'INFO',
'Image cache sent.');
});
};

View File

@ -0,0 +1,47 @@
/**
* @fileOverview Node.js KeepAlive Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Simple refresh of the session token.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/login_auth/keepalive/}
*/
/**
* Includes
*/
var auth = require(global.pathPrefix + 'auth.js');
var log = require(global.pathPrefix + 'log.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice) {
if (err) {
return;
}
/**
* Indicate that the KeepAlive was successful.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10028',
info: 'Keep alive successful.'
},
'INFO');
});
};

View File

@ -0,0 +1,281 @@
/**
* @fileOverview Node.js List Accounts Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Returns a list of accounts for the referenced user.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/account_commands/listaccounts/}
*/
'use strict';
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.js');
var config = require(global.configFile);
var anon = require(global.pathPrefix + '../utils/anon.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Check the EULA version.
*/
if (existingClient.EULAVersionAccepted !== config.EULAVersion) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '293',
info: 'EULA must be accepted before continuing.'
},
'WARNING');
return;
}
/**
* Session login successful. Get an account list from the database (includes all data,
* except accounts that were create from the API)
* Cyclomatic complexity warning due to legacy code.
*/
//jshint -W074, -W071
const query = {
ClientID: existingClient.ClientID,
AccountStatus: {
$bitsAllClear: utils.AccountApiCreated
}
};
mainDB.collectionAccount.find(query).toArray(function(err, items) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '121',
info: 'Database offline.'
});
return;
}
/**
* Client has no accounts in the system.
*/
if (items === null) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '122',
info: 'No Items.'
},
'INFO',
'No accounts.');
return;
}
/**
* Filter the account information.
*/
var counter = 0;
var newArray = [];
var newAccount = {};
var issues = [];
/**
* Deal with listing of deleted accounts only.
*/
var listDeleted = 0;
if (parameters.Command === 'ListDeletedAccounts') {
listDeleted = utils.AccountDeleted;
}
/**
* Go through each item and return a subset of information.
*/
while (counter < items.length) {
/**
* Check for Merchant status: only merchants can see receiving accounts.
* Don't show if it is deleted (unless we are asked to show deleted only).
* The disable for JS Hint is because there is a bitwise comparison.
*/
//jshint -W016
if (((existingClient.Merchant[0].MerchantStatus === 1) ||
(items[counter].AccountType !== 'Credit/Debit Receiving Account')) &&
((items[counter].AccountStatus & utils.AccountDeleted) === listDeleted)) {
//jshint +W016
/**
* Select card information.
*/
newAccount = {};
newAccount.AccountID = items[counter]._id;
newAccount.AccountType = items[counter].AccountType;
newAccount.ClientAccountName = items[counter].ClientAccountName;
newAccount.BillingAddress = items[counter].BillingAddress;
newAccount.VendorID = items[counter].VendorID;
newAccount.VendorAccountName = items[counter].VendorAccountName;
newAccount.NameOnAccount = items[counter].NameOnAccount;
newAccount.ReceivingAccount = items[counter].ReceivingAccount;
newAccount.PaymentsAccount = items[counter].PaymentsAccount;
newAccount.BalanceAvailable = items[counter].BalanceAvailable;
if (newAccount.BalanceAvailable) {
newAccount.Balance = items[counter].Balance;
}
newAccount.IconLocation = items[counter].IconLocation;
newAccount.Integrity = items[counter].Integrity;
/**
* Watch out for merchant images should the person have previously been a Merchant.
*/
if (existingClient.Merchant[0].MerchantStatus === 0) {
if ((items[counter].UserImage === 'defaultCompanyLogo0') ||
(items[counter].UserImage === 'CompanyLogo0')) {
newAccount.UserImage = 'defaultSelfie';
} else {
newAccount.UserImage = items[counter].UserImage;
}
} else {
newAccount.UserImage = items[counter].UserImage;
}
/**
* Pull and verify information and account access.
* Note that only the *Encrypted fields (e.g. CardPANEncrypted) store the actual details.
* No sensitive information is ever stored as plain text.
*/
issues = [];
if (newAccount.AccountType === 'Credit/Debit Payment Card') {
/**
* Check the card details using the keys.
*/
var CardPANEncryptedResult = null;
var CardValidFromEncryptedResult = null;
var CardExpiryEncryptedResult = null;
var IssueNumberEncryptedResult = null;
/**
* Check the card details first.
* Note that detailed error reporting is coming back but it is ignored for the moment.
* Please check the logs for this information.
*/
if (items[counter].CardPANEncrypted !== '') {
CardPANEncryptedResult = utils.checkAccountInformation(
items[counter].CardPANEncrypted,
receivedObject.ClientKey,
existingClient._id.toString(),
items[counter]._id.toString(),
'CardPANEncrypted');
if (CardPANEncryptedResult) {
issues.push(utils.ACCOUNT_ERR.CARD_PAN_DEC);
}
}
if (items[counter].CardValidFromEncrypted !== '') {
CardValidFromEncryptedResult = utils.checkAccountInformation(
items[counter].CardValidFromEncrypted,
receivedObject.ClientKey,
existingClient._id.toString(),
items[counter]._id.toString(),
'CardValidFromEncrypted');
if (CardValidFromEncryptedResult) {
issues.push(utils.ACCOUNT_ERR.CARD_VALID_DEC);
}
}
if (items[counter].CardExpiryEncrypted !== '') {
CardExpiryEncryptedResult = utils.checkAccountInformation(
items[counter].CardExpiryEncrypted,
receivedObject.ClientKey,
existingClient._id.toString(),
items[counter]._id.toString(),
'CardExpiryEncrypted');
if (CardExpiryEncryptedResult) {
// Nesting too deep; functionality here for now for readability.
//jshint -W073
if (CardExpiryEncryptedResult.code === 5) {
issues.push(CardExpiryEncryptedResult.message);
} else {
issues.push(utils.ACCOUNT_ERR.CARD_EXP_DEC);
}
//jshint +W073
}
}
if (items[counter].IssueNumberEncrypted !== '') {
IssueNumberEncryptedResult = utils.checkAccountInformation(
items[counter].IssueNumberEncrypted,
receivedObject.ClientKey,
existingClient._id.toString(),
items[counter]._id.toString(),
'IssueNumberEncrypted');
if (IssueNumberEncryptedResult) {
issues.push(utils.ACCOUNT_ERR.CARD_ISS_DEC);
}
}
/**
* Add the anonymised data.
*/
newAccount.CardPAN = items[counter].CardPAN;
} else if (newAccount.AccountType === 'Bank Account') {
newAccount.AccountNumber = items[counter].AccountNumber;
newAccount.SortCode = items[counter].SortCode;
} else if (newAccount.AccountType === 'Credit/Debit Receiving Account') {
var AcquirerMerchantIDEncryptedResult = null;
if (items[counter].AcquirerMerchantID !== '') {
AcquirerMerchantIDEncryptedResult = utils.decryptDataV1(items[counter].AcquirerMerchantID);
if (typeof AcquirerMerchantIDEncryptedResult === 'string') {
newAccount.AcquirerMerchantID = AcquirerMerchantIDEncryptedResult;
} else {
issues.push(utils.ACCOUNT_ERR.CARD_ISS_DEC);
}
}
}
/**
* Add the responses.
*/
if (issues.length !== 0) {
if (newAccount.Integrity !== null) {
newAccount.Integrity = newAccount.Integrity.concat(issues);
} else {
newAccount.Integrity = issues;
}
}
/**
* Push onto new array subset.
*/
newArray.push(newAccount);
}
/**
* Always increment the counter.
*/
counter++;
}
/**
* Return the account information.
*/
var defaultAccount = existingDevice.DefaultAccount;
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10013',
info: 'Accounts list sent.',
DefaultAccount: defaultAccount,
AccountList: newArray
},
'INFO');
});
//jshint +W074, +W071
});
};

View File

@ -0,0 +1,95 @@
/**
* @fileOverview Node.js List Addresses Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Gets a list of all addresses associated with a particular client.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/account_commands/listaddresses/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var config = require(global.configFile);
var anon = require(global.pathPrefix + '../utils/anon.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Search for the requested addresses.
*/
mainDB.collectionAddresses.find(
{
ClientID: existingClient.ClientID
},
{
_id: 1,
AddressDescription: 1,
BuildingNameFlat: 1,
Address1: 1,
Address2: 1,
Town: 1,
County: 1,
PostCode: 1,
Country: 1,
PhoneNumber: 1,
ResidentTo: 1,
ResidentFrom: 1
}
).toArray(function(err, addresses) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '377',
info: 'Database offline.'
});
return;
}
/**
* Clean up device number detail.
*/
var counter;
for (counter = 0; counter < addresses.length; counter++) {
addresses[counter].AddressID = addresses[counter]._id.toString();
delete addresses[counter]._id;
anon.anonymiseAddress(addresses[counter]);
}
/**
* Success!
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10052',
info: 'Address list returned.',
AddressCount: addresses.length,
MaxAddresses: config.maxAddresses,
Addresses: addresses
},
'INFO',
('Address list requested: ' + addresses.length + ' found (Max. ' + config.maxAddresses + ').'));
});
});
};

View File

@ -0,0 +1,93 @@
/**
* @fileOverview Node.js List Devices Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Gets a list of all devices associated with a particular client.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/registration_commands/listdevices/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var anon = require(global.pathPrefix + '../utils/anon.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Search for the requested items.
*/
mainDB.collectionDevice.find(
{
ClientID: existingClient.ClientID
},
{
_id: 1,
DeviceName: 1,
DeviceNumber: 1,
DeviceStatus: 1,
DeviceHardware: 1,
DeviceSoftware: 1,
LastLoginLocation: 1,
LastLoginIP: 1,
LastLogin: 1,
LastUpdate: 1
}
).toArray(function(err, items) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '362',
info: 'Database offline.'
});
return;
}
/**
* Clean up device number detail.
*/
var counter = 0;
while (counter < items.length) {
items[counter].DeviceIndex = items[counter]._id;
delete items[counter]._id;
anon.anonymiseDevice(items[counter]);
counter++;
}
/**
* Success!
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10050',
info: 'Device list returned.',
DeviceCount: items.length,
MaxDevices: existingClient.MaxDevices,
Devices: items
},
'INFO',
('Device list requested: ' + items.length + ' device(s) found (Max. ' + existingClient.MaxDevices + ').'));
});
});
};

View File

@ -0,0 +1,149 @@
/**
* @fileOverview List the invoices that are outstanding for the logged in user to pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Richard Taylor
* @see #bridge_server-core
*
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/merchant_commands/list_invoices/}
*/
/**
* Includes
*/
var _ = require('lodash');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.js');
var apiHelpers = require(global.pathPrefix + '../utils/api_helpers.js');
/**
* List all invoices for the current client. This is oly the invoices that the
* client is the *customer* for. Invoices where the client is the *merchant*
* can be accessd through the console.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Request the list of invoices from the database
*
* Build the query. The limits are:
* - ClientID of the invoice must match the current client
* - Must have an invoice number
*/
var query = {
CustomerClientID: existingClient.ClientID,
MerchantInvoiceNumber: {
$exists: true
}
};
/**
* If we've been given a last modified date, then we want only invoices
* modified since then.
*/
if (receivedObject.ModifiedSince) {
query.LastUpdate = {
$gte: new Date(receivedObject.ModifiedSince)
};
}
/**
* Define the projection
*/
var projection = {
_id: 1,
TransactionStatus: 1,
MerchantDisplayName: 1,
MerchantSubDisplayName: 1,
MerchantImage: 1,
RequestAmount: 1,
DueDate: 1,
LastUpdate: 1,
MerchantInvoiceNumber: 1
};
/**
* Define the Skip and Number or defaults
*/
var skip = receivedObject.Skip || 0;
var number = receivedObject.number || 30;
/**
* Get all available invoices as an array.
* Note that we don't pass a callback to toArray() so it instead
* returns a promise that we handle with the standard then/catch
*/
mainDB.collectionTransaction
.find(query)
.sort({LastUpdate: -1})
.skip(skip)
.limit(number)
.project(projection)
.toArray()
.then(function(invoices) {
/**
* Successful query (though it may not have found anything)
* Need to iterate through the results, getting the
* CreationDate from the _id, and doing all the renames.
*/
const renames = {
_id: 'InvoiceID',
MerchantDisplayName: 'OtherDisplayName',
MerchantSubDisplayName: 'OtherSubDisplayName',
MerchantImage: 'OtherImage'
};
_.each(invoices, function(invoice) {
invoice.CreationDate = invoice._id.getTimestamp();
apiHelpers.renameFields(invoice, renames);
//
// Copy just the InvoiceNumber up to the top level
//
if (!_.isUndefined(invoice.MerchantInvoiceNumber)) {
invoice.MerchantInvoiceNumber =
invoice.MerchantInvoiceNumber.InvoiceNumber;
}
//
// Fix any missing DueDates
//
if (_.isUndefined(invoice.DueDate)) {
invoice.DueDate = '1970-01-01T00:00:00.000Z';
}
});
auth.respond(res, 200, existingDevice, hmacData, functionInfo,
{
code: '10075',
info: 'Invoices list sent.',
count: invoices.length,
InvoiceList: invoices
},
'INFO',
'ListInvoices successful.'
);
}).catch(function() {
/**
* Query failed, most likely from the database
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '512',
info: 'Database offline.'
});
});
}); // End auth.validSession callback
};

View File

@ -0,0 +1,140 @@
/**
* @fileOverview List a merchant's pre-configured items for use in the POS
* @preserve Copyright 2016 Comcarde Ltd.
* @author Richard Taylor
* @see #bridge_server-core
*
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/merchant_commands/list_items/}
*/
/**
* Includes
*/
var _ = require('lodash');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.js');
/**
* List all items for the current client. This will only be successful if the
* client is an active merchant. If not they will receive an error message.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/*
* Check the user is an active merchant
*/
if (
!_.isArray(existingClient.Merchant) ||
!existingClient.Merchant.length > 0 ||
existingClient.Merchant[0].MerchantStatus !== utils.MerchantStatusActive
) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '470',
info: 'Not a merchant'
},
'WARNING');
return;
}
/**
* Request the list of items from the database
*
* Build the query. The limits are:
* - ClientID of the item must match the current client
* - ItemStatus must be active unless we have a ModifiedSince date
*/
var query = {
ClientID: existingClient.ClientID
};
/**
* If we've been given a last modified date, then we want only items
* modified since then, and we want to include all items (including
* deleted) so that an app can work out the changes.
*
* If we don't, we only want the Active items as it is just a
* basic list of items requested.
*/
if (receivedObject.ModifiedSince) {
query.LastUpdate = {
$gte: new Date(receivedObject.ModifiedSince)
};
} else {
query.ItemStatus = utils.ItemStatusActive;
}
/**
* Define the projection
*/
var projection = {
_id: 1,
BridgeID: 1,
ItemStatus: 1,
ItemCode: 1,
Description: 1,
Tags: 1,
VATCode: 1,
VATRate: 1,
NetAmount: 1,
GrossAmount: 1,
ImageID: 1,
LoyaltyPoints: 1,
LastUpdate: 1
};
/**
* Get all available items as an array.
* Note that we don't pass a callback to toArray() so it instead
* returns a promise that we handle with the standard then/catch
*/
mainDB.collectionItems
.find(query)
.project(projection)
.toArray()
.then(function(items) {
/**
* Successful query (though it may not have found anything)
* Need to iterate through the results, moving _id to ItemID
* before we return them
*/
_.each(items, function(item) {
item.ItemID = item._id;
delete item._id;
});
auth.respond(res, 200, existingDevice, hmacData, functionInfo,
{
code: '10069',
info: 'Items list sent.',
ItemList: items
},
'INFO',
'ListItems successful'
);
}).catch(function() {
/**
* Query failed, most likely from the database
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '469',
info: 'Database offline.'
});
});
}); // End auth.validSession callback
};

View File

@ -0,0 +1,120 @@
/**
* @fileOverview List messages for this client
* @preserve Copyright 2016 Comcarde Ltd.
* @author Richard Taylor
* @see #bridge_server-core
*
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/messaging_commands/listmessages/}
*/
/**
* Includes
*/
var _ = require('lodash');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.js');
/**
* List all messages for the current client, or just those of the Type passed in.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Request the list of messages from the database
*
* Build the query. The limits are:
* - ClientID of the message must match the current client
* - TimeFilter must be before <now>
* - Type must match the request type, if any
*/
var query = {
ClientID: existingClient.ClientID,
TimeFilter: {$lte: new Date()}
};
/**
* If we've been given a Type, filter by that type
*
* If we don't, we only want the Active messages as it is just a
* basic list of messages requested.
*/
if (receivedObject.Type) {
query.Type = receivedObject.Type;
}
/**
* Define the projection
*/
var projection = {
_id: 1,
Type: 1,
Read: 1,
Info: 1
};
/**
* Check the Skip and Number items
*/
var skip = receivedObject.Skip || 0;
var limit = receivedObject.Number || 0;
/**
* Get all available messages as an array.
* Note that we don't pass a callback to toArray() so it instead
* returns a promise that we handle with the standard then/catch
*/
mainDB.collectionMessages
.find(query)
.skip(skip)
.limit(limit)
.project(projection)
.toArray()
.then(function(messages) {
/**
* Successful query (though it may not have found anything)
* Need to iterate through the results, moving _id to MessageID
* and adding a timestamp before we return them
*/
_.each(messages, function(message) {
message.MessageID = message._id;
message.CreationDate = message._id.getTimestamp();
delete message._id;
});
auth.respond(res, 200, existingDevice, hmacData, functionInfo,
{
code: '10070',
info: 'Messages list sent.',
count: messages.length,
MessageList: messages
},
'INFO',
'ListMessages successful'
);
}).catch(function() {
/**
* Query failed, most likely from the database
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '478',
info: 'Database offline.'
});
});
}); // End auth.validSession callback
};

View File

@ -0,0 +1,94 @@
/**
* @fileOverview Node.js Log Out Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Removes the session token and requires a login before proceeding.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/login_auth/logout1/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var mongodb = require('mongodb');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Reset the session token.
*/
var timestamp = new Date();
var logoutData = {};
logoutData.ClientID = existingDevice.ClientID;
logoutData.DeviceToken = existingDevice.DeviceToken;
logoutData.SessionToken = existingDevice.SessionToken;
logoutData.SourceIP = functionInfo.remote;
logoutData.OperationType = 'LogOut';
logoutData.DateTime = timestamp;
mainDB.updateObject(mainDB.collectionDevice, {_id: mongodb.ObjectID(existingDevice._id)}, {
$set: {
SessionToken: '',
SessionTokenExpiry: timestamp
}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '214',
info: 'Database offline.'
});
return;
}
/**
* Store logout data.
*/
mainDB.addObject(mainDB.collectionBridgeLogin, logoutData, undefined, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '225',
info: 'Database offline.'
});
return;
}
/**
* Return successful logout to the user.
*/
var userName = existingClient.DisplayName;
if (userName === '') {
userName = '[New User]';
}
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10030',
info: 'LogOut1 successful.'
},
'INFO',
(userName + ' logged out.'));
});
});
});
};

View File

@ -0,0 +1,337 @@
/**
* @fileOverview Node.js Add Login Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Log in to the system and issues a session token.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/login_auth/login1/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var log = require(global.pathPrefix + 'log.js');
var mailer = require(global.pathPrefix + 'mailer.js');
var auth = require(global.pathPrefix + 'auth.js');
var config = require(global.configFile);
var crypto = require('crypto');
var async = require('async');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {!object} hmacData - hmac information {!address, !method, !body, ?timestamp, ?hmac}
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Valid login request.
* Check API version.
*/
if (receivedObject.APIVersion.split('.')[0] !== config.CCServerVersion.split('.')[0]) {
auth.respond(res, 200, null, null, functionInfo, {
code: '47',
info: 'A major revision change has occurred. The App must be updated.'
},
'WARNING',
'Obsolete App version.',
('AI [' + receivedObject.ClientName + ']'));
return;
}
/**
* Find the device and check the token.
*/
mainDB.findOneObject(mainDB.collectionDevice, {DeviceToken: receivedObject.DeviceToken}, undefined, false,
function(err, existingDevice) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '41',
info: 'Database offline.'
});
return;
}
/**
* Check that the device was found.
*/
if (existingDevice === null) {
auth.respond(res, 200, null, null, functionInfo, {
code: '42',
info: 'Invalid device token.'
},
'ERROR',
'Mobile device cannot be matched to token.',
('AF [' + receivedObject.ClientName + ' (DeviceToken ' + receivedObject.DeviceToken + ')]'));
return;
}
/**
* Check device status.
*/
var currentDeviceStatus = auth.checkDeviceStatus(existingDevice.DeviceStatus);
if (currentDeviceStatus) {
auth.respond(res, 200, null, null, functionInfo, {
code: currentDeviceStatus.code.toString(),
info: currentDeviceStatus.message
},
'WARNING',
null,
('AI [' + receivedObject.ClientName + ' (' + existingDevice.DeviceNumber + ')]'));
return;
}
/**
* Check the passcode - 5 digit pin minimum.
*/
var timestamp = new Date();
auth.checkDevicePIN(receivedObject.DeviceAuthorisation, existingDevice, timestamp, function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: err.code.toString(),
info: err.message
},
'WARNING',
null,
(existingDevice.ClientID + ' (' + existingDevice.DeviceNumber + ')'));
return;
}
// Next pull the owning client based on the ID stored with the device.
mainDB.findOneObject(mainDB.collectionClient, {ClientID: existingDevice.ClientID}, undefined, false,
function(err, existingClient) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '45',
info: 'Database offline.'
});
return;
}
/**
* User name not found.
*/
if (existingClient === null) {
auth.respond(res, 200, null, null, functionInfo, {
code: '47',
info: 'Invalid client e-mail.'
},
'WARNING',
null,
(existingDevice.ClientID + ' (' + existingDevice.DeviceNumber + ')'));
return;
}
// Check the email we were passed matches that of the owning client
if (receivedObject.ClientName !== existingClient.ClientName) {
auth.respond(res, 200, null, null, functionInfo, {
code: '46',
info: 'E-mail mismatch.'
},
'WARNING',
('E-mail mismatch in received data (' + receivedObject.ClientName + ').'),
(existingDevice.ClientID + ' (' + existingDevice.DeviceNumber + ')'));
return;
}
/**
* Check client status.
*/
var currentClientStatus = auth.checkClientStatus(existingClient.ClientStatus);
if (currentClientStatus) {
auth.respond(res, 200, null, null, functionInfo, {
code: currentClientStatus.code.toString(),
info: currentClientStatus.message
},
'WARNING',
null,
(existingDevice.ClientID + ' (' + existingDevice.DeviceNumber + ')'));
return;
}
/**
* Check the HMAC.
* We have to add the ClientName into HMAC data before checking
*/
hmacData.ClientName = existingClient.ClientName;
auth.checkHMAC(existingDevice, hmacData, 'Login1.process', function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: err.code.toString(),
info: err.message
},
'WARNING',
null,
(existingDevice.ClientID + ' (' + existingDevice.DeviceNumber + ')'));
return;
}
/**
* Store the last login location if available.
*/
var newLastLoginLocation = null;
if ((receivedObject.Longitude !== null) && (receivedObject.Latitude !== null)) {
newLastLoginLocation = {
type: 'Point',
coordinates: [receivedObject.Longitude, receivedObject.Latitude]
};
}
/**
* Activate the session token and store a successful login.
*/
var newExpiry = new Date(timestamp);
newExpiry.setMinutes(newExpiry.getMinutes() + utils.sessionTimeout);
var newDeviceHardware = receivedObject.DeviceHardware;
var newDeviceSoftware = receivedObject.DeviceSoftware;
var sessionToken = utils.randomCode(utils.fullAlphaNumeric, utils.tokenLength);
mainDB.updateObject(mainDB.collectionDevice, {DeviceToken: receivedObject.DeviceToken}, {
$set: {
DeviceHardware: newDeviceHardware,
DeviceSoftware: newDeviceSoftware,
LastUpdate: timestamp,
SessionToken: sessionToken,
SessionTokenExpiry: newExpiry,
LastLoginLocation: newLastLoginLocation,
LastLoginIP: functionInfo.remote,
LastLogin: timestamp,
LoginAttempts: 0
}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '49',
info: 'Database offline.'
});
return;
}
/**
* Store the login time and location.
*/
var loginData = {};
loginData.ClientID = existingClient.ClientID;
loginData.DeviceToken = existingDevice.DeviceToken;
loginData.SessionToken = sessionToken;
loginData.SourceLocation = newLastLoginLocation;
loginData.SourceIP = functionInfo.remote;
loginData.OperationType = 'Login';
loginData.ServerVersion = config.CCServerVersion;
loginData.APIVersion = receivedObject.APIVersion;
loginData.DeviceHardware = receivedObject.DeviceHardware;
loginData.DeviceSoftware = receivedObject.DeviceSoftware;
loginData.DateTime = timestamp;
/**
* Store login data.
*/
mainDB.addObject(mainDB.collectionBridgeLogin, loginData, undefined, false, function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '194',
info: 'Database offline.'
});
return;
}
/**
* Status bits.
*
*/
var AcceptEULA = 0;
if (config.EULAVersion !== existingClient.EULAVersionAccepted) {
AcceptEULA = 1;
}
var clientDetailsSet = 0;
if (utils.bitsAllSet(existingClient.ClientStatus, utils.ClientDetailsMask)) {
clientDetailsSet = 1;
}
/**
* Assemble strings and objects for response.
*/
var userName = existingClient.DisplayName;
if (userName === '') {
userName = '[New User]';
}
var toSend = {
SessionToken: sessionToken,
DeviceName: existingDevice.DeviceName,
timeout: utils.sessionTimeout,
PayCodeTimeout: utils.payCodeTimeout,
CallTimeout: config.callTimeout,
MerchantStatus: existingClient.Merchant[0].MerchantStatus,
PollingInterval: utils.pollingInterval,
AcceptEULA: AcceptEULA,
ServerVersion: config.CCServerVersion,
PaymentMin: utils.paymentMin,
PaymentMax: utils.paymentMax,
TipMin: utils.tipMin,
TipMax: utils.tipMax,
TransactionMin: utils.transactionMin,
ClientDetailsSet: clientDetailsSet,
DesyncThreshold: config.HMACDesyncThreshold,
FeatureFlags: existingClient.FeatureFlags
};
if (existingDevice.PendingHMAC !== '') {
toSend.PendingHMAC = existingDevice.PendingHMAC;
}
/**
* Update the Client on first login.
*/
if (existingClient.FirstLogin === 1) {
mainDB.updateObject(mainDB.collectionClient, {'ClientName': existingClient.ClientName}, {
$set: {
LastUpdate: timestamp,
FirstLogin: 0
},
$inc: {LastVersion: 1}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '102',
info: 'Database offline.'
});
return;
}
/**
* Login success (first login).
*/
toSend.code = '10010';
toSend.info = 'Login1 first login successful.';
auth.respond(res, 200, existingDevice, hmacData, functionInfo,
toSend,
'INFO',
(userName + ' first log in.'));
});
return;
}
/**
* Login success (not first login).
*/
toSend.code = '10027';
toSend.info = 'Login1 successful.';
auth.respond(res, 200, existingDevice, hmacData, functionInfo,
toSend,
'INFO',
(userName + ' logged in.'));
});
});
});
});
});
});
};

View File

@ -0,0 +1,128 @@
/**
* @fileOverview Allows a message to be marked as read, unread, etc.
* @preserve Copyright 2016 Comcarde Ltd.
* @author Richard Taylor
* @see #bridge_server-core
*
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/messaging_commands/markmessage/}
*/
/**
* Includes
*/
var Q = require('q');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.js');
var mongodb = require('mongodb');
/**
* This allows a message to be marked as read, unread, etc.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Mark the message as the requested type
*
* Build the query. The limits are:
* - ClientID must be me
* - MessageID must match the _id of the message
* - TimeFilter must be before <now>
*/
var query = {
ClientID: existingClient.ClientID,
_id: mongodb.ObjectID(receivedObject.MessageID),
TimeFilter: {$lte: new Date()}
};
/**
* Define the update
*/
var update = {
$currentDate: {
LastUpdate: true
}
};
if (receivedObject.Mark === 'Read') {
// Mark as read by setting the read date
update.$currentDate.Read = true;
} else if (receivedObject.Mark === 'Unread') {
// Mark as unread by clearing the read date
update.$set = {
Read: null
};
}
/**
* Build the options.
*/
var options = {
upsert: false,
multi: false,
comment: 'MarkMessage' // For profiler logs use
};
/**
* Request the object
*/
Q.nfcall(
mainDB.updateObject,
mainDB.collectionMessages,
query,
update,
options,
false
).then(function(result) {
/**
* Successful query (though it may not have found anything to update)
*/
if (result.result.n === 1) {
/**
* A document was updated, so this is total success
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10072',
info: 'MarkMessage successful'
},
'INFO',
('MarkMessage successful: ' + receivedObject.MessageID));
} else {
/**
* The request ran, but didn't find any documents
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '484',
info: 'Invalid MessageID.'
},
'WARNING',
('Invalid message ID: ' + receivedObject.MessageID));
}
}).catch(function() {
/**
* Query failed, most likely from the database
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '483',
info: 'Database offline.'
});
});
}); // End auth.validSession callback
};

View File

@ -0,0 +1,242 @@
/**
* @fileOverview Node.js PIN Reset Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Resets the device PIN. JSON version.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/login_auth/pinreset/}
*/
/**
* Includes
*/
var templates = require(global.pathPrefix + '../utils/templates.js');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var log = require(global.pathPrefix + 'log.js');
var mailer = require(global.pathPrefix + 'mailer.js');
var auth = require(global.pathPrefix + 'auth.js');
var config = require(global.configFile);
var crypto = require('crypto');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
*/
exports.process = function(res, functionInfo, parameters, receivedObject) {
/**
* Valid PIN reset request. Find the e-mail address.
*/
mainDB.findOneObject(mainDB.collectionClient, {ClientName: receivedObject.ClientName}, undefined, false, function(err, existingClient) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '129',
info: 'Database offline.'
});
return;
}
/**
* User name not found if null.
*/
if (!existingClient) {
auth.respond(res, 200, null, null, functionInfo, {
code: '130',
info: 'E-mail mismatch.'
},
'WARNING',
'E-mail not found in database.',
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* Check client status.
*/
var currentClientStatus = auth.checkClientStatus(existingClient.ClientStatus);
if (currentClientStatus) {
auth.respond(res, 200, null, null, functionInfo, {
code: currentClientStatus.code.toString(),
info: currentClientStatus.message
},
'WARNING', null,
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* Check the password.
*/
var timestamp = new Date();
auth.checkClientPassword(receivedObject.Password, existingClient, timestamp, function(err) {
/**
* Check if there was an error processing the password.
*/
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: err.code.toString(),
info: err.message
},
'WARNING', null,
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* Valid reset request. Find the device again and verify the token.
*/
mainDB.findOneObject(mainDB.collectionDevice, {DeviceNumber: receivedObject.DeviceNumber}, undefined, false,
function(err, existingDevice) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '133',
info: 'Database offline.'
});
return;
}
/**
* Device not found.
*/
if (!existingDevice) {
auth.respond(res, 200, null, null, functionInfo, {
code: '134',
info: 'Device number not found.'
},
'WARNING', null,
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* Check device ID matches.
*/
if (receivedObject.DeviceUuid !== existingDevice.DeviceUuid) {
auth.respond(res, 200, null, null, functionInfo, {
code: '135',
info: 'Invalid device ID.'
},
'WARNING', null,
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* Check device is associated with e-mail.
*/
if (existingDevice.ClientID !== existingClient.ClientID) {
auth.respond(res, 200, null, null, functionInfo, {
code: '136',
info: 'ClientName mismatch.'
},
'WARNING',
'Device does not belong to Client.',
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* Check device status.
*/
var currentDeviceStatus = auth.checkDeviceStatus(existingDevice.DeviceStatus);
if (currentDeviceStatus) {
auth.respond(res, 200, null, null, functionInfo, {
code: currentDeviceStatus.code.toString(),
info: currentDeviceStatus.message
},
'WARNING', null,
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* All checks complete. Reset the PIN.
*/
var timestamp = new Date();
auth.encryptPBKDF2(receivedObject.DeviceAuthorisation, function(err, newSalt, newHash) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '137',
info: 'Encryption error.'
},
'WARNING', null,
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* Update the database.
*/
var newDeviceAuthorisation = config.pinCryptoVersion + '::' + newHash;
mainDB.updateObject(mainDB.collectionDevice, {DeviceNumber: receivedObject.DeviceNumber}, {
$set: {
DeviceSalt: newSalt,
DeviceAuthorisation: newDeviceAuthorisation,
LoginAttempts: 0,
LastUpdate: timestamp
},
$inc: {LastVersion: 1}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '138',
info: 'Database offline.'
});
return;
}
/**
* PIN updated. Confirm by e-mail.
*/
var geolocation = '';
var geoTag = 'Lat/Long';
if ((receivedObject.Latitude === null) ||
(receivedObject.Longitude === null)) {
geoTag = 'No GPS information available.';
} else {
geolocation = 'http://maps.google.com/maps?q=loc:' + receivedObject.Latitude +
',' + receivedObject.Longitude;
}
var htmlEmail = templates.render('pin-reset', {
LatLong: geolocation,
GeoTag: geoTag,
DeviceNumber: receivedObject.DeviceNumber
});
mailer.sendEmail(null, receivedObject.ClientName, 'Bridge PIN Reset', htmlEmail, 'PINReset.process',
function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '139',
info: 'Unable to send e-mail.'
},
'ERROR', null,
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* PIN Reset successful.
*/
auth.respond(res, 200, null, null, functionInfo, {
code: '10014',
info: 'PIN reset successful.'
},
'INFO',
('PIN reset successful at Lat/Long [' + receivedObject.Latitude + ',' +
receivedObject.Longitude + '].'),
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
});
});
});
});
});
});
};

View File

@ -0,0 +1,352 @@
/**
* @fileOverview Node.js PayCode Request Handler for Bridge Pay
* @preserve Copyright 2015 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Gets a limited time PayCode that can be given to the merchant.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/payment_commands/paycoderequest/}
*/
/**
* Includes.
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var mongodb = require('mongodb');
var async = require('async');
var config = require(global.configFile);
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Check that display names are valid. Otherwise, do not allow the Client to continue.
*/
if (utils.MinDisplayNameLength > existingClient.DisplayName.length) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '472',
info: 'DisplayName is invalid. Please fill out customer details.'
},
'WARNING');
return;
}
if ((utils.MinDisplayNameLength > existingClient.Merchant[0].CompanyAlias.length) &&
(existingClient.Merchant[0].MerchantStatus === utils.MerchantStatusActive)) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '473',
info: 'CompanyAlias is invalid. Please fill out Merchant details.'
},
'WARNING');
return;
}
/**
* Set up paycode to write.
*/
var newPayCode = {};
newPayCode.DeviceToken = existingDevice.DeviceToken;
newPayCode.SessionToken = receivedObject.SessionToken;
var timestamp = null;
var payCodeLength = 5;
var counter = 5;
var notWritten = true;
var payCodeObject = null;
/**
* Keep trying to issue a PayCode.
*/
async.doWhilst(
/**
* Keep doing this.
*/
function(callback) {
counter = counter - 1;
newPayCode.PayCode = utils.payCodeGeneration(utils.paycodeString, payCodeLength, 'Bridge');
timestamp = new Date();
newPayCode.Creation = new Date(timestamp);
newPayCode.Expiry = new Date(timestamp);
newPayCode.Expiry.setMinutes(newPayCode.Expiry.getMinutes() + utils.payCodeTimeout);
newPayCode.TransactionID = null;
mainDB.addObject(mainDB.collectionPayCode, newPayCode, undefined, true, function(err, payCodeAdded) {
/**
* If the payCodeAdded is returned then the PayCode is unique and has been added to the DB.
*/
if (payCodeAdded) {
notWritten = false;
payCodeObject = payCodeAdded[0];
}
callback();
});
},
/**
* Until this.
*/
function() {
/**
* Attempt to write 5 paycodes of the same length.
*/
if (counter < 1) {
counter = 5;
payCodeLength = payCodeLength + 1;
}
/**
* Paycode has gotten too long. Return an error.
*/
if (payCodeLength > 12) {
return false;
} else {
return notWritten;
}
},
/**
* Then do this!
*/
function(err) {
/**
* 5 seconds have passed.
*/
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '157',
info: 'Cannot issue a PayCode.'
},
'WARNING',
err.message);
return;
}
/**
* Check that a unique paycode has been issued.
*/
if (payCodeLength > 12) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '158',
info: 'Cannot generate a unique PayCode.'
},
'WARNING');
return;
}
/**
* Create the new transaction.
*/
var newTrans = mainDB.blankTransaction();
newTrans.PayCode = payCodeObject.PayCode;
newTrans.PayCodeID = payCodeObject._id;
newTrans.PayCodeExpiry = payCodeObject.Expiry;
newTrans.CustomerDeviceToken = receivedObject.DeviceToken;
newTrans.CustomerSessionToken = receivedObject.SessionToken;
newTrans.CustomerAccountID = receivedObject.AccountID;
newTrans.CustomerClientID = existingClient.ClientID;
/**
* Get the account information and fill in transaction.
*
* Need to ignore jshint warnings here:
* 1. Cyclomatic complexity too high - W074. Legacy code.
*/
//jshint -W074
mainDB.findOneObject(mainDB.collectionAccount, {_id: mongodb.ObjectID(receivedObject.AccountID)}, undefined, false,
function(err, existingCustomerAccount) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '229',
info: 'Database offline.'
});
return;
}
/**
* Check that we got an account back.
*/
if (!existingCustomerAccount) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '274',
info: 'Invalid customer AccountID.'
},
'WARNING');
return;
}
//jshint -W016
/**
* Check that this is not a deleted account. Uses bitwise comparison.
*
* Need to ignore jshint warnings here:
* 1. Unexpected bitwise comparison. This is deliberately a bitwise comparison.
*/
if (existingCustomerAccount.AccountStatus & utils.AccountDeleted) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '273',
info: 'Deleted customer AccountID.'
},
'WARNING');
return;
}
/**
* Check that this is not an API generated account (as they
* don't have the encrypted info we need to process a transaction)
* Uses bitwise comparison.
*
* Need to ignore jshint warnings here:
* 1. Unexpected bitwise comparison. This is deliberately a bitwise comparison.
*/
if (existingCustomerAccount.AccountStatus & utils.AccountApiCreated) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '557',
info: 'Unsupported account type.'
},
'WARNING');
return;
}
//jshint +W016
/**
* Operational account. Check that there is a valid billing address.
*/
if (existingCustomerAccount.BillingAddress === '') {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '490',
info: 'No valid billing address.'
},
'WARNING');
return;
}
/**
* Ensure it's a payments account.
*/
if (existingCustomerAccount.PaymentsAccount !== 1) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '295',
info: 'Not a payments account.'
},
'WARNING');
return;
}
/**
* Fill in account details.
*/
switch (existingCustomerAccount.UserImage) {
case 'Selfie':
newTrans.CustomerDisplayName = existingClient.DisplayName;
newTrans.CustomerImage = existingClient.Selfie;
break;
case 'defaultSelfie':
newTrans.CustomerDisplayName = existingClient.DisplayName;
newTrans.CustomerImage = config.defaultSelfie;
break;
case 'CompanyLogo0':
newTrans.CustomerDisplayName = existingClient.Merchant[0].CompanyAlias;
newTrans.CustomerSubDisplayName = existingClient.Merchant[0].CompanySubName;
newTrans.CustomerImage = existingClient.Merchant[0].CompanyLogo;
if (existingClient.Merchant[0].VATNo) {
newTrans.CustomerVATNo = existingClient.Merchant[0].VATNo;
}
break;
case 'defaultCompanyLogo0':
newTrans.CustomerDisplayName = existingClient.Merchant[0].CompanyAlias;
newTrans.CustomerSubDisplayName = existingClient.Merchant[0].CompanySubName;
newTrans.CustomerImage = config.defaultCompanyLogo0;
if (existingClient.Merchant[0].VATNo) {
newTrans.CustomerVATNo = existingClient.Merchant[0].VATNo;
}
break;
default:
/**
* Error condition.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '230',
info: 'Invalid image details.'
},
'ERROR',
('The UserImage is invalid for AccountID ' + receivedObject.AccountID));
return;
}
newTrans.StatusInfo = 'Paycode issued. Waiting for merchant...';
if ((receivedObject.Longitude !== null) && (receivedObject.Latitude !== null)) {
newTrans.CustomerLocation = {
type: 'Point',
coordinates: [receivedObject.Longitude, receivedObject.Latitude]
};
}
newTrans.LastUpdate = new Date();
/**
* Write the transaction and get the object ID back.
*/
mainDB.addObject(mainDB.collectionTransaction, newTrans, undefined, false, function(err, transactionAdded) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '159',
info: 'Database offline.'
});
return;
}
/**
* Transactions successfully added. Update the PayCode.
*/
var transaction = mongodb.ObjectID(transactionAdded[0]._id).toString();
mainDB.updateObject(
mainDB.collectionPayCode,
{_id: mongodb.ObjectID(payCodeObject._id)},
{
$set: {
TransactionID: transaction
}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '160',
info: 'Database offline.'
});
return;
}
/**
* Return code to user
*/
var timeNow = new Date();
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10015',
info: 'Paycode issued.',
paycode: newPayCode.PayCode,
creation: newPayCode.Creation,
now: timeNow,
expiry: newPayCode.Expiry,
transactionID: transactionAdded[0]._id
},
'INFO',
('New PayCode ' + newPayCode.PayCode + ' issued.'));
});
});
});
});
});
};

View File

@ -0,0 +1,61 @@
/**
* @fileOverview List the addresses that match the given postcode
* @preserve Copyright 2016 Comcarde Ltd.
* @author Richard Taylor
* @see #bridge_server-core
*
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/utility_commands/postcode_lookup/}
*/
'use strict';
/**
* Includes
*/
var _ = require('lodash');
var auth = require(global.pathPrefix + 'auth.js');
var postcodeUtils = require(global.pathPrefix + '../utils/postcodes.js');
var utils = require(global.pathPrefix + 'utils.js');
var apiHelpers = require(global.pathPrefix + '../utils/api_helpers.js');
/**
* List addresses for the current postcode.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
const lookupP = postcodeUtils.postcodeLookup(receivedObject.PostCode);
lookupP.then((addresses) => {
auth.respond(res, 200, existingDevice, hmacData, functionInfo,
{
code: '10078',
info: 'Address list returned.',
AddressCount: addresses.length,
Addresses: addresses
},
'INFO',
'PostCodeLookup successful.'
);
}).catch((error) => {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '531',
info: 'Unable to lookup addresses.'
});
});
}); // End auth.validSession callback
};

View File

@ -0,0 +1,87 @@
/**
* @fileOverview Node.js Redeem PayCode Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Allows a merchant to add their details to a transaction.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/payment_commands/redeempaycode/}
*/
'use strict';
/**
* Includes
*/
const authP = require(global.pathPrefix + 'auth-promises.js');
const impl = require(global.pathPrefix + '../impl/redeem_paycode.js');
const Q = require('q');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = async function(res, functionInfo, parameters, receivedObject, hmacData) {
let authDetails = null;
try {
/**
* Validate the session. This function responds directly if there is a problem.
*/
authDetails = await authP.validSession(
res,
receivedObject.DeviceToken,
receivedObject.SessionToken,
functionInfo,
hmacData
).then((result) => {
return {
existingDevice: result[0],
existingClient: result[1]
};
}).catch(() => Q.reject()); // Error has been handled in auth.ValidSession
const response = await impl.redeemPaycodeP(
authDetails.existingClient,
receivedObject);
authP.respond(res, 200, authDetails.existingDevice, hmacData, functionInfo,
response,
'INFO',
('PayCode ' + receivedObject.PayCode + ' redeemed for TransactionID ' +
response.TransactionID + '.'));
} catch (error) {
if (error) {
//
// Check if any of these errors need to be logged
//
let logType = null;
const warnings = [
'174',
'474',
'475',
'476',
'176',
'179',
'276',
'491',
'275',
'296',
'231'
];
if (warnings.indexOf(error.code) !== -1) {
logType = 'WARNING';
}
authP.respond(res, 200, authDetails.existingDevice, hmacData, functionInfo,
{
code: error.code,
info: error.info
},
logType);
}
}
};

View File

@ -0,0 +1,483 @@
/**
* @fileOverview Node.js Refund Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Refunds the transaction to the customer. This function should only be used if the full transaction is
* being refunded in one go. There is (will) be a separate transaction for partial refunds.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/payment_commands/refundtransaction/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var worldpay = require(global.pathPrefix + 'worldpay.js');
var mongodb = require('mongodb');
var async = require('async');
var _ = require('lodash');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* There are local variables here that must be available throughout the function.
*/
var newinfo = '';
var locals = {};
/**
* Call all database updates in series.
*/
async.series([
function(callback) {
/**
* Find the transaction to refund.
*/
mainDB.findOneObject(mainDB.collectionTransaction,
{_id: mongodb.ObjectID(receivedObject.TransactionID)}, undefined, false, function(err, existingTransaction) {
if (err) {
callback({
data: {code: '228', info: 'Database offline.'}
});
return;
}
/**
* No transaction in the database.
*/
if (!existingTransaction) {
callback({
data: {code: '237', info: 'Invalid TransactionID.'},
logType: 'WARNING'
});
return;
}
/**
* Only transactions with a status of 3 can be fully refunded.
* Partial refunds may work on other transactions.
*/
if (existingTransaction.TransactionStatus !== 3) {
if (existingTransaction.TransactionStatus === 4) {
callback({
data: {code: '235', info: 'Already refunded.'},
logType: 'WARNING',
altString: 'Transaction already fully refunded.'
});
return;
} else {
callback({
data: {code: '236', info: 'Cannot be refunded.'},
logType: 'WARNING',
altString: 'Cannot be refunded (partial refund may still work).'
});
return;
}
}
/**
* Ensure this is the merchant.
*/
if (existingTransaction.MerchantClientID !== existingClient.ClientID) {
callback({
data: {code: '238', info: 'Invalid ClientName.'},
logType: 'WARNING',
altString: 'Transaction can only be refunded by the merchant.'
});
return;
}
/**
* Store the transaction data.
*/
locals.existingTransaction = existingTransaction;
callback();
});
},
function(callback) {
/**
* Update the transaction.
*/
locals.succeeded = false;
if (locals.existingTransaction.AcquirerName === 'Demo') {
/**
* Refund the Demo transaction.
*/
locals.succeeded = true;
locals.newLastUpdate = new Date();
locals.newStatusInfo = 'Refunded. Authorisation code ' + utils.timeBasedRandomCode() + '.';
locals.newTransactionStatus = utils.TransactionStatus.REFUNDED;
locals.newAmountRefunded = locals.existingTransaction.TotalAmount;
callback();
} else if (locals.existingTransaction.AcquirerName === 'Worldpay') {
/**
* This is a Worldpay transaction refund.
*/
locals.worldpayServiceKey = utils.decryptDataV1(locals.existingTransaction.AcquirerCipher);
if (typeof locals.worldpayServiceKey === 'object') {
callback({
data: {code: '561', info: 'Error decrypting Worldpay service key.'},
logType: 'WARNING',
altString: 'Transaction ' + locals.existingTransaction._id.toString() + ': ' + locals.worldpayServiceKey
});
return;
}
/**
* Call the refund function.
*/
worldpay.worldpayFunction(
'POST',
'orders/' + locals.existingTransaction.SaleReference + '/refund',
locals.worldpayServiceKey,
null, // No additional headers.
{},
function(err, response) {
if (err) {
console.log('ERR:' + JSON.stringify(err));
}
if (response) {
console.log('RESPONSE: ' + response);
}
if (err) {
callback({
data: {code: '260', info: err.message}
});
return;
}
locals.succeeded = true;
locals.newLastUpdate = new Date();
locals.newStatusInfo = 'Refunded. Worldpay.';
locals.newTransactionStatus = utils.TransactionStatus.REFUNDED;
locals.newAmountRefunded = locals.existingTransaction.TotalAmount;
/**
* Success.
*/
callback();
});
} else {
/**
* Invalid acquiring bank.
*/
newinfo = 'Invalid acquiring bank (' + locals.existingTransaction.AcquirerName + ').';
callback({
data: {code: '242', info: 'Invalid acquirer.'},
logType: 'ERROR',
altString: newinfo
});
}
},
function(callback) {
/**
* Update the transaction.
*/
mainDB.collectionTransaction.findOneAndUpdate({
_id: mongodb.ObjectID(receivedObject.TransactionID),
MerchantClientID: existingClient.ClientID,
TransactionStatus: utils.TransactionStatus.COMPLETE
}, {
$set: {
AmountRefunded: locals.newAmountRefunded,
StatusInfo: locals.newStatusInfo,
TransactionStatus: locals.newTransactionStatus,
LastUpdate: locals.newLastUpdate
},
$inc: {
LastVersion: 1
}
}, {
upsert: false
}, function(err) {
if (err) {
callback({
data: {code: '260', info: 'Database offline.'}
});
return;
}
/**
* Check to see if the payment was a success.
*/
if (!locals.succeeded) {
newinfo = 'Refund failed to ' + locals.existingTransaction.CustomerClientID + '.';
callback({
data: {code: '261', info: 'Refund failed.'},
logType: 'WARNING',
altString: newinfo
});
return;
}
/**
* Success. Call back accordingly.
*/
callback();
});
},
function(callback) {
/**
* Set up new history items to reverse the transaction.
*/
locals.newCustomerHist = mainDB.blankTransactionHistory();
locals.newCustomerHist.TransactionID = receivedObject.TransactionID;
locals.newCustomerHist.TransactionType = 3;
locals.newCustomerHist.AccountID = locals.existingTransaction.CustomerAccountID;
locals.newCustomerHist.ClientID = locals.existingTransaction.CustomerClientID;
locals.newCustomerHist.OtherDisplayName = locals.existingTransaction.MerchantDisplayName;
locals.newCustomerHist.OtherSubDisplayName = locals.existingTransaction.MerchantSubDisplayName;
locals.newCustomerHist.OtherImage = locals.existingTransaction.MerchantImage;
locals.newCustomerHist.TotalAmount = locals.existingTransaction.TotalAmount;
locals.newCustomerHist.SaleTime = locals.newLastUpdate;
locals.newCustomerHist.LastUpdate = locals.newLastUpdate;
if (!_.isUndefined(locals.existingTransaction.MerchantInvoiceNumber)) {
locals.newCustomerHist.MerchantInvoiceNumber = locals.existingTransaction.MerchantInvoiceNumber;
}
locals.newMerchantHist = mainDB.blankTransactionHistory();
locals.newMerchantHist.TransactionID = receivedObject.TransactionID;
locals.newMerchantHist.TransactionType = 2;
locals.newMerchantHist.AccountID = locals.existingTransaction.MerchantAccountID;
locals.newMerchantHist.ClientID = locals.existingTransaction.MerchantClientID;
locals.newMerchantHist.OtherDisplayName = locals.existingTransaction.CustomerDisplayName;
locals.newMerchantHist.OtherSubDisplayName = locals.existingTransaction.CustomerSubDisplayName;
locals.newMerchantHist.OtherImage = locals.existingTransaction.CustomerImage;
if ((receivedObject.Longitude !== null) && (receivedObject.Latitude !== null)) {
locals.newMerchantHist.MyLocation = {
type: 'Point',
coordinates: [receivedObject.Longitude, receivedObject.Latitude]
};
}
locals.newMerchantHist.TotalAmount = locals.existingTransaction.TotalAmount;
locals.newMerchantHist.SaleTime = locals.newLastUpdate;
locals.newMerchantHist.LastUpdate = locals.newCustomerHist.LastUpdate;
if (!_.isUndefined(locals.existingTransaction.MerchantInvoiceNumber)) {
locals.newMerchantHist.MerchantInvoiceNumber = locals.existingTransaction.MerchantInvoiceNumber;
}
callback();
},
function(callback) {
/**
* Call in the customer account details.
*/
mainDB.findOneObject(mainDB.collectionAccount,
{_id: mongodb.ObjectID(locals.existingTransaction.CustomerAccountID)}, undefined, false,
function(err, existingCustomerAccount) {
if (err) {
callback({
data: {code: '263', info: 'Database offline.'}
});
return;
}
/**
* Check that we got an account back. Note that deleted is irrelevant here as we can
* refund into a deleted account.
*/
if (!existingCustomerAccount) {
newinfo = 'Refund succeeded but cannot find customer account for ' +
locals.existingTransaction.CustomerClientID + '.';
callback({
data: {code: '264', info: 'Refund successful. No customer account.'},
logType: 'WARNING',
altString: newinfo
});
return;
}
/**
* Store the transaction data.
*/
locals.existingCustomerAccount = existingCustomerAccount;
callback();
});
},
function(callback) {
/**
* Process the transaction - call the merchant account details in.
*/
mainDB.findOneObject(mainDB.collectionAccount,
{_id: mongodb.ObjectID(locals.existingTransaction.MerchantAccountID)}, undefined, false,
function(err, existingMerchantAccount) {
if (err) {
callback({
data: {code: '263', info: 'Database offline.'}
});
return;
}
/**
* Check that we got an account back. Note that deleted is irrelevant here as we
* can refund from a deleted account.
*/
if (!existingMerchantAccount) {
newinfo = 'Refund succeeded but cannot find merchant account for ' +
locals.existingTransaction.MerchantClientID + '.';
callback({
data: {code: '265', info: 'Refund successful. No merchant account.'},
logType: 'WARNING',
altString: newinfo
});
return;
}
/**
* Store the transaction data.
*/
locals.existingMerchantAccount = existingMerchantAccount;
callback();
});
},
function(callback) {
/**
* Update the customer account if appropriate.
*/
if (locals.existingCustomerAccount.BalanceAvailable !== 0) {
locals.newBalance = locals.existingCustomerAccount.Balance + locals.existingTransaction.TotalAmount;
locals.newTransactionTotal = locals.existingCustomerAccount.TransactionTotal +
locals.existingTransaction.TotalAmount;
locals.newTotalDeposits = locals.existingCustomerAccount.TotalDeposits + locals.existingTransaction.TotalAmount;
mainDB.updateObject(mainDB.collectionAccount,
{_id: mongodb.ObjectID(locals.existingTransaction.CustomerAccountID)}, {
$set: {
TransactionTotal: locals.newTransactionTotal,
TotalDeposits: locals.newTotalDeposits,
Balance: locals.newBalance,
LastUpdate: locals.newLastUpdate
},
$inc: {
LastVersion: 1
}
},
{upsert: false}, false, function(err) {
if (err) {
callback({
data: utils.createError('266', 'Database offline')
});
} else {
callback();
}
});
} else {
callback();
}
},
function(callback) {
/**
* Update the merchant account.
*/
if (locals.existingMerchantAccount.BalanceAvailable !== 0) {
locals.newBalance = locals.existingMerchantAccount.Balance - locals.existingTransaction.TotalAmount;
locals.newTransactionTotal = locals.existingMerchantAccount.TransactionTotal +
locals.existingTransaction.TotalAmount;
locals.newTotalWithdrawals = locals.existingMerchantAccount.TotalWithdrawals +
locals.existingTransaction.TotalAmount;
mainDB.updateObject(mainDB.collectionAccount,
{_id: mongodb.ObjectID(locals.existingTransaction.MerchantAccountID)}, {
$set: {
TransactionTotal: locals.newTransactionTotal,
TotalWithdrawals: locals.newTotalWithdrawals,
Balance: locals.newBalance,
LastUpdate: locals.newLastUpdate
},
$inc: {
LastVersion: 1
}
},
{upsert: false}, false, function(err) {
if (err) {
callback({
data: utils.createError('267', 'Database offline')
});
} else {
callback();
}
});
} else {
callback();
}
},
function(callback) {
/**
* Add the customer transaction history.
*/
mainDB.addObject(mainDB.collectionTransactionHistory, locals.newCustomerHist, undefined, false, function(err) {
if (err) {
callback({
data: utils.createError('268', 'Database offline')
});
} else {
callback();
}
});
},
function(callback) {
/**
* Add the merchant transaction history.
*/
mainDB.addObject(mainDB.collectionTransactionHistory, locals.newMerchantHist, undefined, false, function(err) {
if (err) {
callback({
data: utils.createError('269', 'Database offline')
});
} else {
callback();
}
});
}
],
/**
* Callback for information. Note that err can contain 3 elements: data (JSON to return), logType, and altString.
*/
function(err) {
if (err) {
if (err.hasOwnProperty('logType')) {
//jshint -W117
if (err.hasOwnProperty('altString')) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, err.data, err.logType, err.altString);
} else {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, err.data, err.logType);
}
//jshint +W117
} else {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, err.data);
}
return;
}
/**
* Complete success.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10035',
info: 'Refund confirmed.'
},
'INFO',
('Refund confirmed by acquirer to ' + locals.existingTransaction.CustomerClientID + '.'));
});
});
};

View File

@ -0,0 +1,268 @@
/**
* @fileOverview Node.js Register 1 Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* First registration command. Creates Client and Device database entries. JSON version.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/registration_commands/register1/}
*/
/**
* Includes
*/
var templates = require(global.pathPrefix + '../utils/templates.js');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var log = require(global.pathPrefix + 'log.js');
var sms = require(global.pathPrefix + 'sms.js');
var mailer = require(global.pathPrefix + 'mailer.js');
var auth = require(global.pathPrefix + 'auth.js');
var config = require(global.configFile);
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
*/
exports.process = function(res, functionInfo, parameters, receivedObject) {
/**
* Local variables.
*/
var timestamp = new Date();
/**
* Valid registration request.
*/
log.system(
'INFO',
'Registration request received.',
'Register1.process',
'',
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'),
(functionInfo.remote + ' (' + functionInfo.port + ')'));
/**
* Next verify that the e-mail address has not been used before.
*/
mainDB.findOneObject(mainDB.collectionClient, {ClientName: receivedObject.ClientName}, undefined, false, function(err, existingClient) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '3',
info: 'Database offline.'
});
return;
}
/**
* Client info retrieved if present. Check for the device.
*/
mainDB.findOneObject(mainDB.collectionDevice,
{DeviceNumber: receivedObject.DeviceNumber}, undefined, false, function(err, existingDevice) {
/**
* Check for errors.
*/
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '7',
info: 'Database offline.'
});
return;
}
/**
* Check that nothing exists in the database.
*/
if (existingDevice || existingClient) {
auth.respond(res, 200, null, null, functionInfo, {
code: '318',
info: 'Email or mobile number already in use. Please log in to recover.'
},
'WARNING',
'Email or mobile number already in use.',
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* This is a completely new signup. Create client data structure.
*/
var newClient = mainDB.blankClient();
newClient.ClientName = receivedObject.ClientName;
newClient.DisplayName = '';
newClient.EMailValidationToken = utils.randomCode(utils.fullAlphaNumeric, utils.tokenLength);
newClient.EMailValidationTokenExpiry = new Date(timestamp);
newClient.EMailValidationTokenExpiry.setDate(newClient.EMailValidationTokenExpiry.getDate() + 7); // Add a week.
newClient.OperatorName = receivedObject.OperatorName;
newClient.KYC[0].ContactEmail = newClient.ClientName;
newClient.PasswordManagement[0].PasswordExpiry = new Date(timestamp);
newClient.PasswordManagement[0].PasswordExpiry.setDate(
newClient.PasswordManagement[0].PasswordExpiry.getDate() + 365); // Add a year.
newClient.PasswordManagement[0].PasswordLastReset = new Date(timestamp);
newClient.ClientPreferences[0].DefaultAccount = receivedObject.Method;
newClient.LastUpdate = new Date(timestamp);
/**
* Hash the password.
*/
auth.encryptPBKDF2(receivedObject.Password, function(err, newSalt, newHash) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '413',
info: 'Encryption error.'
},
'WARNING', null,
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* Store the new salt and hash.
*/
newClient.Password = '2::' + newHash;
newClient.ClientSalt = newSalt;
/**
* Add the new client object.
*/
mainDB.addObject(mainDB.collectionClient, newClient, undefined, false, function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '6',
info: 'Database offline.'
});
return;
}
/**
* E -mail successfully added - send the registration e-mail.
*/
mailer.sendWelcomeEmail(newClient, receivedObject.Mode, 'Register1.process',
function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '5',
info: 'Unable to send e-mail.'
},
'ERROR', null,
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* E -mail sent. Now set up the new device.
*/
var newDevice = mainDB.blankDevice();
if (receivedObject.DeviceHardware !== '') { // Add device hardware if sent.
newDevice.DeviceName = 'My ' + receivedObject.DeviceHardware;
}
newDevice.DeviceUuid = receivedObject.DeviceUuid;
newDevice.DeviceHardware = receivedObject.DeviceHardware;
newDevice.DeviceSoftware = receivedObject.DeviceSoftware;
newDevice.DeviceNumber = receivedObject.DeviceNumber;
newDevice.ClientID = newClient.ClientID;
newDevice.RegistrationToken = utils.randomCode(utils.numeric, utils.SMStokenLength);
newDevice.RegistrationTokenExpiry = new Date(timestamp);
newDevice.RegistrationTokenExpiry.setHours(newDevice.RegistrationTokenExpiry.getHours() +
utils.smsTokenDuration);
newDevice.SignupIP = functionInfo.remote;
newDevice.LastUpdate = new Date(timestamp);
newDevice.LastVersion = 1;
// Generate a unique device token and check it doesn't exist.
newDevice.DeviceToken = utils.randomCode(utils.fullAlphaNumeric, utils.tokenLength);
mainDB.findOneObject(mainDB.collectionDevice,
{DeviceToken: newDevice.DeviceToken}, undefined, false, function(err, tokenCheck) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '19',
info: 'Database offline.'
});
return;
}
/**
* The device token is not unique; log this and cancel registration.
*/
if (tokenCheck !== null) {
auth.respond(res, 200, null, null, functionInfo, {
code: '20',
info: 'System error - token duplication.'
},
'ERROR', null,
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* All good. Add the device to the database.
*/
mainDB.addObject(mainDB.collectionDevice, newDevice, undefined, false, function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '8',
info: 'Database offline.'
});
return;
}
/**
* Send the registration SMS if not in test mode.
*/
sms.sendSMS(receivedObject.Mode, newDevice.DeviceNumber,
('Your Bridge verification code is ' + newDevice.RegistrationToken),
function(err, smsBalance) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '9',
info: 'SMS send failure.'
},
'WARNING', null,
('RI [' + receivedObject.ClientName + ' (' +
receivedObject.DeviceNumber + ')]'));
return;
}
/**
* Success.
*/
if (receivedObject.Mode === 'Test') {
/**
* Test mode.
*/
auth.respond(res, 200, null, null, functionInfo, {
code: '10011',
info: 'Register1 test successful.',
DeviceToken: newDevice.DeviceToken,
EULAVersion: config.EULAVersion
},
'INFO',
'Register 1 test successful (SMS not sent).',
('RI [' + receivedObject.ClientName + ' (' +
receivedObject.DeviceNumber + ')]'));
} else {
auth.respond(res, 200, null, null, functionInfo, {
code: '10000',
info: 'Register1 successful.',
DeviceToken: newDevice.DeviceToken,
EULAVersion: config.EULAVersion
},
'INFO',
('Registration SMS sent (SMS balance now ' + smsBalance + ').'),
('RI [' + receivedObject.ClientName + ' (' +
receivedObject.DeviceNumber + ')]'));
}
});
});
});
});
});
});
});
});
};

View File

@ -0,0 +1,195 @@
/**
* @fileOverview Node.js Register2 Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Verifies the mobile device by checking SMS code. JSON version.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/registration_commands/register1/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var config = require(global.configFile);
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
*/
exports.process = function(res, functionInfo, parameters, receivedObject) {
/**
* Valid registration request. Find the device again and verify the token.
*/
mainDB.findOneObject(mainDB.collectionDevice, {DeviceNumber: receivedObject.DeviceNumber},
undefined, false, function(err, existingDevice) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '12',
info: 'Database offline.'
});
return;
}
/**
* Check to see if the device was found.
*/
if (!existingDevice) {
auth.respond(res, 200, null, null, functionInfo, {
code: '13',
info: 'Invalid device.'
},
'WARNING',
'Device cannot be found in database.',
('RI [' + receivedObject.DeviceNumber + ' (DeviceToken ' + receivedObject.DeviceToken + ')]'));
return;
}
/**
* Register2 only works on unauthorised devices with no flags set.
* Expected use of bitwise.
*/
//jshint -W016
if ((existingDevice.DeviceStatus !== 0x0) &&
((existingDevice.DeviceStatus & utils.DeviceFullyRegistered) !== utils.DeviceRegister3Mask)) {
auth.respond(res, 200, null, null, functionInfo, {
code: '17',
info: 'This is not a new device.'
},
'WARNING',
'Device is already authorised.',
('RI [' + receivedObject.DeviceNumber + ' (DeviceToken ' + receivedObject.DeviceToken + ')]'));
return;
}
//jshint +W016
/**
* Now check the device token is valid.
*/
if (receivedObject.DeviceToken !== existingDevice.DeviceToken) {
auth.respond(res, 200, null, null, functionInfo, {
code: '14',
info: 'Invalid device token.'
},
'WARNING', null,
('RI [' + receivedObject.DeviceNumber + ' (DeviceToken ' + receivedObject.DeviceToken + ')]'));
return;
}
/**
* Now check the number of attempts that have been made on the token.
*/
if (existingDevice.RegistrationTokenAttempts >= config.maxRegTokenAttempts) {
auth.respond(res, 200, null, null, functionInfo, {
code: '466',
info: 'Too many registration token attempts - delete the device and start again.'
},
'WARNING', 'Too many registration token attempts.',
('RI [' + receivedObject.DeviceNumber + ' (DeviceToken ' + receivedObject.DeviceToken + ')]'));
return;
}
/**
* Check the registration token expiry.
*/
var timestamp = new Date();
var expiry = existingDevice.RegistrationTokenExpiry;
if (timestamp > expiry) {
auth.respond(res, 200, null, null, functionInfo, {
code: '15',
info: 'Expired registration token.'
},
'WARNING', null,
('RI [' + receivedObject.DeviceNumber + ' (DeviceToken ' + receivedObject.DeviceToken + ')]'));
return;
}
/**
* Ensure that the code is valid. If not, increment the failed attempts.
*/
if (receivedObject.RegistrationToken !== existingDevice.RegistrationToken) {
mainDB.updateObject(mainDB.collectionDevice, {DeviceNumber: existingDevice.DeviceNumber}, {
$set: {LastUpdate: timestamp},
$inc: {
LastVersion: 1,
RegistrationTokenAttempts: 1
}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '467',
info: 'Database offline.'
});
return;
}
auth.respond(res, 200, null, null, functionInfo, {
code: '16',
info: 'Invalid registration token.'
},
'WARNING', null,
('RI [' + receivedObject.DeviceNumber + ' (DeviceToken ' + receivedObject.DeviceToken + ')]'));
});
return;
}
/**
* Update object and rewrite.
* Write to database. Valid bitwise operation.
*/
//jshint -W016
var newDeviceStatus = existingDevice.DeviceStatus | utils.DeviceRegister2Mask;
//jshint +W106
mainDB.updateObject(mainDB.collectionDevice, {DeviceNumber: existingDevice.DeviceNumber}, {
$set: {
LastUpdate: timestamp,
DeviceStatus: newDeviceStatus,
RegistrationToken: '',
RegistrationTokenExpiry: '',
RegistrationTokenAttempts: 0
},
$inc: {LastVersion: 1}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '18',
info: 'Database offline.'
});
return;
}
/**
* Successful new registration if equal to zero.
*/
if (existingDevice.DeviceStatus === 0x0) {
auth.respond(res, 200, null, null, functionInfo, {
code: '10001',
info: 'Register2 successful.'
},
'INFO', 'Mobile successfully verified.',
('RI [' + receivedObject.DeviceNumber + ' (DeviceToken ' + receivedObject.DeviceToken + ')]'));
return;
}
// Successful re-registration.
auth.respond(res, 200, null, null, functionInfo, {
code: '10043',
info: 'Register2 re-registration successful.'
},
'INFO', 'Mobile successfully re-verified.',
('RI [' + receivedObject.DeviceNumber + ' (DeviceToken ' + receivedObject.DeviceToken + ')]'));
});
});
};

View File

@ -0,0 +1,184 @@
/**
* @fileOverview Node.js Register 4 Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Resends the SMS message to an existing phone. JSON version.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/registration_commands/register4/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var log = require(global.pathPrefix + 'log.js');
var sms = require(global.pathPrefix + 'sms.js');
var auth = require(global.pathPrefix + 'auth.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
*/
exports.process = function(res, functionInfo, parameters, receivedObject) {
// Resend the SMS message.
/**
* Valid registration request. Find the device to resend the token.
*/
mainDB.findOneObject(mainDB.collectionDevice, {DeviceToken: receivedObject.DeviceToken}, undefined, false,
function(err, existingDevice) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '26',
info: 'Database offline.'
});
return;
}
/**
* Ensure there is a device.
*/
if (!existingDevice) {
auth.respond(res, 200, null, null, functionInfo, {
code: '27',
info: 'Invalid device token.'
},
'WARNING',
'Mobile device cannot be matched to token.',
('RI [' + receivedObject.DeviceNumber + ' (DeviceToken ' + receivedObject.DeviceToken + ')]'));
return;
}
/**
* This function only works on unverified devices.
*/
//jshint -W016
if ((existingDevice.DeviceStatus !== 0x0) &&
((existingDevice.DeviceStatus & utils.DeviceFullyRegistered) !== utils.DeviceRegister3Mask)) {
auth.respond(res, 200, null, null, functionInfo, {
code: '28',
info: 'Device already verified.'
},
'WARNING', null,
('RI [' + receivedObject.DeviceNumber + ' (DeviceToken ' + receivedObject.DeviceToken + ')]'));
return;
}
//jshint +W016
/**
* Match the phone number.
*/
if (receivedObject.DeviceNumber !== existingDevice.DeviceNumber) {
auth.respond(res, 200, null, null, functionInfo, {
code: '29',
info: 'DeviceNumber mismatched.'
},
'WARNING',
'Phone number does not match token.',
('RI [' + receivedObject.DeviceNumber + ' (DeviceToken ' + receivedObject.DeviceToken + ')]'));
return;
}
/**
* Match the unique device ID.
*/
if (receivedObject.DeviceUuid !== existingDevice.DeviceUuid) {
auth.respond(res, 200, null, null, functionInfo, {
code: '30',
info: 'DeviceUuid mismatched.'
},
'WARNING',
'Unique device ID does not match token.',
('RI [' + receivedObject.DeviceNumber + ' (DeviceToken ' + receivedObject.DeviceToken + ')]'));
return;
}
/**
* Check the registration token expiry.
*/
var timestamp = new Date();
var expiry = existingDevice.RegistrationTokenExpiry;
if (timestamp > expiry) {
auth.respond(res, 200, null, null, functionInfo, {
code: '31',
info: 'Invalid registration token.'
},
'WARNING',
'Registration token is invalid.',
('RI [' + receivedObject.DeviceNumber + ' (DeviceToken ' + receivedObject.DeviceToken + ')]'));
return;
}
/**
* Timestamp has at least not expired. Check that the new timestamp is at least 20 seconds.
* older than the last update.
*/
var twentySeconds = existingDevice.LastUpdate;
twentySeconds.setSeconds(twentySeconds.getSeconds() + 20);
if (twentySeconds > timestamp) {
auth.respond(res, 200, null, null, functionInfo, {
code: '33',
info: 'SMS 20 second timeout.'
},
'WARNING',
'Please wait 20 seconds before requesting another SMS.',
('RI [' + receivedObject.DeviceNumber + ' (DeviceToken ' + receivedObject.DeviceToken + ')]'));
return;
}
/**
* A reasonable amount of time has been left between clicks. Re-send the registration SMS.
*/
sms.sendSMS(null, existingDevice.DeviceNumber, ('Your Bridge verification code is ' + existingDevice.RegistrationToken),
function(err, smsBalance) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '34',
info: 'SMS send failure.'
},
'ERROR',
('Cannot send SMS. ' + err),
('RI [' + receivedObject.DeviceNumber + ' (DeviceToken ' + receivedObject.DeviceToken + ')]'));
return;
}
/**
* Success.
*/
var newExpiry = new Date(timestamp);
newExpiry.setHours(newExpiry.getHours() + utils.smsTokenDuration);
var newVersion = existingDevice.LastVersion + 1;
mainDB.updateObject(mainDB.collectionDevice, {DeviceToken: receivedObject.DeviceToken}, {
$set: {
LastUpdate: timestamp,
RegistrationTokenExpiry: newExpiry,
LastVersion: newVersion
}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '32',
info: 'Database offline.'
});
return;
}
auth.respond(res, 200, null, null, functionInfo, {
code: '10003',
info: 'Register4 successful.'
},
'INFO',
('Registration SMS re-sent. (SMS balance now ' + smsBalance + ').'),
('RI [' + receivedObject.DeviceNumber + ' (DeviceToken ' + receivedObject.DeviceToken + ')]'));
});
});
});
};

View File

@ -0,0 +1,200 @@
/**
* @fileOverview Node.js Register 6 Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Resend verification e-mail. JSON version.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/registration_commands/register6/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var log = require(global.pathPrefix + 'log.js');
var mailer = require(global.pathPrefix + 'mailer.js');
var auth = require(global.pathPrefix + 'auth.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
*/
exports.process = function(res, functionInfo, parameters, receivedObject) {
/**
* Resend verification e-mail. JSON version.
*/
/**
* Valid registration request. Find the account to resend the token.
*/
mainDB.findOneObject(mainDB.collectionClient, {ClientName: receivedObject.ClientName}, undefined, false, function(err, existingClient) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '85',
info: 'Database offline.'
});
return;
}
/**
* Check that the client exists.
*/
if (!existingClient) {
auth.respond(res, 200, null, null, functionInfo, {
code: '86',
info: 'Invalid e-mail address.'
},
'WARNING',
'Cannot find this e-mail address in the database.',
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* This function only works if the client has never logged in. Check the flag.
* Valid bitwise comparison.
*/
//jshint -W016
if (existingClient.ClientStatus & utils.ClientEmailVerifiedMask) {
auth.respond(res, 200, null, null, functionInfo, {
code: '87',
info: 'Account already verified.'
},
'WARNING',
'Account already verified (DeviceStatus bit 0x1 set).',
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
//jshint +W016
/**
* Get the DeviceUuid.
*/
mainDB.findOneObject(mainDB.collectionDevice, {DeviceNumber: receivedObject.DeviceNumber}, undefined, false,
function(err, existingDevice) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '88',
info: 'Database offline.'
});
return;
}
/**
* Ensure that the device was found.
*/
if (!existingDevice) {
auth.respond(res, 200, null, null, functionInfo, {
code: '203',
info: 'Mobile phone number not available.'
},
'WARNING',
'Device does not exist.',
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* Ensure DeviceUuid is correct.
*/
if (receivedObject.DeviceUuid !== existingDevice.DeviceUuid) {
auth.respond(res, 200, null, null, functionInfo, {
code: '204',
info: 'Invalid DeviceUuid.'
},
'WARNING', null,
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* Check the registration token expiry.
*/
var timestamp = new Date();
var expiry = existingClient.EMailValidationTokenExpiry;
if (timestamp > expiry) {
auth.respond(res, 200, null, null, functionInfo, {
code: '89',
info: 'Invalid registration token.'
},
'WARNING', null,
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* Timestamp has at least not expired. Check that the new timestamp is at least 20
* seconds older than the last update.
*/
var twentySeconds = existingClient.LastUpdate;
twentySeconds.setSeconds(twentySeconds.getSeconds() + 20);
if (twentySeconds > timestamp) {
auth.respond(res, 200, null, null, functionInfo, {
code: '90',
info: 'E-mail 20 second timeout.'
},
'WARNING',
'Wait 20 seconds before requesting another e-mail.',
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* Update database.
*/
var newExpiry = new Date(timestamp);
newExpiry.setDate(newExpiry.getDate() + 7); // Add a week.
var newVersion = existingClient.LastVersion + 1;
mainDB.updateObject(mainDB.collectionClient, {ClientName: receivedObject.ClientName}, {
$set: {
LastUpdate: timestamp,
EMailValidationTokenExpiry: newExpiry,
LastVersion: newVersion
}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '92',
info: 'Database offline.'
});
return;
}
/**
* Resend the welcome / address confirmation e-mail.
*/
mailer.sendWelcomeEmail(existingClient, '', 'Register6.process', function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '91',
info: 'E-mail send failure.'
},
'ERROR',
'Unable to send e-mail.',
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* Success!
*/
auth.respond(res, 200, null, null, functionInfo, {
code: '10009',
info: 'Register6 successful.'
},
'INFO',
'Registration e-mail re-sent.',
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
});
});
});
});
};

View File

@ -0,0 +1,255 @@
/**
* @fileOverview Node.js Register 7 Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Deletes an account. Add the "Mode"="ForceDelete" parameter to force deletion. HTML version.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/registration_commands/register7/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var config = require(global.configFile);
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
*/
exports.process = function(res, functionInfo, parameters) {
/**
* Valid delete request. Find the e-mail address first.
*/
mainDB.findOneObject(mainDB.collectionClient, {ClientName: parameters.ClientName}, undefined, false, function(err, existingClient) {
if (err) {
auth.respondHTML(res, 200, functionInfo, '53', 'templates/undef_database_offline.pug', {
pretty: true,
title: 'Comcarde Bridge',
errornumber: '53',
ipInfo: functionInfo.remote
});
return;
}
/**
* Check that the client exists.
*/
if (!existingClient) {
auth.respondHTML(res, 200, functionInfo, '54', 'templates/54_email_not_found.pug', {
pretty: true,
title: 'Comcarde Bridge',
errornumber: '54',
ClientName: parameters.ClientName,
ipInfo: functionInfo.remote
},
'WARNING',
'E-mail does not exist.',
('RI [' + parameters.ClientName + ' (' + parameters.DeviceNumber + ')]'));
return;
}
/**
* Client does exist - pull the device.
*/
mainDB.findOneObject(mainDB.collectionDevice, {DeviceNumber: parameters.DeviceNumber}, undefined, false,
function(err, existingDevice) {
if (err) {
auth.respondHTML(res, 200, functionInfo, '55', 'templates/undef_database_offline.pug', {
pretty: true,
title: 'Comcarde Bridge',
errornumber: '55',
ipInfo: functionInfo.remote
});
return;
}
/**
* Check we got a device match.
*/
if (!existingDevice) {
auth.respondHTML(res, 200, functionInfo, '56', 'templates/56_mobile_number_not_found.pug', {
pretty: true,
title: 'Comcarde Bridge',
errornumber: '56',
DeviceNumber: parameters.DeviceNumber,
ipInfo: functionInfo.remote
},
'WARNING',
'Device does not exist.',
('RI [' + parameters.ClientName + ' (' + parameters.DeviceNumber + ')]'));
return;
}
/**
* OK, both exist. Run checks to ensure it can be deleted.
*/
if (existingClient.ClientID !== existingDevice.ClientID) {
auth.respondHTML(res, 200, functionInfo, '57', 'templates/57_association_error.pug', {
pretty: true,
title: 'Comcarde Bridge',
errornumber: '57',
ClientName: parameters.ClientName,
DeviceNumber: parameters.DeviceNumber,
ipInfo: functionInfo.remote
},
'WARNING',
'Device is not registered to this e-mail address.',
('RI [' + parameters.ClientName + ' (' + parameters.DeviceNumber + ')]'));
return;
}
/**
* Check registration status. If FirstLogin !== 1, disallow the deletion.
*/
if (existingClient.FirstLogin !== 1) {
/**
* There is one exception - if this is the Dev server and ForceDelete is sent.
*/
if ((('Mode' in parameters) && (parameters.Mode === 'ForceDelete')) && config.isDevEnv) {
/**
* Deletion will be forced as this is the Dev server.
*/
log.system(
'INFO',
'Forced deletion initialised (Mode = ForceDelete).',
'Register7.process',
'',
('RI [' + parameters.ClientName + ' (' + parameters.DeviceNumber + ')]'),
(functionInfo.remote + ' (' + functionInfo.port + ')'));
} else {
auth.respondHTML(res, 200, functionInfo, '58', 'templates/58_fully_registered.pug', {
pretty: true,
title: 'Comcarde Bridge',
errornumber: '58',
ipInfo: functionInfo.remote
},
'WARNING',
'This is a fully registered account.',
('RI [' + parameters.ClientName + ' (' + parameters.DeviceNumber + ')]'));
return;
}
}
/**
* Clean up the existing client before archiving it
* - Move _id to OldClientID: Note the clash with the
* client identifier ClientID which must be retained.
* - Remove the existing Password and ClientSalt.
* - Update the LastUpdate time.
*/
var clientId = existingClient._id;
existingClient.OldClientID = clientId.toString();
delete existingClient._id;
existingClient.Password = '';
existingClient.ClientSalt = '';
existingClient.LastUpdate = new Date();
/**
* Back up Client to the Archive.
*/
mainDB.addObject(mainDB.collectionClientArchive, existingClient, undefined, false, function(err) {
if (err) {
auth.respondHTML(res, 200, functionInfo, '146', 'templates/undef_database_offline.pug', {
pretty: true,
title: 'Comcarde Bridge',
errornumber: '146',
ipInfo: functionInfo.remote
});
return;
}
/**
* Now remove the Client.
*/
mainDB.removeObject(mainDB.collectionClient, {_id: clientId}, undefined, false, function(err) {
if (err) {
auth.respondHTML(res, 200, functionInfo, '59', 'templates/undef_database_offline.pug', {
pretty: true,
title: 'Comcarde Bridge',
errornumber: '59',
ipInfo: functionInfo.remote
});
return;
}
/**
* Report that the Client is out of database.
*/
log.system(
'INFO',
'Client has been successfully removed.',
'Register7.process',
'10005',
('RI [' + parameters.ClientName + ' (' + parameters.DeviceNumber + ')]'),
(functionInfo.remote + ' (' + functionInfo.port + ')'));
/**
* Back up existing Device.
*/
var deviceId = existingDevice._id;
existingDevice.DeviceIndex = existingDevice._id.toString();
delete existingDevice._id;
existingDevice.DeviceAuthorisation = '';
existingDevice.DeviceSalt = '';
existingDevice.PendingHMAC = '';
existingDevice.CurrentHMAC = '';
existingDevice.LastUpdate = new Date();
/**
* Back up existing Device.
*/
mainDB.addObject(mainDB.collectionDeviceArchive, existingDevice, undefined, false, function(err) {
// Check for errors.
if (err) {
auth.respondHTML(res, 200, functionInfo, '145', 'templates/undef_database_offline.pug', {
pretty: true,
title: 'Comcarde Bridge',
errornumber: '145',
ipInfo: functionInfo.remote
});
return;
}
/**
* Device added to archive. Delete from active devices.
*/
mainDB.removeObject(mainDB.collectionDevice, {_id: deviceId}, undefined, false, function(err) {
if (err) {
auth.respondHTML(res, 200, functionInfo, '60', 'templates/undef_database_offline.pug', {
pretty: true,
title: 'Comcarde Bridge',
errornumber: '60',
ipInfo: functionInfo.remote
});
return;
}
/**
* Device removed from database.
*/
auth.respondHTML(res, 200, functionInfo, '10005', 'templates/10005_reg_deleted.pug', {
pretty: true,
title: 'Comcarde Bridge',
ClientName: parameters.ClientName,
DeviceNumber: parameters.DeviceNumber,
ipInfo: functionInfo.remote
},
'INFO',
'Device has been successfully removed.',
('RI [' + parameters.ClientName + ' (' + parameters.DeviceNumber + ')]'));
});
});
});
});
});
});
};

View File

@ -0,0 +1,227 @@
/**
* @fileOverview Node.js Register 8 Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Deletes an account. Add the "Mode"="ForceDelete" parameter to force deletion. JSON version.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/registration_commands/register8/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var config = require(global.configFile);
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
*/
exports.process = function(res, functionInfo, parameters, receivedObject) {
/**
* Valid registration request. Find the e-mail address first.
*/
mainDB.findOneObject(mainDB.collectionClient, {ClientName: receivedObject.ClientName}, undefined, false,
function(err, existingClient) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '61',
info: 'Database offline.'
});
return;
}
/**
* Valid delete request. Find the e-mail address first.
*/
if (!existingClient) {
auth.respond(res, 200, null, null, functionInfo, {
code: '62',
info: 'E-mail address does not exist.'
},
'WARNING', null,
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* OK, so the client does exist. Check for the device.
*/
mainDB.findOneObject(mainDB.collectionDevice, {DeviceNumber: receivedObject.DeviceNumber}, undefined, false,
function(err, existingDevice) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '63',
info: 'Database offline.'
});
return;
}
/**
* Find the device second.
*/
if (!existingDevice) {
auth.respond(res, 200, null, null, functionInfo, {
code: '64',
info: 'Mobile phone number not in use.'
},
'WARNING',
'Device does not exist.',
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* OK, both exist. Run checks to ensure they can be deleted.
* Firstly, check that they are not wrongly linked.
*/
if (existingClient.ClientID !== existingDevice.ClientID) {
auth.respond(res, 200, null, null, functionInfo, {
code: '65',
info: 'Device not registered to this e-mail address.'
},
'WARNING', null,
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
/**
* Check registration status. If FirstLogin !== 1, disallow the deletion.
*/
if (existingClient.FirstLogin !== 1) {
/**
* There is one exception - if this is the Dev server and ForceDelete is sent.
*/
if ((('Mode' in receivedObject) && (receivedObject.Mode === 'ForceDelete')) && config.isDevEnv) {
/**
* Deletion will be forced as this is the Dev server.
*/
log.system(
'INFO',
'Forced deletion initialised (Mode = ForceDelete).',
'Register8.process',
'',
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'),
(functionInfo.remote + ' (' + functionInfo.port + ')'));
} else {
auth.respond(res, 200, null, null, functionInfo, {
code: '66',
info: 'Account fully registered.'
},
'WARNING',
'This is a fully registered account.',
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
return;
}
}
/**
* Clean up the existing client before archiving it
* - Move _id to ClientID
* - Remove the existing Password and ClientSalt.
* - Update the LastUpdate time
*/
var clientId = existingClient._id;
existingClient.OldClientID = clientId.toString();
delete existingClient._id;
existingClient.Password = '';
existingClient.ClientSalt = '';
existingClient.LastUpdate = new Date();
/**
* Back up Client Archive.
*/
mainDB.addObject(mainDB.collectionClientArchive, existingClient, undefined, false, function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '147',
info: 'Database offline.'
});
return;
}
/**
* The account can be safely deleted.
*/
mainDB.removeObject(mainDB.collectionClient, {_id: clientId}, undefined, false, function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '67',
info: 'Database offline.'
});
return;
}
/**
* Client is out of database.
*/
log.system(
'INFO',
'Client has been successfully removed.',
'Register8.process',
'10006',
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'),
(functionInfo.remote + ' (' + functionInfo.port + ')'));
/**
* Back up existing Device.
*/
var deviceId = existingDevice._id;
existingDevice.DeviceIndex = existingDevice._id.toString();
delete existingDevice._id;
existingDevice.DeviceAuthorisation = '';
existingDevice.DeviceSalt = '';
existingDevice.PendingHMAC = '';
existingDevice.CurrentHMAC = '';
existingDevice.LastUpdate = new Date();
/**
* Archive it.
*/
mainDB.addObject(mainDB.collectionDeviceArchive, existingDevice, undefined, false, function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '148',
info: 'Database offline.'
});
return;
}
/**
* Remove device.
*/
mainDB.removeObject(mainDB.collectionDevice, {_id: deviceId}, undefined, false, function(err) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '68',
info: 'Database offline.'
});
return;
}
/**
* Device is out of database
*/
auth.respond(res, 200, null, null, functionInfo, {
code: '10006',
info: 'Client and Device removed.'
},
'INFO',
'Device has been successfully removed.',
('RI [' + receivedObject.ClientName + ' (' + receivedObject.DeviceNumber + ')]'));
});
});
});
});
});
});
};

View File

@ -0,0 +1,184 @@
/**
* @fileOverview Rejects an invoice (with optional comment)
* @preserve Copyright 2016 Comcarde Ltd.
* @author Richard Taylor
* @see #bridge_server-core
*
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/merchant_commands/reject_invoice/}
*/
/*
* Includes
*/
var mongodb = require('mongodb');
var templates = require(global.pathPrefix + '../utils/templates.js');
var Q = require('q');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.js');
var mailer = require(global.pathPrefix + 'mailer.js');
var formattingUtils = require(global.pathPrefix + '../utils/formatting.js');
/**
* Rejects an invoice that the client doesn't believe is correct. An optional
* comment can be provided.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in invoice body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Build the query for the invoice we are rejecting. The limits are:
* - ClientID of the invoice must match the current client
* - _id must match the given InvoiceID
* - Invoice must be in Pending state
*/
var query = {
_id: mongodb.ObjectID(receivedObject.InvoiceID),
CustomerClientID: existingClient.ClientID,
TransactionStatus: utils.TransactionStatus.PENDING_INVOICE
};
/**
* Define the projection
*/
var projection = {
_id: 1,
/* Values required for formatting the notification email */
MerchantClientID: 1,
CustomerDisplayName: 1,
MerchantInvoiceNumber: 1,
CustomerComment: 1
};
var options = {
projection: projection,
upsert: false,
returnOriginal: false, // Return the updated document
comment: 'RejectInvoice' // For profiler logs use
};
/*
* Update values
*/
var update = {
$set: {
TransactionStatus: utils.TransactionStatus.REJECTED_INVOICE
},
$inc: {
LastVersion: 1
},
$currentDate: {
LastUpdate: true
}
};
if (receivedObject.Comment) {
update.$set.CustomerComment = receivedObject.Comment;
} else {
update.$set.CustomerComment = '';
}
/**
* Find the invoice
*/
mainDB.collectionTransaction.findOneAndUpdate(
query,
update,
options,
function(err, response) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '517',
info: 'Database offline.'
});
return;
}
/**
* Couldn't find anything to update
*/
if (!response.value) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '518',
info: 'Invalid InvoiceID or not in Pending status.'
},
'WARNING');
return;
}
/**
* Notify the merchant that the customer has queried the invoice.
* This doesn't affect the success of querying the invoice.
*/
notifyRejectedInvoice(
response.value.MerchantClientID,
response.value
);
/**
* Report the success of querying the invoice
*/
auth.respond(
res,
200,
existingDevice,
hmacData,
functionInfo, {
code: '10077',
info: 'Invoice rejected.'
},
'INFO',
'Invoice rejected (InvoiceID ' + receivedObject.InvoiceID + ').'
);
});
});
};
/**
* Notifies the merchant that an existing invoice has been queried by the customer.
*
* @param {string} merchantID - the merchant's ID (to find their email address)
* @param {object} invoice - the invoice (for adding info to the email)
*
* @returns {Promise} - a promise for the result of notifying the customer
*/
function notifyRejectedInvoice(merchantID, invoice) {
var reviewUrl = formattingUtils.formatPortalUrl(
'business/invoices/' + invoice._id.toString() + '/update'
);
/**
* Render the html for the email
*/
var htmlEmail = templates.render('invoice-queried', {
customer: invoice.CustomerDisplayName,
number: invoice.MerchantInvoiceNumber.InvoiceNumber,
comment: invoice.CustomerComment,
reviewUrl: reviewUrl
});
return Q.nfcall(
mailer.sendEmailByID,
'', // Mode ('Test' to just log, anything else to send)
merchantID, // Destination
'Queried Invoice', // Subject
htmlEmail,
'notifyRejectedInvoice'
);
}

View File

@ -0,0 +1,140 @@
/**
* @fileOverview Node.js Report Image Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Marks an image as offensive in the database.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/image_commands/reportimage/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var config = require(global.configFile);
var mongodb = require('mongodb');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Check whether client is allowed to report images.
*/
//jshint -W016
if (existingClient.ClientStatus & utils.ClientCantReport) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '224',
info: 'Reporting disabled.'
},
'WARNING',
'Client not allowed to flag images.');
return;
}
//jshint +W016
/**
* Check this is not one of the Client's own images.
*/
if ((receivedObject.ImageRef === config.defaultSelfie) ||
(receivedObject.ImageRef === config.defaultCompanyLogo0) ||
(receivedObject.ImageRef === existingClient.Selfie) ||
(receivedObject.ImageRef === existingClient.Merchant[0].CompanyLogo)) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '326',
info: 'Client cannot report default or their own images.'
},
'WARNING');
return;
}
/**
* Find the image file to return.
*/
mainDB.findOneObject(mainDB.collectionImages,
{_id: mongodb.ObjectID(receivedObject.ImageRef)},
undefined,
false,
function(err, existingImage) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '327',
info: 'Database offline.'
});
return;
}
/**
* Check to see if the image exists.
*/
if (!existingImage) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '328',
info: 'Invalid ImageRef.'
},
'WARNING',
'Client reported an invalid ImageRef.');
return;
}
/**
* Image exists. Report again requires a counter increment.
*/
var newLastUpdate = new Date();
var newImageReported = existingImage.ImageReported;
if (newImageReported < 9999) {
newImageReported = newImageReported + 1;
}
/**
* Update the database.
*/
mainDB.updateObject(mainDB.collectionImages, {_id: mongodb.ObjectID(receivedObject.ImageRef)}, {
$set: {
ImageReported: newImageReported,
LastUpdate: newLastUpdate
}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '288',
info: 'Database offline.'
});
return;
}
/**
* Tell the user that the image has been marked as reported.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10034',
info: 'Image reported.'
},
'INFO',
('Image reported (ID ' + receivedObject.ImageRef + ').'));
});
});
});
};

View File

@ -0,0 +1,140 @@
/**
* @fileOverview Node.js Resume Device Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Resumes a particular device if it belongs to the current client.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/registration_commands/resumedevice/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.js');
var mongodb = require('mongodb');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Local variables
*/
var timestamp = new Date();
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Check the current password.
*/
auth.checkClientPassword(receivedObject.Password, existingClient, timestamp, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: err.code.toString(),
info: err.message
},
'WARNING');
return;
}
/**
* Find the device.
*/
mainDB.findOneObject(mainDB.collectionDevice,
{
_id: mongodb.ObjectId(receivedObject.DeviceIndex),
ClientID: existingClient.ClientID
},
undefined,
false,
function(err, device) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '433',
info: 'Database offline.'
});
return;
}
/**
* No hits from database.
*/
if (device === null) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '434',
info: 'Invalid device or device does not belong to client.'
},
'WARNING');
return;
}
/**
* Device has not been suspended.
* Valid bitwise operation.
*/
//jshint -W016
if (!(device.DeviceStatus & utils.DeviceSuspendedMask)) {
//jshint +W016
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '435',
info: 'Device has not been suspended.'
},
'WARNING');
return;
}
/**
* The device can be resumed.
* Correct use of bitwise manipulation.
*/
//jshint -W016
mainDB.updateObject(mainDB.collectionDevice, {_id: mongodb.ObjectId(receivedObject.DeviceIndex)}, {
$bit: {
DeviceStatus: {and: ~utils.DeviceSuspendedMask}
},
$set: {
LastUpdate: timestamp
},
$inc: {LastVersion: 1}
},
{upsert: false}, false, function(err) {
//jshint +W016
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '436',
info: 'Database offline.'
});
return;
}
/**
* Success.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10063',
info: 'Device Resumed.'
},
'INFO');
});
});
});
});
};

View File

@ -0,0 +1,98 @@
/**
* @fileOverview Node.js RotateHMAC Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Confirms the new HMAC has been received, accepted and stored by the device. JSON version.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/login_auth/rotatehmac/}
*/
/**
* Includes
*/
var auth = require(global.pathPrefix + 'auth.js');
var log = require(global.pathPrefix + 'log.js');
var utils = require(global.pathPrefix + 'utils.js');
var mainDB = require(global.pathPrefix + 'mainDB.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* If there is no pending HMAC, do nothing.
*/
if (existingDevice.PendingHMAC === '') {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '442',
info: 'No pending HMAC.'
},
'WARNING');
return;
}
/**
* Check the call is signed with the Pending HMAC.
*/
auth.checkHMAC(existingDevice, hmacData, 'RotateHMAC.process', function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: err.code.toString(),
info: err.message
},
'WARNING');
return;
}
/**
* Rotate the HMAC. Clear HMAC attempts as a bad HMAC may have corrupted the CurrentHMAC.
*/
var timestamp = new Date();
mainDB.updateObject(mainDB.collectionDevice, {DeviceToken: existingDevice.DeviceToken},
{
$set: {
PendingHMAC: '',
CurrentHMAC: existingDevice.PendingHMAC,
HMACAttempts: 0,
LastUpdate: timestamp
}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '443',
info: 'Database offline.'
});
return;
}
/**
* HMAC successfully updated.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10065',
info: 'HMAC rotation successful.'
},
'INFO');
});
});
});
};

View File

@ -0,0 +1,148 @@
/**
* @fileOverview Node.js Send Report Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Returns a list of transactions on an account for the referenced user.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/misc_commands/sendreport/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.js');
var moment = require('moment');
var config = require(global.configFile);
/**
* Module variables.
*/
var bankFees = 29; // RBS charges 29p.
var incomingIP = '62.232.80.210';
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
*/
exports.process = function(res, functionInfo, parameters) {
/**
* Operations are only allowed in certain situations or from certain locations.
*/
if ((functionInfo.remote !== incomingIP) || (!config.isDevEnv)) {
auth.respond(res, 200, null, null, functionInfo, {
code: '317',
info: 'Invalid IP or not Dev server.'
},
'WARNING');
return;
}
/**
* Find the relevant transactions.
* Note that the cyclomatic complexity is known to be high.
*/
//jshint -W074
mainDB.collectionTransaction.find(
{
MerchantClientID: parameters.MerchantClientID,
SaleTime: {'$gte': new Date(parameters.DateGTE), '$lt': new Date(parameters.DateLT)}
},
{
_id: 1,
CustomerDisplayName: 1,
SaleTime: 1,
RequestAmount: 1,
TipAmount: 1,
SaleReference: 1,
SaleAuthCode: 1,
TransactionStatus: 1
}
).sort({SaleTime: -1}).toArray(function(err, items) {
if (err) {
auth.respond(res, 200, null, null, functionInfo, {
code: '316',
info: 'Database offline.'
});
return;
}
/**
* Respond with a list of items.
*/
if (items) {
/**
* Filter the account information.
*/
var total = 0;
var tipTotal = 0;
var counter = 0;
var timestamp = new Date();
var csvFile = 'BRIDGE activity report for ' + parameters.MerchantClientName + utils.CarriageReturn;
csvFile += 'Generated:' + timestamp + utils.CarriageReturn;
csvFile += 'Period: GTE ' + parameters.DateGTE + ', LT ' + parameters.DateLT +
utils.CarriageReturn + utils.CarriageReturn;
csvFile += 'Date\t\tTime\t\tCustomer\t\tAmount\tTip\tSale Reference\tAuth\tTransactionID' + utils.CarriageReturn;
/**
* Go through each item and return a subset of information.
*/
while (counter < items.length) {
/**
* Select card information.
*/
if ((items[counter].TransactionStatus === 3) || (items[counter].TransactionStatus === 4)) {
csvFile += moment(items[counter].SaleTime).format('DD-MM-YYYY') + '\t';
csvFile += moment(items[counter].SaleTime).format('HH:mm:ss') + '\t';
csvFile += items[counter].CustomerDisplayName + '\t\t';
if (items[counter].TransactionStatus === 3) {
csvFile += (items[counter].RequestAmount / 100).toFixed(2) + '\t';
} else {
csvFile += (items[counter].RequestAmount / 100).toFixed(2) + 'R\t';
}
if (items[counter].TransactionStatus === 3) {
csvFile += (items[counter].TipAmount / 100).toFixed(2) + '\t';
} else {
csvFile += (items[counter].TipAmount / 100).toFixed(2) + 'R\t';
}
csvFile += items[counter].SaleReference + '\t';
csvFile += items[counter].SaleAuthCode + '\t';
csvFile += items[counter]._id + utils.CarriageReturn;
if (items[counter].TransactionStatus === 3) {
total += items[counter].RequestAmount;
tipTotal += items[counter].TipAmount;
}
}
counter++; // Always increment the counter.
}
csvFile += utils.CarriageReturn + '-----------------------' + utils.CarriageReturn + 'Total Amount:\t' +
(total / 100).toFixed(2) + utils.CarriageReturn;
csvFile += 'Total Tip:\t' + (tipTotal / 100).toFixed(2) + utils.CarriageReturn;
csvFile += 'Bank Fees:\t' + (bankFees / 100).toFixed(2) + utils.CarriageReturn;
csvFile += '-----------------------' + utils.CarriageReturn + 'Total (GBP):\t' +
((total + tipTotal + bankFees) / 100).toFixed(2) + utils.CarriageReturn + '-----------------------';
/**
* Send the information.
*/
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end(csvFile);
log.system(
'INFO',
'Report sent.',
'SendReport.process',
'',
'UU',
(functionInfo.remote + ' (' + functionInfo.port + ')'));
}
});
//jshint +W074
};

View File

@ -0,0 +1,176 @@
/**
* @fileOverview Node.js Set Account Address Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Allows the user to change the address associated with an account.
*
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/account_commands/setaccountaddress/}
*/
/**
* Includes
*/
var mongodb = require('mongodb');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Find the address.
*/
mainDB.findOneObject(mainDB.collectionAddresses,
{
_id: mongodb.ObjectID(receivedObject.AddressID),
ClientID: existingClient.ClientID
},
{
_id: 1
},
false,
function(err, existingAddress) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '392',
info: 'Database offline.'
});
return;
}
/**
* Ensure that an address was found.
*/
if (!existingAddress) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '393',
info: 'Cannot find address.'
},
'WARNING');
return;
}
/**
* Find the account.
*/
mainDB.findOneObject(mainDB.collectionAccount,
{
_id: mongodb.ObjectID(receivedObject.AccountID),
ClientID: existingClient.ClientID
},
{
_id: 1,
AccountStatus: 1,
BillingAddress: 1
},
false,
function(err, existingAccount) {
/**
* Check for errors.
*/
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '394',
info: 'Database offline.'
});
return;
}
/**
* Ensure that an account was found.
*/
if (!existingAccount) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '395',
info: 'Cannot find account.'
},
'WARNING');
return;
}
/**
* Check for deleted accounts.
*/
//jshint -W016
if (existingAccount.AccountStatus & utils.AccountDeleted) {
//jshint +W016
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '396',
info: 'Cannot change a deleted account.'
},
'WARNING');
return;
}
/**
* Different response if there is no change.
*/
if (existingAccount.BillingAddress === receivedObject.AddressID) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10056',
info: 'BillingAddress already set to this AddressID.'
},
'INFO');
return;
}
/**
* Update the account with the new Address.
*/
var timestamp = new Date();
mainDB.updateObject(mainDB.collectionAccount,
{
_id: mongodb.ObjectID(receivedObject.AccountID),
ClientID: existingClient.ClientID
},
{
$set: {
BillingAddress: receivedObject.AddressID,
LastUpdate: timestamp
},
$inc: {LastVersion: 1}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '397',
info: 'Database offline.'
});
return;
}
/**
* Account address successfully set.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10055',
info: 'Account address set.'
},
'INFO');
});
});
});
});
};

View File

@ -0,0 +1,106 @@
/**
* @fileOverview Node.js Set Client Details Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Sets the client's KYC details in the Client record.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/registration_commands/setclientdetails/}
*/
'use strict';
/**
* Includes
*/
var httpStatus = require('http-status-codes');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.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');
var clientUtils = require(global.pathPrefix + '../utils/client/client.js');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
//
// Get the current user's email from the session
//
var setP = clientUtils.setKyc(existingClient, receivedObject);
setP.then((result) => {
//
// We may have warnings to respond with
//
const responses = [
[
clientUtils.SETKYC_RESPONSES.OK,
httpStatus.OK, 10059, 'Client details set.'
],
[
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.respondAuth(
res, result, existingDevice, hmacData, functionInfo, 'INFO'
);
}).catch((error) => {
const responses = [
[
'MongoError',
httpStatus.OK, 423, 'Database Offline', true
],
[
references.ERRORS.INVALID_ADDRESS,
httpStatus.OK, 532, 'Invalid Address', true
],
[
diligence.ERRORS.VERIFICATION_FAILED,
httpStatus.OK, 533, 'Unable to verify id', true
],
[
clientUtils.SETKYC_ERRORS.DOB_MISMATCH,
httpStatus.OK, 426, 'Date of birth mismatch'
],
[
clientUtils.SETKYC_ERRORS.UPDATE_FAILED,
httpStatus.OK, 534, 'Client not found during update'
],
[
clientUtils.SETKYC_ERRORS.INVALID_PARAMETERS,
httpStatus.OK, 535, 'Invalid paramters'
]
];
const responseHandler = new responsesUtils.ErrorResponses(responses);
responseHandler.respondAuth(
res, error, existingDevice, hmacData, functionInfo, 'INFO'
);
}).done();
});
};

View File

@ -0,0 +1,165 @@
/**
* @fileOverview Node.js Set Default Account Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Sets the default account for the user.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/account_commands/setdefaultaccount/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var mongodb = require('mongodb');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Local variables
*/
var timestamp = new Date();
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Check to see if this is a clear default account.
*/
if (receivedObject.AccountID === '') {
mainDB.updateObject(mainDB.collectionDevice, {DeviceToken: receivedObject.DeviceToken}, {
$set: {
DefaultAccount: '',
LastUpdate: timestamp
},
$inc: {LastVersion: 1}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '314',
info: 'Database offline.'
});
return;
}
/**
* Success! There are different return codes for set or clear.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10046',
info: 'Default account cleared.'
},
'INFO');
});
return;
}
/**
* Set request. Get the account from the database.
*/
mainDB.findOneObject(mainDB.collectionAccount,
{
_id: mongodb.ObjectID(receivedObject.AccountID),
ClientID: existingClient.ClientID
},
undefined,
false,
function(err, existingAccount) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '301',
info: 'Database offline.'
});
return;
}
/**
* No hits from database.
*/
if (existingAccount === null) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '302',
info: 'No account match.'
},
'WARNING',
'Invalid AccountID or Account does not belong to client.');
return;
}
/**
* Check to ensure that the account has not already been deleted.
* Valid bitwise comparison.
*/
//jshint -W016
if (existingAccount.AccountStatus & utils.AccountDeleted) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '304',
info: 'Account has been deleted.'
},
'WARNING');
return;
}
if (existingAccount.AccountStatus & utils.AccountApiCreated) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '556',
info: 'Unsupported account type.'
},
'WARNING');
return;
}
//jshint +W016
/**
* Update the default account.
*/
mainDB.updateObject(mainDB.collectionDevice, {DeviceToken: receivedObject.DeviceToken}, {
$set: {
DefaultAccount: receivedObject.AccountID,
LastUpdate: timestamp
},
$inc: {LastVersion: 1}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '305',
info: 'Database offline.'
});
return;
}
/**
* Success!
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10045',
info: 'Account successfully set as default.'
},
'INFO',
('AccountID ' + receivedObject.AccountID + ' set as default.'));
});
});
});
};

View File

@ -0,0 +1,92 @@
/**
* @fileOverview Node.js Set Device Name Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Sets the name of a particular device if it belongs to the current client.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/registration_commands/setdevicename/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.js');
var mongodb = require('mongodb');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Local variables
*/
var timestamp = new Date();
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Update the device.
*/
mainDB.updateObject(mainDB.collectionDevice,
{
_id: mongodb.ObjectId(receivedObject.DeviceIndex),
ClientID: existingClient.ClientID
},
{
$set: {
DeviceName: receivedObject.DeviceName,
LastUpdate: timestamp
},
$inc: {LastVersion: 1}
},
{upsert: false}, false, function(err, result) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '439',
info: 'Database offline.'
});
return;
}
/**
* No hits from database if 0 or less.
*/
if (result.result.n <= 0) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '440',
info: 'Invalid device or device does not belong to client.'
},
'WARNING');
return;
}
/**
* Success.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10064',
info: 'Device name set.'
},
'INFO');
});
});
};

View File

@ -0,0 +1,136 @@
/**
* @fileOverview Node.js Suspend Device Handler for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*
* Suspends a particular device if it belongs to the current client.
* @see {@link http://10.0.10.242/w/tricore_architecture/server_interface/registration_commands/suspenddevice/}
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var log = require(global.pathPrefix + 'log.js');
var auth = require(global.pathPrefix + 'auth.js');
var utils = require(global.pathPrefix + 'utils.js');
var mongodb = require('mongodb');
/**
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
* For a full description of functionality, please see the link in fileOverview.
*
* @type {function} process
* @param {!object} res - Response object for returning information.
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?object} parameters - Input parameters posted on link.
* @param {?object} receivedObject - Input parameters in message body.
* @param {?object} hmacData - HMAC information from incoming packet.
*/
exports.process = function(res, functionInfo, parameters, receivedObject, hmacData) {
/**
* Local variables
*/
var timestamp = new Date();
/**
* Validate the session. This function responds directly if there is a problem.
*/
auth.validSession(res, receivedObject.DeviceToken, receivedObject.SessionToken, functionInfo, hmacData,
function(err, existingDevice, existingClient) {
if (err) {
return;
}
/**
* Find the device.
*/
mainDB.findOneObject(mainDB.collectionDevice,
{
_id: mongodb.ObjectId(receivedObject.DeviceIndex),
ClientID: existingClient.ClientID
},
undefined,
false,
function(err, device) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '428',
info: 'Database offline.'
});
return;
}
/**
* No hits from database.
*/
if (device === null) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '429',
info: 'Invalid device or device does not belong to client.'
},
'WARNING');
return;
}
/**
* Cannot suspend the device that is being used.
*/
if (device.DeviceNumber === existingDevice.DeviceNumber) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '430',
info: 'Cannot suspend the device currently in use.'
},
'WARNING');
return;
}
/**
* Device will not be suspended as it is already suspended.
* Valid bitwise operation.
*/
//jshint -W016
if (device.DeviceStatus & utils.DeviceSuspendedMask) {
//jshint +W016
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '437',
info: 'Device has already been suspended.'
},
'WARNING');
return;
}
/**
* The device can be suspended.
*/
mainDB.updateObject(mainDB.collectionDevice, {_id: mongodb.ObjectId(receivedObject.DeviceIndex)}, {
$bit: {
DeviceStatus: {or: utils.DeviceSuspendedMask}
},
$set: {
LastUpdate: timestamp
},
$inc: {LastVersion: 1}
},
{upsert: false}, false, function(err) {
if (err) {
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '431',
info: 'Database offline.'
});
return;
}
/**
* Success.
*/
auth.respond(res, 200, existingDevice, hmacData, functionInfo, {
code: '10062',
info: 'Device suspended.'
},
'INFO');
});
});
});
};

View File

@ -0,0 +1,322 @@
/**
* Unit testing file for ElevateSession command
*/
'use strict';
/* eslint max-nested-callbacks: ["error", 5] */
// eslint-disable-next-line no-unused-vars
const testGlobals = require('../../../tools/test/testGlobals.js');
const chai = require('chai');
const sinon = require('sinon');
const sinonChai = require('sinon-chai');
const chaiAsPromised = require('chai-as-promised');
const rewire = require('rewire');
/**
* Use `rewire` instead of require so that we can access private functions for test
*/
const elevateSession = rewire('../ElevateSession.js');
const authStub = elevateSession.__get__('authP');
const expect = chai.expect;
chai.use(sinonChai);
chai.use(chaiAsPromised);
/**
* Define a sample Client and Device object to return
*/
const DEVICE_TOKEN = 'abc123';
const SESSION_TOKEN = 'def456';
const CLIENT_EMAIL = 'a@example.com';
const PASSWORD = '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'; // "password"
const FAKE_CLIENT = {
ClientName: CLIENT_EMAIL
};
const FAKE_DEVICE = {};
/**
* Values for testing failures
*/
const NOT_DEVICE_TOKEN = 'ghi789';
const NOT_SESSION_TOKEN = 'jkl012';
const NOT_CLIENT_EMAIL = 'not-a@example.com';
const NOT_PASSWORD = '05f721989a4f70756a3b8387767affd11c776a0c863f1a22410855e606753321'; // "notpassword"
/**
* Define some fake parameters
*/
const fakeFunctionInfo = {};
const fakeParameters = {};
const fakeHmacData = [];
const res = sinon.spy();
describe('ElevateSession', () => {
describe('with valid parameters', () => {
let callP;
/**
* Before each test, set up the tests stubs to return the controlled data,
* then call the function we are testing.
*/
beforeEach(() => {
sinon.stub(authStub, 'validSession').resolves([FAKE_DEVICE, FAKE_CLIENT]);
sinon.stub(authStub, 'checkClientPassword').resolves('');
sinon.stub(authStub, 'respond').returns();
const testData = {
DeviceToken: DEVICE_TOKEN,
SessionToken: SESSION_TOKEN,
ClientName: CLIENT_EMAIL,
Password: PASSWORD
};
callP = elevateSession.process(res, fakeFunctionInfo, fakeParameters, testData, fakeHmacData);
});
/**
* After each tests, reset the stubs.
*/
afterEach(() => {
authStub.validSession.restore();
authStub.checkClientPassword.restore();
authStub.respond.restore();
});
it('runs', () => {
return expect(callP).to.eventually.be.fulfilled;
});
it('validates the session', () => {
return callP.then(() =>
expect(authStub.validSession).to.have.been
.calledOnce
.calledWith(res, DEVICE_TOKEN, SESSION_TOKEN)
);
});
it('checks the client password', () => {
return callP.then(() =>
expect(authStub.checkClientPassword).to.have.been
.calledOnce
.calledWith(PASSWORD, FAKE_CLIENT)
);
});
it('responds', () => {
return callP.then(() =>
expect(authStub.respond).to.have.been
.calledOnce
.calledWithMatch(
sinon.match.any,
sinon.match.any,
sinon.match.any,
sinon.match.any,
sinon.match.any,
sinon.match({
code: '10079',
info: 'Session Elevated.'
})
)
);
});
});
describe('without valid session', () => {
let callP;
/**
* Before each test, set up the tests stubs to return the controlled data,
* then call the function we are testing.
*/
beforeEach(() => {
sinon.stub(authStub, 'validSession').rejects();
sinon.stub(authStub, 'checkClientPassword').resolves('');
sinon.stub(authStub, 'respond').returns();
const testData = {
DeviceToken: NOT_DEVICE_TOKEN,
SessionToken: NOT_SESSION_TOKEN,
ClientName: CLIENT_EMAIL,
Password: PASSWORD
};
callP = elevateSession.process(res, fakeFunctionInfo, fakeParameters, testData, fakeHmacData);
});
/**
* After each tests, reset the stubs.
*/
afterEach(() => {
authStub.validSession.restore();
authStub.checkClientPassword.restore();
authStub.respond.restore();
});
it('runs', () => {
return expect(callP).to.eventually.be.fulfilled;
});
it('validates the session', () => {
return callP.then(() =>
expect(authStub.validSession).to.have.been
.calledOnce
.calledWith(res, NOT_DEVICE_TOKEN, NOT_SESSION_TOKEN)
);
});
it('fails before checking password', () => {
return callP.then(() =>
expect(authStub.checkClientPassword).to.not.have.been.called
);
});
it('does NOT respond (validSession deals with the response)', () => {
return callP.then(() =>
expect(authStub.respond).to.have.not.been.called
);
});
});
describe('with wrong email address', () => {
let callP;
/**
* Before each test, set up the tests stubs to return the controlled data,
* then call the function we are testing.
*/
beforeEach(() => {
sinon.stub(authStub, 'validSession').resolves([FAKE_DEVICE, FAKE_CLIENT]);
sinon.stub(authStub, 'checkClientPassword').resolves('');
sinon.stub(authStub, 'respond').returns();
const testData = {
DeviceToken: DEVICE_TOKEN,
SessionToken: SESSION_TOKEN,
ClientName: NOT_CLIENT_EMAIL,
Password: PASSWORD
};
callP = elevateSession.process(res, fakeFunctionInfo, fakeParameters, testData, fakeHmacData);
});
/**
* After each tests, reset the stubs.
*/
afterEach(() => {
authStub.validSession.restore();
authStub.checkClientPassword.restore();
authStub.respond.restore();
});
it('runs', () => {
return expect(callP).to.eventually.be.fulfilled;
});
it('validates the session', () => {
return callP.then(() =>
expect(authStub.validSession).to.have.been
.calledOnce
.calledWith(res, DEVICE_TOKEN, SESSION_TOKEN)
);
});
it('fails before checking password', () => {
return callP.then(() =>
expect(authStub.checkClientPassword).to.not.have.been.called
);
});
it('responds with correct error', () => {
return callP.then(() =>
expect(authStub.respond).to.have.been
.calledOnce
.calledWithMatch(
sinon.match.any,
sinon.match.any,
sinon.match.any,
sinon.match.any,
sinon.match.any,
sinon.match({
code: '559',
info: 'Invalid ClientName.'
})
)
);
});
});
describe('with wrong email password', () => {
let callP;
/**
* Before each test, set up the tests stubs to return the controlled data,
* then call the function we are testing.
*/
beforeEach(() => {
sinon.stub(authStub, 'validSession').resolves([FAKE_DEVICE, FAKE_CLIENT]);
sinon.stub(authStub, 'checkClientPassword').rejects({
code: 123,
message: 'One of many password errors'
});
sinon.stub(authStub, 'respond').returns();
const testData = {
DeviceToken: DEVICE_TOKEN,
SessionToken: SESSION_TOKEN,
ClientName: CLIENT_EMAIL,
Password: NOT_PASSWORD
};
callP = elevateSession.process(res, fakeFunctionInfo, fakeParameters, testData, fakeHmacData);
});
/**
* After each tests, reset the stubs.
*/
afterEach(() => {
authStub.validSession.restore();
authStub.checkClientPassword.restore();
authStub.respond.restore();
});
it('runs', () => {
return expect(callP).to.eventually.be.fulfilled;
});
it('validates the session', () => {
return callP.then(() =>
expect(authStub.validSession).to.have.been
.calledOnce
.calledWith(res, DEVICE_TOKEN, SESSION_TOKEN)
);
});
it('checks the client password', () => {
return callP.then(() =>
expect(authStub.checkClientPassword).to.have.been
.calledOnce
.calledWith(NOT_PASSWORD, FAKE_CLIENT)
);
});
it('responds with correct error', () => {
return callP.then(() =>
expect(authStub.respond).to.have.been
.calledOnce
.calledWithMatch(
sinon.match.any,
sinon.match.any,
sinon.match.any,
sinon.match.any,
sinon.match.any,
sinon.match({
code: '123',
info: 'One of many password errors'
})
)
);
});
});
});

View File

@ -0,0 +1,184 @@
/**
* Unit testing file for RedeemPaycode command
*/
'use strict';
/* eslint max-nested-callbacks: ["error", 5] */
// eslint-disable-next-line no-unused-vars
const testGlobals = require('../../../tools/test/testGlobals.js');
const chai = require('chai');
const sinon = require('sinon');
const sinonChai = require('sinon-chai');
const chaiAsPromised = require('chai-as-promised');
const rewire = require('rewire');
/**
* Use `rewire` instead of require so that we can access private functions for test
*/
const redeemPaycodeClass = rewire('../RedeemPayCode.js');
const authStub = redeemPaycodeClass.__get__('authP');
const implStub = redeemPaycodeClass.__get__('impl');
const expect = chai.expect;
chai.use(sinonChai);
chai.use(chaiAsPromised);
/**
* Define a sample Client and Device object to return
*/
const DEVICE_TOKEN = 'abc123';
const SESSION_TOKEN = 'def456';
const CLIENT_EMAIL = 'a@example.com';
const ACCOUNTID = '58e3a700f50f21000166b890';
const PAYCODE = 'KCT9A';
const MERCHANTCOMMENT = 'You were served today by Stuey.';
const REQUESTAMOUNT = 399;
const REQUESTTIP = 1;
const LATITUDE = 0.0;
const LONGITUDE = 0.0;
const FAKE_CLIENT = {
ClientName: CLIENT_EMAIL
};
const FAKE_DEVICE = {};
const SuccessReturn = {
code: '10020',
info: 'PayCode redeemed.',
TransactionID: '23N2O5D9'
};
const FailureReturn = {
code: '474',
info: 'DisplayName is invalid. Please fill out customer details.'
};
/**
* Define some fake parameters
*/
const fakeFunctionInfo = {};
const fakeParameters = {};
const fakeHmacData = [];
const res = sinon.spy();
describe('RedeemPaycode', () => {
describe('with valid parameters', () => {
let callP;
/**
* Before each test, set up the tests stubs to return the controlled data,
* then call the function we are testing.
*/
beforeEach(() => {
sinon.stub(authStub, 'validSession').resolves([FAKE_DEVICE, FAKE_CLIENT]);
sinon.stub(implStub, 'redeemPaycodeP').resolves(SuccessReturn);
sinon.stub(authStub, 'respond').returns();
const testData = {
DeviceToken: DEVICE_TOKEN,
SessionToken: SESSION_TOKEN,
AccountID: ACCOUNTID,
PayCode: PAYCODE,
MerchantComment: MERCHANTCOMMENT,
RequestAmount: REQUESTAMOUNT,
RequestTip: REQUESTTIP,
Latitude: LATITUDE,
Longitude: LONGITUDE
};
callP = redeemPaycodeClass.process(res, fakeFunctionInfo, fakeParameters, testData, fakeHmacData);
});
/**
* After each tests, reset the stubs.
*/
afterEach(() => {
authStub.validSession.restore();
authStub.respond.restore();
implStub.redeemPaycodeP.restore();
});
it('runs', () => {
return expect(callP).to.eventually.be.fulfilled;
});
it('validates the session', () => {
return callP.then(expect(authStub.validSession).to.have.been
.calledOnce
.calledWith(res, DEVICE_TOKEN, SESSION_TOKEN));
});
it('responds', () => {
return callP.then(() =>
expect(authStub.respond).to.have.been
.calledOnce
.calledWithMatch(
sinon.match.any,
sinon.match.any,
sinon.match.any,
sinon.match.any,
sinon.match.any,
sinon.match({
code: '10020',
info: 'PayCode redeemed.'
})
));
});
});
describe('reponds with WARNING', () => {
let callP;
/**
* Before each test, set up the tests stubs to return the controlled data,
* then call the function we are testing.
*/
beforeEach(() => {
sinon.stub(authStub, 'validSession').resolves([FAKE_DEVICE, FAKE_CLIENT]);
sinon.stub(implStub, 'redeemPaycodeP').rejects(FailureReturn);
sinon.stub(authStub, 'respond').returns();
const testData = {
DeviceToken: DEVICE_TOKEN,
SessionToken: SESSION_TOKEN,
AccountID: ACCOUNTID,
PayCode: PAYCODE,
MerchantComment: MERCHANTCOMMENT,
RequestAmount: REQUESTAMOUNT,
RequestTip: REQUESTTIP,
Latitude: LATITUDE,
Longitude: LONGITUDE
};
callP = redeemPaycodeClass.process(res, fakeFunctionInfo, fakeParameters, testData, fakeHmacData);
});
/**
* After each tests, reset the stubs.
*/
afterEach(() => {
authStub.validSession.restore();
authStub.respond.restore();
implStub.redeemPaycodeP.restore();
});
it('runs', () => {
return expect(callP).to.eventually.be.fulfilled;
});
it('responds', () => {
return callP.then(() =>
expect(authStub.respond).to.have.been
.calledOnce
.calledWithMatch(
sinon.match.any,
sinon.match.any,
sinon.match.any,
sinon.match.any,
sinon.match.any,
sinon.match({
code: '474',
info: 'DisplayName is invalid. Please fill out customer details.'
}),
sinon.match('WARNING')
));
});
});
});

View File

@ -0,0 +1,68 @@
/**
* @fileOverview Node.js Console Logging Functionality for Bridge Pay
* @preserve Copyright 2015 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*/
/**
* Includes
*/
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var config = require(global.configFile);
/**
* Object variables
*/
exports.verbose = 1; // Used to start/stop console logging. Set to 0 on release versions.
/**
* Writes info plus time stamp to both console and database. and log file.
* Entry type defines the message type: e.g. Info, Warning, Error, WWW etc.
*
* @see {@link http://10.0.10.242/w/tricore_architecture/database_design/collections/systemlog/}
*/
exports.system = function(entryClass, entryInfo, entryFunction, entryCode, entryUser, entrySource) {
/**
* @type {function} system
* @param {!string} entryClass - The type of event - e.g. INFO, ERROR, WARNING.
* @param {!string} entryInfo - A free text string giving more information on whatever happened.
* @param {!string} entryFunction - The function or module that generated the entry.
* @param {?string} entryCode - An error code if appropriate (blank otherwise).
* @param {?string} entryUser - The originating person 'client@me.com (0771823450)'.
* @param {!string} entrySource - The source that caused the event e.g. '86.118.232.2 (HTTPS:443)'.
*/
/**
* Create the log data structure.
*/
var logData = {};
logData.DateTime = new Date();
logData.ServerID = config.CCServerName + ' (VIP ' + config.CCServerIP + ')'; // Note the Virtual IP can be used by more than one box.
logData.Class = entryClass;
logData.Function = entryFunction;
logData.Code = entryCode;
logData.Info = entryInfo;
logData.User = entryUser;
logData.Source = entrySource;
/**
* If the system is in verbose mode, output to the console.
*/
if (exports.verbose) {
var consoleOutput = '[' + logData.DateTime.toISOString() + ' ' + logData.ServerID + '] ';
consoleOutput += logData.Class + ' (' + logData.Function;
if (entryCode !== '') {
consoleOutput += (', ' + entryCode);
}
consoleOutput += ') from ' + logData.User + ' at ' + logData.Source + ': ' + logData.Info;
console.log(consoleOutput);
}
/**
* Add the object to the system log.
*/
if (mainDB.dbOnline) {
mainDB.addObject(mainDB.collectionSystemLog, logData, undefined, false, null);
}
};

View File

@ -0,0 +1,15 @@
/**
* @file This file wraps the functions in mailer.js with promises for simpler
* use in promises and async/await
*/
const Q = require('q');
const mailer = require('./mailer.js');
module.exports = {
sendEmail: (...args) => Q.nfapply(mailer.sendEmail, args),
sendEmailByID: (...args) => Q.nfapply(mailer.sendEmailByID, args),
sendWelcomeEmail: (...args) => Q.nfapply(mailer.sendWelcomeEmail, args),
sendEmailChangedEmails: (...args) => Q.nfapply(mailer.sendEmailChangedEmails, args),
sendEmailRevertedEmails: (...args) => Q.nfapply(mailer.sendEmailRevertedEmails, args)
};

View File

@ -0,0 +1,341 @@
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Comcarde Node.js Mailer Functionality
// Provides -Bridge- pay functionality.
// Copyright 2014 Comcarde
// Written by Keith Symington
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Includes
var nodemailer = require('nodemailer');
var log = require(global.pathPrefix + 'log.js');
var Q = require('q');
var templates = require(global.pathPrefix + '../utils/templates.js');
var formattingUtils = require(global.pathPrefix + '../utils/formatting.js');
var references = require(global.pathPrefix + '../utils/references.js');
var debug = require('debug')('utils:mailer');
/**
* Set up the exports
*/
module.exports = {
sendEmail: sendEmail,
sendEmailByID: sendEmailByID,
sendWelcomeEmail: sendWelcomeEmail,
sendEmailChangedEmails: sendEmailChangedEmails,
sendEmailRevertedEmails: sendEmailRevertedEmails
};
/**
* Define the email transport options
*/
const TRANSPORTER = nodemailer.createTransport({
service: 'Gmail',
auth: {
user: 'admin@comcarde.com',
pass: 'xnasacgwvfvskvlj'
}
});
/**
* Generic function to send emails
*
* @param {String} mode - 'Test' to not send
* @param {String} destination - email address to send to
* @param {String} subject - email subject
* @param {String} htmlBody - email body in prepared HTML
* @param {String} caller - name of the caller for logging purposes
* @param {function} [next] - callback for success (if needed)
*/
function sendEmail(mode, destination, subject, htmlBody, caller, next) {
// Create the log data structure.
var mailOptions = {
from: 'admin@comcarde.com', // sender address
to: destination, // list of receivers
subject: subject, // Subject line
html: htmlBody // html body
};
if (mode !== 'Test') {
// Sent the e-mail using the transporter.
TRANSPORTER.sendMail(mailOptions, function(err, info) {
if (err) {
log.system(
'CRITICAL',
('Unable to send e-mail. ' + err),
caller,
'',
'System',
'127.0.0.1');
if (next) {
next(err);
}
} else {
log.system(
'INFO',
('E-mail sent to ' + destination + ' (' + info.response + ').'),
caller,
'',
'System',
'127.0.0.1');
if (next) {
next(null);
}
}
});
} else {
// Simply call back in test mode.
log.system(
'WARNING',
('E-mail test to ' + destination + ' (e-mail not sent).'),
caller,
'',
'System',
'127.0.0.1');
if (next) {
next(null);
}
}
}
/**
* Generic function to send emails to a client identified by an ID.
* This will lookup the client's email address from the database and then pass
* it on to the basic function for completion.
*
* @param {String} mode - 'Test' to not send
* @param {String} clientID - ID of the client to send email to
* @param {String} subject - email subject
* @param {String} htmlBody - email body in prepared HTML
* @param {String} caller - name of the caller for logging purposes
* @param {function} [next] - callback for success (if needed)
*/
function sendEmailByID(mode, clientID, subject, htmlBody, caller, next) {
references.getEmailAddress(clientID)
.then(function(email) {
sendEmail(mode, email, subject, htmlBody, caller, next);
})
.catch(function(err) {
log.system(
'CRITICAL',
('Unable to find client to send e-mail to. ' + err),
caller,
'',
'System',
'127.0.0.1');
if (next) {
next(err);
}
});
}
/**
* Sends the welcome email to the client.
*
* @param {Client} newClient - The newly added client
* @param {String} mode - 'test' to not actually send the email
* @param {String} caller - The name of the caller for logging purposed
* @param {Function} next - Callback function for callers who don't want promises
*
* @returns {Promise} - A promise for the result of sending the email
*/
function sendWelcomeEmail(newClient, mode, caller, next) {
//
// Get the email parameters
//
var token = newClient.EMailValidationToken;
var email = newClient.ClientName;
var query = {
code: token,
email: email
};
var confirmUrl = formattingUtils.formatPortalUrl('confirmemail-link', query);
var denyEmailUrl = formattingUtils.formatPortalUrl('denyemail-link', query);
debug('- send welcome email to: [%s], token [%s] ', email, token);
//
// Render the email
//
var htmlEmail = templates.render(
'bridge-welcome',
{
emailValidationCode: token,
confirmEmailUrl: confirmUrl,
denyEmailUrl: denyEmailUrl
});
var subject = 'Welcome to Bridge';
//
// Pass it to the mailer to send (wrapped in a Q.nfcall to turn it into
// a promise).
// When the promise completes, call any callback defined
//
return Q.nfcall(sendEmail, mode, email, subject, htmlEmail, caller)
.then(function() {
// Success has no return values
if (next) {
next();
}
})
.catch(function(err) {
if (next) {
next(err);
}
return Q.reject(err); // Pass on the error
});
}
/**
* Sends the emails related to an email change.
* This sends an email to the old address so they can revert if neccessary,
* and an address to the new address to confirm the email
*
* @param {String} oldEmail - the old email address being changed away from
* @param {String} newEmail - the new email address being changed to
* @param {Object} revertToken - The token to use to revert the email change
* @param {String} revertToken.token - the token
* @param {Object} confirmToken - The token to use to confirm the new email address
* @param {String} confirmToken.token - the token
* @param {String} mode - 'test' to not actually send the email
* @param {String} caller - The name of the caller for logging purposed
* @param {Function} next - Callback function for callers who don't want promises
*
* @returns {Promise} - A promise for the result of sending the email
*/
function sendEmailChangedEmails(oldEmail, newEmail, revertToken, confirmToken, mode, caller, next) {
//
// Build the urls.
//
var revertQuery = {
code: revertToken.token,
email: oldEmail
};
var confirmQuery = {
code: confirmToken.token,
email: newEmail
};
var baseRevertUrl = formattingUtils.formatPortalUrl('revert-changed-email-link');
var revertUrl = formattingUtils.formatPortalUrl('revert-changed-email-link', revertQuery);
var confirmChangeUrl = formattingUtils.formatPortalUrl('confirmemail-link', confirmQuery);
debug('- send email changed emails to: [%s] -> [%s] ', oldEmail, newEmail);
//
// Render the emails
//
var revertEmailBody = templates.render(
'email-changed-old',
{
oldEmail: oldEmail,
newEmail: newEmail,
revertEmailChangeUrl: revertUrl,
revertEmailChangeBaseUrl: baseRevertUrl,
revertValidationCode: revertQuery.code
});
var revertEmailSubject = 'Important: Email changed on Bridge Account';
var confirmEmailBody = templates.render(
'email-changed-new',
{
confirmChangedEmailUrl: confirmChangeUrl,
emailValidationCode: confirmQuery.code
});
var confirmEmailSubject = 'Please confirm your email address';
//
// Pass it to the mailer to send (wrapped in a Q.nfcall to turn it into
// a promise).
// When the promise completes, call any callback defined
//
var sendRevertEmail = Q.nfcall(
sendEmail,
mode,
oldEmail,
revertEmailSubject,
revertEmailBody,
caller
);
var sendConfirmEmail = Q.nfcall(
sendEmail,
mode,
newEmail,
confirmEmailSubject,
confirmEmailBody,
caller
);
return Q.all([sendRevertEmail, sendConfirmEmail])
.then(function() {
// Success has no return values
if (next) {
next();
}
})
.catch(function(err) {
if (next) {
next(err);
}
return Q.reject(err); // Pass on the error
});
}
/**
* Sends the emails related to an email change.
* This sends an email to the old address so they can revert if neccessary,
* and an address to the new address to confirm the email
*
* @param {String} revertToEmail - the old email address being reverted back to
* @param {String} revertFromEmail - the new email address being reverted from
* @param {String} mode - 'test' to not actually send the email
* @param {String} caller - The name of the caller for logging purposed
* @param {Function} next - Callback function for callers who don't want promises
*
* @returns {Promise} - A promise for the result of sending the email
*/
function sendEmailRevertedEmails(revertToEmail, revertFromEmail, mode, caller, next) {
debug('- send email reverted emails to: [%s] -> [%s] ', revertToEmail, revertFromEmail);
//
// Render the emails
//
var revertToEmailBody = templates.render('email-reverted-to', {});
var revertToEmailSubject = 'Email change reverted on Bridge Account';
var revertFromEmailBody = templates.render('email-reverted-from', {});
var revertFromEmailSubject = 'Email change reverted on Bridge Account';
//
// Pass it to the mailer to send (wrapped in a Q.nfcall to turn it into
// a promise).
// When the promise completes, call any callback defined
//
var sendRevertToEmail = Q.nfcall(
sendEmail,
mode,
revertToEmail,
revertToEmailSubject,
revertToEmailBody,
caller
);
var sendRevertFromEmail = Q.nfcall(
sendEmail,
mode,
revertFromEmail,
revertFromEmailSubject,
revertFromEmailBody,
caller
);
return Q.all([sendRevertToEmail, sendRevertFromEmail])
.then(function() {
// Success has no return values
if (next) {
next();
}
})
.catch(function(err) {
if (next) {
next(err);
}
return Q.reject(err); // Pass on the error
});
}

View File

@ -0,0 +1,76 @@
/**
* @file This file wraps the functions in mainDB.js with promises for simpler
* use in promises and async/await
*/
const Q = require('q');
const httpStatus = require('http-status-codes');
//
// We MUST require maindDB with the exact same path as where it is initialised or we
// end up with a different instance of it where the collections have not been initialised.
//
const mainDB = require(global.pathPrefix + 'mainDB.js');
const utils = require(global.pathPrefix + 'utils.js');
module.exports = {
findOneObject: (...args) => Q.nfapply(mainDB.findOneObject, args),
addObject: (...args) => Q.nfapply(mainDB.addObject, args),
addMany: (...args) => Q.nfapply(mainDB.addMany, args),
updateObject: (...args) => Q.nfapply(mainDB.updateObject, args),
removeObject: (...args) => Q.nfapply(mainDB.removeObject, args),
addObjectPWithCode: (...args) => withCode(module.exports.addObject, args),
findOneObjectPWithCode: (...args) => withCode(module.exports.findOneObject, args),
updateObjectPWithCode: (...args) => withCode(module.exports.updateObject, args),
removeObjectPWithCode: (...args) => withCode(module.exports.removeObject, args),
updateObjectPCheckObjectUpdated: (...args) => checkObjectUpdated(mainDB.updateObject, args),
/**
* Share the mainDB file for easy access to the collections
*/
mainDB
};
/**
* Wrapper functions that allows for specific error handling or promise functions
*
* @type {Function} withCode
* @param {!Function} action - function that this function has wrapped around
* @param {!Array} args - Options for the insert command. Use 'undefined' if there are none.
*/
function withCode(action, args) {
const code = args[args.length - 1];
const params = args.slice(0, args.length - 1);
return action(...params).catch(() =>
Q.reject(utils.createError(code, 'Database offline.', httpStatus.BAD_GATEWAY)));
}
/**
* Specific Wrapper for mongoDB update
* Handles general mongoDB errors and if the update fails to update any objects.
*
* @type {Function} checkObjectUpdated
* @param {!Function} action - function that this function has wrapped around
* @param {!Array} args - Options for the insert command. Use 'undefined' if there are none.
*/
function checkObjectUpdated(action, args) {
const code = args[args.length - 1];
const params = args.slice(0, args.length - 1);
return Q.nfcall(action, ...params)
.then((result) => {
if (result.result.nModified === 1) {
return Q.resolve(result);
} else {
return Q.reject(utils.createError(code, 'Failed to update object', httpStatus.CONFLICT));
}
})
.catch((error) => {
if (error.code && error.httpCode && error.message) {
return Q.reject(error);
} else {
return Q.reject(utils.createError(code, 'Database offline.', httpStatus.BAD_GATEWAY));
}
});
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,314 @@
/**
* @fileOverview Node.js Bridge Server data migrations functions
*/
'use strict';
var Q = require('q');
var _ = require('lodash');
var mongodb = require('mongodb');
var log = require(global.pathPrefix + 'log.js');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
module.exports = {
migrateClientNameToID: migrateClientNameToID
};
/**
* This function migrates the data such that the ClientName (i.e. email address)
* is no longer the foreign key in the related tables. This is changed to use
* the ClientID instead.
*/
function migrateClientNameToID() {
/**
* Find all the Clients without a ClientID, and give them one
*/
var query = {
ClientID: {$exists: false}
};
var projection = {
_id: 1
};
var deferAddComplete = Q.defer();
var addClientIdPromises = [];
mainDB.collectionClient.find(query)
.project(projection)
.forEach(
addIdToClient.bind(null, addClientIdPromises),
onAddIdDone.bind(null, deferAddComplete)
);
/**
* Wait for the iteration to complete,
* then wait for the updates to all complete,
* then start doing updates for all clients
*/
deferAddComplete.promise.then(function() {
Q.all(addClientIdPromises).then(function() {
doMigration();
});
});
}
/**
* Called for all clients that don't have an id.
* Used to add a unique id, then add all the other fields
*
* @param {Promise[]} promisesArray - array of promises to add our update to
* @param {Object} client - the value from the database
*/
function addIdToClient(promisesArray, client) {
var query = {
_id: client._id
};
var randomId = utils.timeBasedRandomCode();
var update = {
$set: {
ClientID: randomId
}
};
promisesArray.push(
mainDB.collectionClient.updateOne(
query,
update
));
}
/**
* Called when the foreach iterator has gone through every client that needs
* an ID added
*
* @param {Defer} defer - a deffered promise for the completion of the foreach
* @param {any} err - any errors while doing the foreach
*/
function onAddIdDone(defer, err) {
if (err) {
log.system(
'CRITICAL',
'failed to iterate all clients needing ids',
'migrations.migrateClientNameToID',
'',
'System',
'127.0.0.1');
defer.reject(err);
} else {
// All passed
defer.resolve();
}
}
function doMigration() {
log.system(
'INFO',
'Starting migration for ClientID',
'migrations.doMigration',
'',
'System',
'127.0.0.1');
/**
* Find all the Clients and update all the related tables
*/
var query = {
ClientID: {$exists: true}
};
var projection = {
ClientName: 1,
ClientID: 1
};
var deferUpdateComplete = Q.defer();
var updatePromises = [];
/**
* List of collections to change. Format is either:
* {String} - Name of the collection, with default ClientName -> ClientID conversion
* {String[]} - Name of collection, Name of existing Email field, Name of new ID field
*/
const collectionsToChange = [
'Account',
'AccountArchive',
'Addresses',
'AddressArchive',
'BridgeLogin',
'Device',
'DeviceArchive',
'Images',
'Items',
'Messages',
'MessagesArchive',
'PayCode',
['Transaction', 'CustomerClientName', 'CustomerClientID'],
['Transaction', 'MerchantClientName', 'MerchantClientID'],
['TransactionArchive', 'CustomerClientName', 'CustomerClientID'],
['TransactionArchive', 'MerchantClientName', 'MerchantClientID'],
'TransactionHistory'
];
/**
* Create bulk operations for all collections
*/
var ops = createBulkOps(collectionsToChange);
log.system(
'INFO',
'Created [' + Object.keys(ops).length + '] bulk operations',
'migrations.doMigration',
'',
'System',
'127.0.0.1');
mainDB.collectionClient.find(query)
.project(projection)
.forEach(
createClientOps.bind(null, collectionsToChange, ops),
runOps.bind(null,ops)
);
}
/**
* Create the bulk operations that we will update with all the changes we need
* to do.
*
* @param {(String|String[])[]} collectionsToChange - the list of collections to chnage
*
* @returns {Object} - object with key = collection name, value = UnorderedBulkOperation
*/
function createBulkOps(collectionsToChange) {
log.system(
'INFO',
'Initializing [' + collectionsToChange.length + '] bulk operations',
'migrations.createBulkOps',
'',
'System',
'127.0.0.1');
var ops = {};
for (var i = 0; i < collectionsToChange.length; ++i) {
var collName = collectionsToChange[i];
if (_.isArray(collName)) {
collName = collName[0];
}
var collection = mainDB['collection' + collName];
var bulkOp = collection.initializeUnorderedBulkOp();
ops[collName] = bulkOp;
log.system(
'INFO',
' - initialized bulk op for [' + collName + ']',
'migrations.createBulkOps',
'',
'System',
'127.0.0.1');
}
return ops;
}
/**
* Creates all the bulk operations
*
* @param {(String|String[])[]} collectionsToChange - the list of collections to change
* @param {UnorderedBulkOperation[]} ops - the list of operations to add to
* @param {Object} client - the client to create ops for
*/
function createClientOps(collectionsToChange, ops, client) {
log.system(
'INFO',
'Creating update ops for [' + client.ClientName + '] -> [' + client.ClientID + ']',
'migrations.createClientOps',
'',
'System',
'127.0.0.1');
for (var i = 0; i < collectionsToChange.length; ++i) {
var collName = collectionsToChange[i];
var srcName = 'ClientName';
var destName = 'ClientID';
if (_.isArray(collName)) {
collName = collectionsToChange[i][0];
srcName = collectionsToChange[i][1];
destName = collectionsToChange[i][2];
}
// Find all records that have the specified ClientName
var query = {};
query[srcName] = client.ClientName;
// Update with the relevant ClientID, and remove the ClientName
var update = {
$set: {},
$unset: {}
};
update.$unset[srcName] = '';
update.$set[destName] = client.ClientID;
ops[collName].find(query).update(update);
}
}
/**
* Run the update operations
*
* @param {UnorderedBulkOperation[]} ops - the list of operations to add to
* @param {varies} err - any errors
*/
function runOps(ops, err) {
if (err) {
log.system(
'CRITICAL',
'failed to create all bulk operations',
'migrations.runOps',
'',
'System',
'127.0.0.1');
return;
}
var opsPromises = [];
log.system(
'INFO',
'Executing [' + Object.keys(ops).length + '] bulk operation',
'migrations.runOps',
'',
'System',
'127.0.0.1');
/**
* Execute all the operations
*/
_.forEach(ops, function(val, key) {
log.system(
'INFO',
' - executing bulk operation for: ' + key,
'migrations.runOps',
'',
'System',
'127.0.0.1');
opsPromises.push(val.execute({
fsync: true
}));
});
Q.all(opsPromises)
.then(function() {
log.system(
'CRITICAL',
'Migration to ClientID complete successfully!',
'migrations.runOps',
'',
'System',
'127.0.0.1');
})
.catch(function(err) {
log.system(
'CRITICAL',
'failed to run all update operations',
'migrations.runOps',
'',
'System',
'127.0.0.1');
return Q.reject(err);
});
}

View File

@ -0,0 +1,99 @@
/**
* @fileOverview Rate limit functions for the app API
* @preserve Copyright 2016 Comcarde Ltd.
* @author Richard Taylor
* @see #bridge_server-core
*
* Defines rate limits for the app API, and a default rate limit for any other
* requests.
*/
var _ = require('lodash');
var RateLimit = require('express-rate-limit');
var config = require(global.configFile);
/**
* Mobiles are frequently behind NAT, so we combine IP address + port number
* to give a good approximation of who a remote connection is related to.
* Note that the firewalls all seem to populate different information, the remotePort
* being the most difficult to verify. Therefore the rate limiter will only work correctly
* if the connection is deemed secure.
*
* @param {object} req - The request object
*
* @returns {string} - The key to use to identify this client
*/
function rateLimitByIpAndPort(req) {
if (req.secure) {
/**
* If the request is coming from a secure source, we can trust the headers.
* Azure and Bluemix are special cases as the info is put elsewhere.
*/
switch (global.CURRENT_DEPLOYMENT_ENV) {
case 'Azure':
if (req.headers.hasOwnProperty('x-forwarded-for')) {
return (req.headers['x-forwarded-for'].split(':')[0] + '-' + req.headers['x-forwarded-for'].split(':')[1]);
}
break;
case 'Bluemix':
if (req.headers.hasOwnProperty('$wsra')) {
return (req.headers.$wsra + '-' + req.connection.remotePort);
}
break;
default:
break;
}
}
/**
* The request is not secure or no headers are present. The default action should be used.
* This may result in limiting problems in new environments if the configuration requires
* a special case as shown above.
*/
return (req.ip + '-' + req.connection.remotePort);
}
/*
* Rate limiting for the app API
* Warning: we must clone the value from config so that when we change the
* keyGenerator etc. it doesn't affect other places using the same
* config.
*/
var rateLimitConfig = _.clone(config.rateLimits.api);
rateLimitConfig.keyGenerator = rateLimitByIpAndPort;
rateLimitConfig.handler = jsonResponse;
var limiterApi = new RateLimit(rateLimitConfig);
/**
* Sends a JSON response if the limit is reached
*
* @param {Object} req - The request object
* @param {Object} res - The response object
*
*/
function jsonResponse(req, res) {
res.status(rateLimitConfig.statusCode).json({
code: 465,
info: 'Rate limit reached. Please wait and try again.'
});
}
/*
* Rate limiting for everything else
* Warning: we must clone the value from config so that when we change the
* keyGenerator etc. it doesn't affect other places using the same
* config.
*/
var rateLimitConfigDefault = _.clone(config.rateLimits.fallback);
rateLimitConfigDefault.keyGenerator = rateLimitByIpAndPort;
var limiterDefault = new RateLimit(rateLimitConfigDefault);
/**
* Function to insert the rate limiting middleware into the chain for the
* appropriate paths
*
* @param {object} server - The https server to connect the middleware to
*/
exports.enableLimits = function(server) {
server.use(/\/server_post/i, limiterApi);
server.use(/\/(?!server_post).*/i, limiterDefault); // Everything except /server_post
};

View File

@ -0,0 +1,11 @@
/**
* @file This file wraps the functions in mailer.js with promises for simpler
* use in promises and async/await
*/
const Q = require('q');
const sms = require('./sms.js');
module.exports = {
sendSMS: (...args) => Q.nfapply(sms.sendSMS, args)
};

121
node_server/ComServe/sms.js Normal file
View File

@ -0,0 +1,121 @@
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Comcarde Node.js SMS Compatibility with Textlocal.
// Provides -Bridge- pay functionality.
// Copyright 2014 Comcarde
// Written by Keith Symington
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Includes
var https = require('https');
var querystring = require('querystring');
var log = require(global.pathPrefix + 'log.js');
var adminNotifier = require(global.pathPrefix + '../utils/adminNotifier.js');
// SMS setup code.
exports.smsPart1 = '/send/?username=admin@comcarde.com&hash=0d0781473c9df47b3cce91d5f71d9d958eed6e3a&numbers=';
exports.smsPart2 = '&message=';
exports.smsPart3 = '&sender=Bridge';
exports.smsTest = '&test=true';
exports.smsTestMode = false;
exports.smsCredits = -1;
exports.adminMobile = '07713904702'; // Keith Symington
exports.backupMobile = '07789191413'; // Tom Mathews
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Sends an SMS message to defined number using a TextLocal account.
// Uses SSL. The function needs to be passed a callback. The callback
// will be given two arguments: the first is an error, the second
// is the number of texts remaining in the balance.
// This function is silent and does not log to console.
// The system should automatically add escape characters (%20) -
// if this fails the error 'No sender name was specified' will be returned via callback.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
exports.sendSMS = function(mode, number, message, next) {
// Local variables.
var fullQuery = '';
var returnedData = '';
// Check for test mode.
if (mode !== 'Test') {
// Check for available credits.
if (exports.smsCredits === 0) {
next('No SMS credits with Txtlocal.', 0);
} else {
// When in test mode, add the test identifier.
if (exports.smsTestMode) {
fullQuery = exports.smsPart1 + number + exports.smsPart2 + querystring.escape(message) + exports.smsPart3 + exports.smsTest;
} else {
fullQuery = exports.smsPart1 + number + exports.smsPart2 + querystring.escape(message) + exports.smsPart3;
}
// Set up query.
var smsOptions = {
host: 'api.txtlocal.com',
port: 443,
path: fullQuery
};
// Initiate the get request.
https.get(smsOptions, function(result) {
if (result.statusCode !== 200) {
next('HTTPS error when trying to reach Textlocal servers.', null);
} else {
// Data indicates that information is still arriving.
result.on('data', function(chunk) {
returnedData += chunk;
});
// End indicates that everything has been read.
result.on('end', function() { // Try to parse the data to see if it is JSON.
var returnedJSON = JSON.parse(returnedData);
// Check the status of the SMS send.
if (returnedJSON.status !== 'success') {
// SMS send failed.
next(returnedJSON.status, returnedJSON.balance);
} else {
// SMS send successful.
exports.smsCredits = returnedJSON.balance;
// Warning text for low credit. Throw away callback.
if (exports.smsCredits === 10) {
exports.sendSMS(null, (exports.adminMobile + ',' + exports.backupMobile),
'System Warning: Txtlocal SMS credits nearly exhausted.', function(err, smsBalance) {
// Check for errors.
if (err) {
log.system(
'CRITICAL',
('Cannot send SMS. ' + err),
'sms.sendSMS',
'',
'System',
'127.0.0.1');
} else {
log.system(
'ERROR',
('Txtlocal SMS credits now exhausted (' + smsBalance + ').'),
'sms.sendSMS',
'',
'System',
'127.0.0.1');
}
});
}
//
// Also notify via adminNotifier
//
adminNotifier.notifyCredits('txtlocal', returnedJSON.balance);
// Success!!!! Send callback.
next(null, returnedJSON.balance);
}
});
}
}).on('error', function(err) {
next(err, null);
});
}
} else {
// Simply call back in test mode.
next(null, null);
}
};

View File

@ -0,0 +1,178 @@
/**
* Unit testing file for mainDB
*/
'use strict';
/* eslint max-nested-callbacks: ["error", 5] */
// eslint-disable-next-line no-unused-vars
const testGlobals = require('../../tools/test/testGlobals.js');
const chai = require('chai');
const sinon = require('sinon');
const sinonChai = require('sinon-chai');
const chaiAsPromised = require('chai-as-promised');
const rewire = require('rewire');
const httpStatus = require('http-status-codes');
/**
* Use `rewire` instead of require so that we can access private functions for test
*/
const mainDBPromisesClass = rewire('../mainDB-promises.js');
const maindDBStub = mainDBPromisesClass.__get__('mainDB');
const sandbox = sinon.createSandbox();
const expect = chai.expect;
chai.use(sinonChai);
chai.use(chaiAsPromised);
describe('mainDB', () => {
/**
* After each tests, reset the stubs.
*/
afterEach(() => {
sandbox.restore();
});
describe('calls "checkObjectUpdated"', () => {
describe('which succeeds', () => {
let returnValue;
/**
* Before each test, set up the tests stubs to return the controlled data,
* then call the function we are testing.
*/
beforeEach(async () => {
sandbox.stub(maindDBStub, 'updateObject').callsArgWith(4, null, {result: {nModified: 1}});
returnValue = await mainDBPromisesClass.updateObjectPCheckObjectUpdated(1, 2, 3, 4, 5);
});
it('called with correct params', () => {
return expect(maindDBStub.updateObject)
.calledOnce
.calledWith(
1, 2, 3, 4
);
});
it('it returns with the return value', () => {
return expect(returnValue).eql(
{
result: {
nModified: 1
}
}
);
});
});
describe('which fails', () => {
describe('general monoDB error', () => {
let errorValue;
beforeEach(async () => {
sandbox.stub(maindDBStub, 'updateObject').callsArgWith(4, 'mongo error');
try {
await mainDBPromisesClass.updateObjectPCheckObjectUpdated(1, 2, 3, 4, 5);
} catch (error) {
errorValue = error;
}
});
it('called with correct params', () => {
return expect(maindDBStub.updateObject)
.calledOnce
.calledWith(
1, 2, 3, 4
);
});
it('it returns with correct error code', () => {
return expect(errorValue).to.eql({
code: 5,
message: 'Database offline.',
httpCode: httpStatus.BAD_GATEWAY
});
});
});
describe('failed to update any objects', () => {
let errorValue;
beforeEach(async () => {
sandbox.stub(maindDBStub, 'updateObject').callsArgWith(4, null, {result: {nModified: 0}});
try {
await mainDBPromisesClass.updateObjectPCheckObjectUpdated(1, 2, 3, 4, 5);
} catch (error) {
errorValue = error;
}
});
it('called with correct params', () => {
return expect(maindDBStub.updateObject)
.calledOnce
.calledWith(
1, 2, 3, 4
);
});
it('it returns with correct error code', () => {
return expect(errorValue).to.eql({
code: 5,
message: 'Failed to update object',
httpCode: httpStatus.CONFLICT
});
});
});
});
});
describe('calls "withCode"', () => {
describe('which succeeds', () => {
let returnValue;
/**
* Before each test, set up the tests stubs to return the controlled data,
* then call the function we are testing.
*/
beforeEach(async () => {
sandbox.stub(mainDBPromisesClass, 'addObject').resolves({aValue: 5});
returnValue = await mainDBPromisesClass.addObjectPWithCode(1, 2, 3, 4, 5);
});
it('called with correct params', () => {
return expect(mainDBPromisesClass.addObject)
.calledOnce
.calledWith(
1, 2, 3, 4
);
});
it('it returns with the return value', () => {
return expect(returnValue).to.eql(
{aValue: 5}
);
});
});
describe('which fails', () => {
let errorValue;
/**
* Before each test, set up the tests stubs to return the controlled data,
* then call the function we are testing.
*/
beforeEach(async () => {
sandbox.stub(mainDBPromisesClass, 'addObject').rejects();
try {
await mainDBPromisesClass.addObjectPWithCode(1, 2, 3, 4, 5);
} catch (error) {
errorValue = error;
}
});
it('called with correct params', () => {
return expect(mainDBPromisesClass.addObject)
.calledOnce
.calledWith(
1, 2, 3, 4
);
});
it('it returns with correct error code', () => {
return expect(errorValue).to.eql({
code: 5,
message: 'Database offline.',
httpCode: httpStatus.BAD_GATEWAY
});
});
});
});
});

View File

@ -0,0 +1,685 @@
/* eslint-disable max-nested-callbacks */
/* eslint-disable mocha/no-hooks-for-single-case */
/* eslint-disable max-len*/
'use strict';
const chai = require('chai');
const sinonChai = require('sinon-chai');
const chaiAsPromised = require('chai-as-promised');
const rewire = require('rewire');
// eslint-disable-next-line import/no-unassigned-import
require('../../tools/test/testGlobals');
const utils = rewire('../utils');
const expect = chai.expect;
chai.use(sinonChai);
chai.use(chaiAsPromised);
const DATA_STRING = 'someData';
const KEY = '1';
const CLIENT_ID = '123456789012345678901234';
const INVALID_CLIENT_ID = '1234567890123456789012345';
const DIFFERENT_CLIENT_ID = '123456789012345678900000';
const ENCRYPTED_DATA = '3::9b357eaf82b6e86f8bfc8761a6fd2e3f6787b4e96f7e228388fb70a8e1d5b6f9a0f7ea03f3e9f19684a6c2cd97ebeba85d45c77c05c2f3b9adba878f84b86a95';
const ENCRYPTED_DATA_BAD_FORMAT = '3117354dce8e95918cc6cd5ad7932d258b2dd9558e0a613a2825fe54d36528d22c58c9a75c4d62c8adabc73a345f31c899a9cf9b12ee66489cd9b71f29b1fa2cf81';
const ENCRYPTED_DATA_WRONG_VERSION = '1::7354dce8e95918cc6cd5ad7932d258b2dd9558e0a613a2825fe54d36528d22c58c9a75c4d62c8adabc73a345f31c899a9cf9b12ee66489cd9b71f29b1fa2cf81';
describe('ComServe.utils', () => {
describe('calls encryptDataV3', () => {
it('returns a different value when encrypting identical data', () => {
const firstValue = utils.encryptDataV3(DATA_STRING, KEY, CLIENT_ID);
const secondValue = utils.encryptDataV3(DATA_STRING, KEY, CLIENT_ID);
return expect(firstValue).to.not.equal(secondValue);
});
describe('returns with an error', () => {
it('Nothing to encrypt.', () => {
const returnValue = utils.encryptDataV3(null, KEY, CLIENT_ID);
return expect(returnValue).to.deep.equal(utils.createError(1, 'Nothing to encrypt.'));
});
it('No client key.', () => {
const returnValue = utils.encryptDataV3(DATA_STRING, null, CLIENT_ID);
return expect(returnValue).to.deep.equal(utils.createError(2, 'No client key.'));
});
it('No client ID.', () => {
const returnValue = utils.encryptDataV3(DATA_STRING, KEY, null);
return expect(returnValue).to.deep.equal(utils.createError(3, 'No client ID.'));
});
it('Invalid client ID.', () => {
const returnValue = utils.encryptDataV3(DATA_STRING, KEY, INVALID_CLIENT_ID);
return expect(returnValue).to.deep.equal(utils.createError(3, 'Client Id length must be 24'));
});
it('Data to encrypt must be a string.', () => {
const returnValue = utils.encryptDataV3({}, KEY, CLIENT_ID);
return expect(returnValue).to.deep.equal(utils.createError(4, 'Data to encrypt must be a string.'));
});
it('Client key must be a string.', () => {
const returnValue = utils.encryptDataV3(DATA_STRING, {}, CLIENT_ID);
return expect(returnValue).to.deep.equal(utils.createError(5, 'Client key must be a string.'));
});
it('Client Key must be in Hex', () => {
const returnValue = utils.encryptDataV3(DATA_STRING, 'z', CLIENT_ID);
return expect(returnValue).to.deep.equal(utils.createError(5, 'Client Key must be in Hex'));
});
it('Client ID must be a string.', () => {
const returnValue = utils.encryptDataV3(DATA_STRING, KEY, {});
return expect(returnValue).to.deep.equal(utils.createError(6, 'Client ID must be a string.'));
});
});
});
describe('calls decryptDataV3', () => {
it('returns the correct decrypted data after encrypting the data', () => {
const decryptedData = utils.decryptDataV3(ENCRYPTED_DATA, KEY, CLIENT_ID);
return expect(decryptedData).to.equal(DATA_STRING);
});
describe('returns with an error', () => {
it('Nothing to decrypt.', () => {
const returnValue = utils.decryptDataV3(null, KEY, CLIENT_ID);
return expect(returnValue).to.deep.equal(utils.createError(1, 'Nothing to decrypt.'));
});
it('No client key.', () => {
const returnValue = utils.decryptDataV3(ENCRYPTED_DATA, null, CLIENT_ID);
return expect(returnValue).to.deep.equal(utils.createError(2, 'No client key.'));
});
it('No client ID.', () => {
const returnValue = utils.decryptDataV3(ENCRYPTED_DATA, KEY, null);
return expect(returnValue).to.deep.equal(utils.createError(3, 'No client ID.'));
});
it('Data to encrypt must be a string.', () => {
const returnValue = utils.decryptDataV3({}, KEY, CLIENT_ID);
return expect(returnValue).to.deep.equal(utils.createError(4, 'Data to be decrypted must be a string.'));
});
it('Client key must be a string.', () => {
const returnValue = utils.decryptDataV3(ENCRYPTED_DATA, {}, CLIENT_ID);
return expect(returnValue).to.deep.equal(utils.createError(5, 'Client key must be a string.'));
});
it('Client ID must be a string.', () => {
const returnValue = utils.decryptDataV3(ENCRYPTED_DATA, KEY, {});
return expect(returnValue).to.deep.equal(utils.createError(6, 'Client ID must be a string.'));
});
it('Encrypted data did not contain the 2 expected elements.', () => {
const returnValue = utils.decryptDataV3(ENCRYPTED_DATA_BAD_FORMAT, KEY, CLIENT_ID);
return expect(returnValue).to.deep.equal(utils.createError(7, 'Encrypted data did not contain the 2 expected elements.'));
});
it('Unexpected encryption version.', () => {
const returnValue = utils.decryptDataV3(ENCRYPTED_DATA_WRONG_VERSION, KEY, CLIENT_ID);
return expect(returnValue).to.deep.equal(utils.createError(8, 'Unexpected encryption version.'));
});
it('Information does not belong to client.', () => {
const returnValue = utils.decryptDataV3(ENCRYPTED_DATA, KEY, DIFFERENT_CLIENT_ID);
return expect(returnValue).to.deep.equal(utils.createError(12, 'Information does not belong to client.'));
});
});
});
describe('calls identifyCard', () => {
it('returns ISO / TC68 Card', () => {
const cardDetails = utils.identifyCard('0000000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '0*** **** **** 0000',
type: 'ISO / TC68 Card',
icon: 'Generic-card.png'
});
});
it('returns Airline/UATP', () => {
const cardDetails = utils.identifyCard('1000000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '1*** **** **** 0000',
type: 'Airline/UATP',
icon: 'Generic-card.png'
});
});
it('returns Diners Club enRoute, try 1', () => {
const cardDetails = utils.identifyCard('2014000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '2*** **** **** 0000',
type: 'Diners Club enRoute',
icon: 'Diners-Generic.png'
});
});
it('returns Diners Club enRoute, try 2', () => {
const cardDetails = utils.identifyCard('2149000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '2*** **** **** 0000',
type: 'Diners Club enRoute',
icon: 'Diners-Generic.png'
});
});
it('returns MIR, try 1', () => {
const cardDetails = utils.identifyCard('2204000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '2*** **** **** 0000',
type: 'MIR',
icon: 'MIR.png'
});
});
it('returns MIR, try 2', () => {
const cardDetails = utils.identifyCard('2200000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '2*** **** **** 0000',
type: 'MIR',
icon: 'MIR.png'
});
});
it('returns Mastercard, try 1', () => {
const cardDetails = utils.identifyCard('2720000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '2*** **** **** 0000',
type: 'Mastercard',
icon: 'MASTERCARD_CREDIT.png'
});
});
it('returns Mastercard, try 2', () => {
const cardDetails = utils.identifyCard('2221000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '2*** **** **** 0000',
type: 'Mastercard',
icon: 'MASTERCARD_CREDIT.png'
});
});
it('returns Airline/UATP/Other Card', () => {
const cardDetails = utils.identifyCard('2000000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '2*** **** **** 0000',
type: 'Airline/UATP/Other Card',
icon: 'Generic-card.png'
});
});
it('returns American Express, try 1', () => {
const cardDetails = utils.identifyCard('3400000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '3*** **** **** 0000',
type: 'American Express',
icon: 'AMEX.png'
});
});
it('returns American Express, try 2', () => {
const cardDetails = utils.identifyCard('3700000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '3*** **** **** 0000',
type: 'American Express',
icon: 'AMEX.png'
});
});
it('returns Diners Club International, try 1', () => {
const cardDetails = utils.identifyCard('3600000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '3*** **** **** 0000',
type: 'Diners Club International',
icon: 'DINERS.png'
});
});
it('returns Diners Club International, try 2', () => {
const cardDetails = utils.identifyCard('3800000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '3*** **** **** 0000',
type: 'Diners Club International',
icon: 'DINERS.png'
});
});
it('returns Diners Club International, try 3', () => {
const cardDetails = utils.identifyCard('3900000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '3*** **** **** 0000',
type: 'Diners Club International',
icon: 'DINERS.png'
});
});
it('returns Diners Club International, try 4', () => {
const cardDetails = utils.identifyCard('3090000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '3*** **** **** 0000',
type: 'Diners Club International',
icon: 'DINERS.png'
});
});
it('returns Diners Club, try 1', () => {
const cardDetails = utils.identifyCard('3000000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '3*** **** **** 0000',
type: 'Diners Club',
icon: 'DINERS.png'
});
});
it('returns Diners Club, try 2', () => {
const cardDetails = utils.identifyCard('3050000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '3*** **** **** 0000',
type: 'Diners Club',
icon: 'DINERS.png'
});
});
it('returns JCB. try 1', () => {
const cardDetails = utils.identifyCard('3589000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '3*** **** **** 0000',
type: 'JCB',
icon: 'JCB.png'
});
});
it('returns JCB. try 2', () => {
const cardDetails = utils.identifyCard('3528000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '3*** **** **** 0000',
type: 'JCB',
icon: 'JCB.png'
});
});
it('returns Other Card, try 1', () => {
const cardDetails = utils.identifyCard('3590000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '3*** **** **** 0000',
type: 'Other Card',
icon: 'Generic-card.png'
});
});
it('returns Dankort, try 1', () => {
const cardDetails = utils.identifyCard('4175000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '4*** **** **** 0000',
type: 'Dankort',
icon: 'Dankort.png'
});
});
it('returns Dankort, try 2', () => {
const cardDetails = utils.identifyCard('4571000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '4*** **** **** 0000',
type: 'Dankort',
icon: 'Dankort.png'
});
});
it('returns Maestro , try 1', () => {
const cardDetails = utils.identifyCard('4903000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '4*** **** **** 0000',
type: 'Maestro',
icon: 'MAESTRO.png'
});
});
it('returns Maestro , try 2', () => {
const cardDetails = utils.identifyCard('4905000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '4*** **** **** 0000',
type: 'Maestro',
icon: 'MAESTRO.png'
});
});
it('returns Maestro , try 3', () => {
const cardDetails = utils.identifyCard('4911000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '4*** **** **** 0000',
type: 'Maestro',
icon: 'MAESTRO.png'
});
});
it('returns Maestro , try 4', () => {
const cardDetails = utils.identifyCard('4936000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '4*** **** **** 0000',
type: 'Maestro',
icon: 'MAESTRO.png'
});
});
it('returns Visa', () => {
const cardDetails = utils.identifyCard('4000000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '4*** **** **** 0000',
type: 'Visa',
icon: 'VISA_CREDIT.png'
});
});
it('returns MasterCard, try 1', () => {
const cardDetails = utils.identifyCard('5100000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'MasterCard',
icon: 'MASTERCARD_CREDIT.png'
});
});
it('returns MasterCard, try 2', () => {
const cardDetails = utils.identifyCard('5200000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'MasterCard',
icon: 'MASTERCARD_CREDIT.png'
});
});
it('returns MasterCard, try 3', () => {
const cardDetails = utils.identifyCard('5300000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'MasterCard',
icon: 'MASTERCARD_CREDIT.png'
});
});
it('returns MasterCard, try 4', () => {
const cardDetails = utils.identifyCard('5400000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'MasterCard',
icon: 'MASTERCARD_CREDIT.png'
});
});
it('returns MasterCard, try 5', () => {
const cardDetails = utils.identifyCard('5500000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'MasterCard',
icon: 'MASTERCARD_CREDIT.png'
});
});
it('returns Maestro, try 1', () => {
const cardDetails = utils.identifyCard('5000000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'Maestro',
icon: 'MAESTRO.png'
});
});
it('returns Maestro, try 2', () => {
const cardDetails = utils.identifyCard('5600000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'Maestro',
icon: 'MAESTRO.png'
});
});
it('returns Maestro, try 3', () => {
const cardDetails = utils.identifyCard('5700000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'Maestro',
icon: 'MAESTRO.png'
});
});
it('returns Maestro, try 4', () => {
const cardDetails = utils.identifyCard('5800000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'Maestro',
icon: 'MAESTRO.png'
});
});
it('returns Dankort', () => {
const cardDetails = utils.identifyCard('5019000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'Dankort',
icon: 'Dankort.png'
});
});
it('returns Bankcard, try 1', () => {
const cardDetails = utils.identifyCard('5610000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'Bankcard',
icon: 'Generic-card.png'
});
});
it('returns CardGuard', () => {
const cardDetails = utils.identifyCard('5392000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'CardGuard',
icon: 'Generic-card.png'
});
});
it('returns Maestro, try 5', () => {
const cardDetails = utils.identifyCard('5641820000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'Maestro',
icon: 'MAESTRO.png'
});
});
it('returns Bankcard, try 2', () => {
const cardDetails = utils.identifyCard('5602210000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'Bankcard',
icon: 'Generic-card.png'
});
});
it('returns Bankcard, try 3', () => {
const cardDetails = utils.identifyCard('5602250000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'Bankcard',
icon: 'Generic-card.png'
});
});
it('returns Verve, try 1', () => {
const cardDetails = utils.identifyCard('5060990000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'Verve',
icon: 'Generic-card.png'
});
});
it('returns Verve, try 2', () => {
const cardDetails = utils.identifyCard('5061980000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'Verve',
icon: 'Generic-card.png'
});
});
it('returns Other Card, try 2', () => {
const cardDetails = utils.identifyCard('5900000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '5*** **** **** 0000',
type: 'Other Card',
icon: 'Generic-card.png'
});
});
it('returns China UnionPay', () => {
const cardDetails = utils.identifyCard('6200000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'China UnionPay',
icon: 'Generic-card.png'
});
});
it('returns Discover Card, try 1', () => {
const cardDetails = utils.identifyCard('6500000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'Discover Card',
icon: 'Discover-card.png'
});
});
it('returns InstaPayment, try 1', () => {
const cardDetails = utils.identifyCard('6370000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'InstaPayment',
icon: 'Generic-card.png'
});
});
it('returns InstaPayment, try 2', () => {
const cardDetails = utils.identifyCard('6380000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'InstaPayment',
icon: 'Generic-card.png'
});
});
it('returns InstaPayment, try 3', () => {
const cardDetails = utils.identifyCard('6390000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'InstaPayment',
icon: 'Generic-card.png'
});
});
it('returns Discover Card. try 2', () => {
const cardDetails = utils.identifyCard('6440000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'Discover Card',
icon: 'Discover-card.png'
});
});
it('returns Discover Card. try 3', () => {
const cardDetails = utils.identifyCard('6490000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'Discover Card',
icon: 'Discover-card.png'
});
});
it('returns InterPayment Card', () => {
const cardDetails = utils.identifyCard('6360000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'InterPayment Card',
icon: 'Generic-card.png'
});
});
it('returns Discover Card. try 4', () => {
const cardDetails = utils.identifyCard('6011000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'Discover Card',
icon: 'Discover-card.png'
});
});
it('returns Laser, try 1', () => {
const cardDetails = utils.identifyCard('6304000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'Laser',
icon: 'Generic-card.png'
});
});
it('returns Laser, try 2', () => {
const cardDetails = utils.identifyCard('6706000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'Laser',
icon: 'Generic-card.png'
});
});
it('returns Laser, try 3', () => {
const cardDetails = utils.identifyCard('6771000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'Laser',
icon: 'Generic-card.png'
});
});
it('returns Laser, try 4', () => {
const cardDetails = utils.identifyCard('6709000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'Laser',
icon: 'Generic-card.png'
});
});
it('returns Solo, try 1', () => {
const cardDetails = utils.identifyCard('6334000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'Solo',
icon: 'Generic-card.png'
});
});
it('returns Solo, try 2', () => {
const cardDetails = utils.identifyCard('6767000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'Solo',
icon: 'Generic-card.png'
});
});
it('returns Maestro, try 6', () => {
const cardDetails = utils.identifyCard('6333000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'Maestro',
icon: 'MAESTRO.png'
});
});
it('returns Maestro, try 7', () => {
const cardDetails = utils.identifyCard('6759000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'Maestro',
icon: 'MAESTRO.png'
});
});
it('returns Verve, try 3', () => {
const cardDetails = utils.identifyCard('6500020000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'Verve',
icon: 'Generic-card.png'
});
});
it('returns Verve, try 4', () => {
const cardDetails = utils.identifyCard('6500270000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'Verve',
icon: 'Generic-card.png'
});
});
it('returns Discover Card. try 5', () => {
const cardDetails = utils.identifyCard('6221260000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'Discover Card',
icon: 'Discover-card.png'
});
});
it('returns Discover Card. try 6', () => {
const cardDetails = utils.identifyCard('6229250000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'Discover Card',
icon: 'Discover-card.png'
});
});
it('returns Maestro, try 8', () => {
const cardDetails = utils.identifyCard('6666660000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '6*** **** **** 0000',
type: 'Maestro',
icon: 'MAESTRO.png'
});
});
it('returns Petroleum / Other Card', () => {
const cardDetails = utils.identifyCard('7000000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '7*** **** **** 0000',
type: 'Petroleum / Other Card',
icon: 'Generic-card.png'
});
});
it('returns Health / Telco / Other Card', () => {
const cardDetails = utils.identifyCard('8000000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '8*** **** **** 0000',
type: 'Health / Telco / Other Card',
icon: 'Generic-card.png'
});
});
it('returns National / Other Card', () => {
const cardDetails = utils.identifyCard('9000000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: '9*** **** **** 0000',
type: 'National / Other Card',
icon: 'Generic-card.png'
});
});
it('returns error invalid card', () => {
const cardDetails = utils.identifyCard('a000000000000000');
return expect(cardDetails).to.deep.equal({
hiddenString: 'a*** **** **** 0000',
type: 'Invalid Card',
icon: 'Generic-card.png'
});
});
});
});

View File

@ -0,0 +1,577 @@
/* globals describe, beforeEach, afterEach, it */
/**
* Unit testing file for the validation code
*/
'use strict';
// eslint-disable-next-line no-unused-vars
const testGlobals = require('../../tools/test/testGlobals.js');
const chai = require('chai');
const valid = require('../valid.js');
const expect = chai.expect;
//
// Array of test cases for testing MerchantInvoice validation
// Note that all we need is the RequestAmount and the MerchantInvoice itself as
// this is NOT validating the whole RedeemPaycode (or other function) details.
// See http://10.0.10.242/T1235#30055 for the rules
//
const merchantInvoiceBasic = [
{
name: 'no merchant invoice',
valid: true,
data: {
RequestAmount: 123
}
}
];
//
// Tests where Items used for MerchantInvoice are on a NET + VAT basis
//
const merchantInvoiceNet = [
{
name: 'rounding down line',
valid: true,
data: {
RequestAmount: 2,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: 2, // 2p
Item_GrossAmount: null, // using Net
Item_Quantity: 1,
Line_VATAmount: 0, // 2p * 20% = 0.4p = round down to 0
Line_TotalAmount: 2 // Net + VAT
}
]
}
},
{
name: 'rounding up line',
valid: true,
data: {
RequestAmount: 4,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: 3, // 3p
Item_GrossAmount: null, // using Net
Item_Quantity: 1,
Line_VATAmount: 1, // 3p * 20% = 0.6p = round up to 1
Line_TotalAmount: 4 // Net + VAT
}
]
}
},
{
name: '2 items that would individually round down, but round up in total',
valid: true,
data: {
RequestAmount: 5,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: 2, // 2p
Item_GrossAmount: null, // using Net
Item_Quantity: 2,
Line_VATAmount: 1, // 2p * 2 * 20% = 0.8p = round up to 1
Line_TotalAmount: 5 // Net * 2 + VAT
}
]
}
},
{
name: '2 items that would individually round up, but round down in total',
valid: true,
data: {
RequestAmount: 7,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: 3, // 3p
Item_GrossAmount: null, // using Net
Item_Quantity: 2,
Line_VATAmount: 1, // 3p * 2 * 20% = 1.2p = round down to 1
Line_TotalAmount: 7 // Net * 2 + VAT
}
]
}
},
//
// Cases that should fail
//
{
name: 'line INCORRECTLY rounded up',
valid: false,
data: {
RequestAmount: 3,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: 2, // 2p
Item_GrossAmount: null, // using Net
Item_Quantity: 1,
Line_VATAmount: 1, // should be 2p * 20% = 0.4p = round down to 0
Line_TotalAmount: 3 // Net + VAT
}
]
}
},
{
name: 'line INCORRECTLY rounded down',
valid: false,
data: {
RequestAmount: 3,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: 3, // 3p
Item_GrossAmount: null, // using Net
Item_Quantity: 1,
Line_VATAmount: 0, // Should be 3p * 20% = 0.6p = round up to 1
Line_TotalAmount: 3 // Net + VAT
}
]
}
},
{
name: '2 items that would individually round down but round up in total, being INCORRECTLY rounded down',
valid: false,
data: {
RequestAmount: 4,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: 2, // 2p
Item_GrossAmount: null, // using Net
Item_Quantity: 2,
Line_VATAmount: 0, // should be 2p * 2 * 20% = 0.8p = round up to 1
Line_TotalAmount: 4 // Net * 2 + VAT
}
]
}
},
{
name: '2 items that would individually round up but round down in total, being INCORRECTLY rounded up',
valid: false,
data: {
RequestAmount: 6,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: 3, // 3p
Item_GrossAmount: null, // using Net
Item_Quantity: 2,
Line_VATAmount: 0, // should be 3p * 2 * 20% = 1.2p = round down to 1
Line_TotalAmount: 6 // Net * 2 + VAT
}
]
}
}
];
//
// Tests where Items used for MerchantInvoice are on a GROSS basis
//
const merchantInvoiceGross = [
{
name: 'rounding down line',
valid: true,
data: {
RequestAmount: 2,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: null, // using gross
Item_GrossAmount: 2, // 2p
Item_Quantity: 1,
Line_VATAmount: 0, // 2p - (2p / 120%) = (2p - 1.666...) = 0.22... = round down to 0
Line_TotalAmount: 2 // Gross * count
}
]
}
},
{
name: 'rounding up line',
valid: true,
data: {
RequestAmount: 3,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: null, // using gross
Item_GrossAmount: 3, // 3p
Item_Quantity: 1,
Line_VATAmount: 1, // 3p - (3p/120%) = (3p - 2.5p) = 0.5p = round up to 1
Line_TotalAmount: 3 // Gross * count
}
]
}
},
{
name: '2 items that would individually round down, but round up in total',
valid: true,
data: {
RequestAmount: 4,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: null, // using gross
Item_GrossAmount: 2, // 2p
Item_Quantity: 2,
Line_VATAmount: 1, // 4p - (4p/120%) = (4p - 3.33) = 0.66..p = round up to 1
Line_TotalAmount: 4 // Gross * 2
}
]
}
},
{
name: '2 items that would individually round up, but are exact pennies in total',
valid: true,
data: {
RequestAmount: 6,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: null, // using gross
Item_GrossAmount: 3, // 3p
Item_Quantity: 2,
Line_VATAmount: 1, // 6p - (6p/120%) = (6 - 5) = 1p exactly
Line_TotalAmount: 6 // Gross * 2
}
]
}
},
{
name: '2 items that would individually round up, but round down in total',
valid: true,
data: {
RequestAmount: 8,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: null, // using gross
Item_GrossAmount: 4, // 4p
Item_Quantity: 2,
Line_VATAmount: 1, // 8p - (8p/120%) = (8 - 6.66) = 1.333 = round down
Line_TotalAmount: 8 // Gross * 2
}
]
}
},
//
// Cases that should fail
//
{
name: 'INCORRECTLY rounding up line',
valid: false,
data: {
RequestAmount: 3,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: null, // using gross
Item_GrossAmount: 2, // 2p
Item_Quantity: 1,
Line_VATAmount: 1, // should be 2p - (2p / 120%) = (2p - 1.666...) = 0.22... = round down to 0
Line_TotalAmount: 2 // Gross * count
}
]
}
},
{
name: 'INCORRECTLY rounding down line',
valid: false,
data: {
RequestAmount: 3,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: null, // using gross
Item_GrossAmount: 3, // 3p
Item_Quantity: 1,
Line_VATAmount: 0, // should be 3p - (3p/120%) = (3p - 2.5p) = 0.5p = round up to 1
Line_TotalAmount: 3 // Gross * count
}
]
}
},
{
name: '2 items that would individually round down but round up in total, being INCORRECTLY rounded down',
valid: false,
data: {
RequestAmount: 4,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: null, // using gross
Item_GrossAmount: 2, // 2p
Item_Quantity: 2,
Line_VATAmount: 0, // should be 4p - (4p/120%) = (4p - 3.33) = 0.66..p = round up to 1
Line_TotalAmount: 4 // Gross * 2
}
]
}
},
{
name: '2 items that would individually round up but are exact pennies in total, being INCORRECTLY rounded up',
valid: false,
data: {
RequestAmount: 6,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: null, // using gross
Item_GrossAmount: 3, // 3p
Item_Quantity: 2,
Line_VATAmount: 2, // should be 6p - (6p/120%) = (6 - 5) = 1p exactly
Line_TotalAmount: 6 // Gross * 2
}
]
}
},
{
name: '2 items that would individually round up but round down in total, being INCORRECTLY rounded up',
valid: false,
data: {
RequestAmount: 8,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: null, // using gross
Item_GrossAmount: 4, // 4p
Item_Quantity: 2,
Line_VATAmount: 2, // should be 8p - (8p/120%) = (8 - 6.66) = 1.333 = round down
Line_TotalAmount: 8 // Gross * 2
}
]
}
}
];
/**
* Tests of mixed types of line items in a single transaction
*/
const merchantInvoiceMixed = [
{
name: 'one round up, one down, for each of NET and GROSS + one 0% vat',
valid: true,
data: {
RequestAmount: 21,
MerchantInvoice: [
{
Item_VATRate: 2000, // 20%
Item_NetAmount: null, // using gross
Item_GrossAmount: 2, // 2p
Item_Quantity: 1,
Line_VATAmount: 0, // 2p - (2p / 120%) = (2p - 1.666...) = 0.22... = round down to 0
Line_TotalAmount: 2 // Gross * count
},
{
Item_VATRate: 2000, // 20%
Item_NetAmount: null, // using gross
Item_GrossAmount: 3, // 3p
Item_Quantity: 1,
Line_VATAmount: 1, // 3p - (3p/120%) = (3p - 2.5p) = 0.5p = round up to 1
Line_TotalAmount: 3 // Gross * count
},
{
Item_VATRate: 2000, // 20%
Item_NetAmount: 2, // 2p
Item_GrossAmount: null, // using Net
Item_Quantity: 1,
Line_VATAmount: 0, // 2p * 20% = 0.4p = round down to 0
Line_TotalAmount: 2 // Net + VAT
},
{
Item_VATRate: 2000, // 20%
Item_NetAmount: 3, // 3p
Item_GrossAmount: null, // using Net
Item_Quantity: 1,
Line_VATAmount: 1, // 3p * 20% = 0.6p = round up to 1
Line_TotalAmount: 4 // Net + VAT
},
{
Item_VATRate: 0, // 0%
Item_NetAmount: null, // using Gross
Item_GrossAmount: 10, // 10p
Item_Quantity: 1,
Line_VATAmount: 0, //
Line_TotalAmount: 10 // Gross * count
}
]
}
}
];
/**
* Tests of mixed types of line items in a single transaction
*/
const merchantInvoiceFracQuantity = [
{
name: 'Line_TotalAmount rounded to nearest (gross)',
valid: true,
data: {
RequestAmount: 1,
MerchantInvoice: [
{
Item_VATRate: 0, // 0%
Item_NetAmount: null, // using gross
Item_GrossAmount: 1, // 1p
Item_Quantity: 0.5,
Line_VATAmount: 0, // 0
Line_TotalAmount: 1 // Gross * count = 0.5 = round up to 1p
},
{
Item_VATRate: 0, // 0%
Item_NetAmount: null, // using gross
Item_GrossAmount: 1, // 1p
Item_Quantity: 0.49,
Line_VATAmount: 0, // 0
Line_TotalAmount: 0 // Gross * count = 0.49 = round down to 0p
}
]
}
},
{
name: 'Line_TotalAmount rounded to nearest (net)',
valid: true,
data: {
RequestAmount: 1,
MerchantInvoice: [
{
Item_VATRate: 0, // 0%
Item_NetAmount: 1, // 1p
Item_GrossAmount: null, // using net
Item_Quantity: 0.5,
Line_VATAmount: 0, // 0
Line_TotalAmount: 1 // Gross * count = 0.5 = round up to 1p
},
{
Item_VATRate: 0, // 0%
Item_NetAmount: 1, // 1p
Item_GrossAmount: null, // using net
Item_Quantity: 0.49,
Line_VATAmount: 0, // 0
Line_TotalAmount: 0 // Gross * count = 0.49 = round down to 0p
}
]
}
},
{
name: 'Line_VATAmount based on post-rounded numbers, not pre-rounded (net)',
valid: true,
data: {
RequestAmount: 3,
MerchantInvoice: [
{
Item_VATRate: 3000, // 30%
Item_NetAmount: 3, // 3p
Item_GrossAmount: null, // using net
Item_Quantity: 0.5,
Line_VATAmount: 1, // 2p * 0.3 = 0.6 => 1p rounded up. Not 0p if 1.5p used
Line_TotalAmount: 3 // Net * count = 1.5 = round up to 2p. Plus VAT above.
}
]
}
},
{
name: 'Line_VATAmount based on post-rounded numbers, not pre-rounded (gross)',
valid: true,
data: {
RequestAmount: 2,
MerchantInvoice: [
{
Item_VATRate: 4000, // 40%
Item_NetAmount: null, // using gross
Item_GrossAmount: 3, // 3p
Item_Quantity: 0.5,
Line_VATAmount: 1, // 2p - (2p / 1.4) = 2-1.43 = 0.57 => 0p rounded down. Not 1p if 1.5p used
Line_TotalAmount: 2 // Gross * count = 1.5 = round up to 2p.
}
]
}
}
];
/**
* Categories of test cases
*/
const groups = [
{
description: 'general transactions',
cases: merchantInvoiceBasic
},
{
description: 'transaction with merchant invoice with NET item(s)',
cases: merchantInvoiceNet
},
{
description: 'transaction with merchant invoice with GROSS item(s)',
cases: merchantInvoiceGross
},
{
description: 'transaction with merchant invoice with MIXED item(s)',
cases: merchantInvoiceMixed
},
{
description: 'transaction with fractional quantity of items',
cases: merchantInvoiceFracQuantity
}
];
/**
* Applies the test cases array, creating a test case for each one.
*
* @param {Object[]} cases - array of test cases
*/
function applyTestCases(cases) {
const validate = valid.validateRedeemPayCode;
for (let i = 0; i < cases.length; ++i) {
/**
* Get the testcase, then the test data in the correct format.
*/
const tc = cases[i];
/**
* Build a meaningful name for the test.
*/
let name = tc.valid ? 'should accept ' : 'should NOT accept ';
name += tc.name;
/**
* Run a test for this case.
*/
it(name, () => {
const expected = expect(validate(tc.data));
if (tc.valid) {
return expected.to.equal(null);
} else {
return expected.to.include({code: 174});
}
});
}
}
/**
* Unit test definitions
*/
describe('validation', () => {
describe('validateRedeemPaycode', () => {
//
// Loop through all the groups of tests cases, adding a describe for each one
//
for (let i = 0; i < groups.length; ++i) {
describe(groups[i].description, () => {
applyTestCases(groups[i].cases);
});
}
});
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,335 @@
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Comcarde Node.js General Functionality
// Provides -Bridge- pay functionality.
// Copyright 2015 Comcarde
// Written by Keith Symington and Richard Vanneck
// Refactored 17-9-2015 by KJS
// Largely replaced by JSON Schema validators Oct 2016 by RJT
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Includes
const _ = require('lodash');
const utils = require(global.pathPrefix + 'utils.js');
const config = require(global.configFile);
const debug = require('debug')('validation:invoices');
/**
* Declare the exports at the top so we know what functions are used externally
*/
module.exports = {
checkInt,
checkDP,
validateFieldTimeStamp,
validateFieldHMAC,
validateRedeemPayCode,
validateFieldMerchantInvoice
};
/**
* Checks over a string for a whitelisted set of characters.
*
* @type {Function} checkWhitelist
* @param {!string} inputString - The string to be checked for valid characters.
* @param {!string} whitelist - The list of valid charactes as a string 'abcde...'. Also accepts RegEx 'a-z'.
* @param {!int} offset - Added to the index error position: for reporting only. The default should be 0.
* @returns {null | string} Returns either null for success or an error string if there was a problem.
*/
function checkWhitelist(inputString, whitelist, offset) {
const regexString = '[^' + whitelist + ']'; // Wrap the whitelist with not operator, and repeat 0-many times.
const regex = new RegExp(regexString); // No need for global - first invalid char is fine.
const index = regex.exec(inputString);
if (index) {
return ('Error at index [' + (index.index + offset) + '], [' + index[0] + '].');
} else {
return null;
}
}
/**
* Checks whether a variable is a safe integer or not.
* Used externally by utils.js to check the config values loaded from the data.
*
* @param {any} input - the item to be tested
*
* @returns {string | null} - 'null' to confirm it is an Integer or a string if it is not.
*/
function checkInt(input) {
if (_.isSafeInteger(input)) {
return null;
} else {
return 'Not a number.';
}
}
/**
* Checks whether a variable is in ISO date format (zulu).
* input must be length checked before passing at 24 chars.
* Note that this function seems to be excessive versus a RegEx but we have had problems
* with the results so it was taken back to basics so that the code is reliable if ported.
*
* @param {string} input - the string to test
* @returns {string | null} - Returns 'null' to confirm it is valid or a string if it is not.
*/
/* eslint-disable complexity */
function checkDateTime(input) {
// Legacy code so cyclomatic comlexity is high.
// Local variables.
let output;
// Check items.
output = checkWhitelist(input.substr(0, 4), utils.numeric, 0);
if (output) {
return ('Invalid year. ' + output);
}
if (input.charAt(4) !== '-') {
return ('Invalid separator. Error at index [4].');
}
output = checkWhitelist(input.substr(5, 2), utils.numeric, 5);
if (output) {
return ('Invalid month. ' + output);
}
if (input.charAt(7) !== '-') {
return ('Invalid separator. Error at index [7].');
}
output = checkWhitelist(input.substr(8, 2), utils.numeric, 8);
if (output) {
return ('Invalid day. ' + output);
}
if (input.charAt(10) !== 'T') {
return ('Invalid time indicator. Error at index [10].');
}
output = checkWhitelist(input.substr(11, 2), utils.numeric, 11);
if (output) {
return ('Invalid hours. ' + output);
}
if (input.charAt(13) !== ':') {
return ('Invalid separator. Error at index [13].');
}
output = checkWhitelist(input.substr(14, 2), utils.numeric, 14);
if (output) {
return ('Invalid minutes. ' + output);
}
if (input.charAt(16) !== ':') {
return ('Invalid separator. Error at index [16].');
}
output = checkWhitelist(input.substr(17, 2), utils.numeric, 17);
if (output) {
return ('Invalid seconds. ' + output);
}
if (input.charAt(19) !== '.') {
return ('Invalid separator. Error at index [19].');
}
output = checkWhitelist(input.substr(20, 3), utils.numeric, 20);
if (output) {
return ('Invalid milliseconds. ' + output);
}
if (input.charAt(23) !== 'Z') {
return ('Invalid time zone. Error at index [23].');
}
// Success!
return null;
}
/* eslint-enable complexity */
/**
* Checks the number of dp after the decimal point and returns this number.
* Used by mainDB.js to validate the number of decimal places in coordinates
*
* @type {Function} checkDP
* @param {!number} numberToCheck - The number to process.
*
* @returns {number} The number of decimal places
*/
function checkDP(numberToCheck) {
const tempString = String(numberToCheck);
if (0 > tempString.indexOf('.')) {
return 0;
}
const pieces = tempString.split('.');
return pieces[1].length;
}
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Common field validations.
// All return null for success or a string otherwise.
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Validates that the provided HMAC is in the correct format.
* Used externally by auth.js to validate bridge-hmac
*
* @param {string} input - the string to test
* @returns {string | null} - error string on error, or null on success
*/
function validateFieldHMAC(input) {
if (!_.isString(input)) {
return 'Identifier not a string.';
}
if (input.length !== (config.HMACBytes * 2)) {
return ('Identifier length not ' + (config.HMACBytes * 2) + '.');
}
const output = checkWhitelist(input, utils.lowerCaseHex, 0);
if (output) {
return ('Invalid characters in HMAC. ' + output);
}
return null;
}
/**
* Validates that timestamps are correct.
* Used externally by auth.js to validate bridge-timestamp
*
* @param {any} input - the item to validate
*
* @returns {string | null} A string detailing any error, or null if no errors found.
*/
function validateFieldTimeStamp(input) {
if (!_.isString(input)) {
return 'TimeStamp not a string.';
}
if (input.length !== 24) {
return 'TimeStamp should be 24 characters long.';
}
const output = checkDateTime(input);
if (output) {
return ('TimeStamp not in ISO date format. ' + output);
}
return null;
}
/**
* Validates the functional correctness of a MerchantInvoice:
* - No rows with the same Item_ID
* - Check that the Net, VAT, Quantity, and Total match up in a row
* - Check that the rows total the requested overall TotalAmount
*
* @param {Object[]} input - array of merchantInvoice line items
* @param {number} expectedTotal - the overall total the line items should match
* @param {boolean} allowRepeatItems - true to allow the same Item_ID to appear multiple times
*
* @returns {string | null} - an error string if any errors are found, or null if no errors
*/
function validateFieldMerchantInvoice(input, expectedTotal, allowRepeatItems) {
const items = {};
let cumulativeTotal = 0;
// Iterate all line items.
for (let counter = 0; counter < input.length; counter++) {
const line = input[counter];
if (line.Item_ID && !allowRepeatItems) {
/**
* Check for duplicate Item_IDs.
*/
if (items.hasOwnProperty(line.Item_ID)) {
return 'Invalid body.MerchantInvoice[' + counter + ']: ' +
'Duplicate Item_ID in MerchantInvoice.';
} else {
items[line.Item_ID] = true;
}
}
/**
* Check the calculated per-line vat and total amounts are correct,
* depending on whether the base price is on a Net or Gross basis.
*/
let expectedLineTotal = -1;
let expectedVatTotal = -1;
if (_.isInteger(line.Item_NetAmount) && _.isNull(line.Item_GrossAmount)) {
/**
* This is a NET line item
*/
const netTotal = _.round(line.Item_NetAmount * line.Item_Quantity);
const vatMultiplier = line.Item_VATRate / (100 * 100);
expectedVatTotal = _.round(netTotal * vatMultiplier);
expectedLineTotal = netTotal + expectedVatTotal;
debug(
'NET:',
line.Item_NetAmount, line.Item_Quantity, line.Item_VATRate
);
debug(
'Interim:',
netTotal, vatMultiplier
);
} else if (_.isNull(line.Item_NetAmount) && _.isInteger(line.Item_GrossAmount)) {
/**
* This is a GROSS line item
*/
expectedLineTotal = _.round(line.Item_GrossAmount * line.Item_Quantity);
const vatMultiplier = line.Item_VATRate / (100 * 100);
expectedVatTotal = _.round(expectedLineTotal - (expectedLineTotal / (1 + vatMultiplier)));
debug(
'GROSS:',
line.Item_GrossAmount, line.Item_Quantity, line.Item_VATRate
);
} else {
/**
* Both are set (or both null) which is wrong
*/
return 'Invalid body.MerchantInvoice[' + counter + ']: ' +
'Only 1 of NetAmount and GrossAmount should be set (with the other null)';
}
debug(
'-- Actual: ',
line.Line_VATAmount, line.Line_TotalAmount
);
debug(
'-- Expect: ',
expectedVatTotal, expectedLineTotal
);
if (expectedVatTotal !== line.Line_VATAmount) {
return 'Invalid body.MerchantInvoice[' + counter + ']: ' +
'Line_VATAmount incorrect.';
}
if (expectedLineTotal !== line.Line_TotalAmount) {
return 'Invalid body.MerchantInvoice[' + counter + ']: ' +
'Line_TotalAmount incorrect.';
}
// Record total.
cumulativeTotal += line.Line_TotalAmount;
}
// Check total.
if (expectedTotal !== cumulativeTotal) {
return 'Invalid body.MerchantInvoice: Cumulative total does not match RequestAmount.';
}
// Success.
return null;
}
/**
* Provides additional function validation of the RedeemPaycode command. In
* particular, it validates that the MerchantInvoice values are consistent,
* both per-row and compared to the overall total.
*
* @param {Object} inputInfo - the input to be functionally validated
*
* @returns {Object|null} - An object to return to the user on error, or null if no errors found
*/
function validateRedeemPayCode(inputInfo) {
// Key and token check.
const errorcode = 174;
let output;
if ('MerchantInvoice' in inputInfo) {
output = validateFieldMerchantInvoice(
inputInfo.MerchantInvoice,
inputInfo.RequestAmount,
false
);
if (output) {
debug('--- Error:', output);
return utils.createError(errorcode, output);
}
}
// All valid.
return null;
}

View File

@ -0,0 +1,170 @@
/**
* @fileOverview Node.js Worldpay Acquiring Code for Bridge Pay
* @preserve Copyright 2016 Comcarde Ltd.
* @author Keith Symington
* @see #bridge_server-core
*/
/**
* Includes needed for this module.
*/
var request = require('request');
var log = require(global.pathPrefix + 'log.js');
var _ = require('lodash');
var config = require(global.configFile);
var sms = require(global.pathPrefix + 'sms.js');
exports.useMCC6012 = 0; // Add MCC information where appropriate.
exports.useAVS = 1; // Use AVS on tokenisation.
exports.worldpayPostData = 1; // Shows the info sent to Worldpay.
exports.primaryFailedComms = 0; // Ticks up every time communications fail with Worldpay's primary server.
exports.worldpaySMSAlertSent = 0; // Designed to prevent multiple SMS calls.
exports.worldpayTimeout = 25000; // Apps use 30 seconds as we need a little time to respond to them.
/**
* Worldpay API function call.
*
* @type {function} worldpayFunction
* @param {String} method - http method: 'GET', 'POST', etc.
* @param {?string} urlPath - Additional path information - e.g. tokens, order etc. Alternatively use null.
* @param {?string} authKey - Add the key string here if a key needs to be used. Alternatively use null.
* @param {?object} additionalHeaders - JSON object with any additional headers. null if none needed.
* @param {!object} postBody - JSON body for the request data in the main packet. null if none needed.
* @param {!function} callback - Function to call when processing complete..
*/
exports.worldpayFunction = function(method, urlPath, authKey, additionalHeaders, postBody, callback) {
/**
* Set the default headers.
*/
var postHeaders = {};
_.merge(postHeaders, {'User-Agent': 'Super Agent/0.0.1', 'Content-type': 'application/json'});
/**
* Add the auth key if it exists.
*/
if (authKey !== null) {
postHeaders.Authorization = authKey;
}
/**
* Add any additional headers.
*/
if (additionalHeaders !== null) {
_.merge(postHeaders, additionalHeaders);
}
// Show the data to be posted. Never post this information on the live server.
if (exports.worldpayPostData && config.isDevEnv) {
log.system(
'INFO',
('[OUT] headers: ' + JSON.stringify(postHeaders) + ' body: ' + JSON.stringify(postBody)),
'worldpay.worldpayFunction',
'',
'System',
'127.0.0.1');
}
/**
* Process the data by submitting it to Worldpay.
* The assumption is that there must be data in the body to proceed.
*/
if (postBody) {
/**
* Configure the request.
*/
var location = config.worldpayPrimaryGateway;
if (urlPath !== null) {
location += urlPath;
}
var options = {
url: location,
method: method,
headers: postHeaders,
json: true,
strictSSL: true,
body: postBody,
timeout: exports.worldpayTimeout
};
/**
* Start the request.
*/
request(options, function(error, response, body) {
/**
* Handle the response appropriately.
*/
var worldpayResult = '';
if (!error && (response.statusCode === 200)) {
/**
* A response was received. 200 means the result was successful.
*/
worldpayResult = body;
return callback(null, worldpayResult);
} else if (!error && (response.statusCode !== 200)) {
/**
* A response was received. It's not 200 so it's an error.
* The important values are:
* https://developer.worldpay.com/jsonapi/api#errors
*/
worldpayResult = body;
return callback(worldpayResult);
} else {
/**
* Return a request error.
*/
return callback('Error: ' + error);
}
});
} else {
return callback('No data to process.');
}
};
/**
* This function deals with Worldpay communication failures. Currently there is no switchover gateway.
*
* @type {function} commsFailure
* @param {!string} source - The function where the error was called from e.g. 'AddCard.process'.
*/
exports.commsFailure = function(source) {
/**
* General error - usually indicates Worldpay is down.
*/
if (exports.primaryFailedComms >= (config.worldpayNotificationThreshold - 1)) {
/**
* Inform admins as it is over the threshold.
*/
if (exports.worldpaySMSAlertSent === 0) {
/**
* Block multiple SMS messages and send a single one to the admin(s).
* Note that SMS is blocked before we know it has been sent; the callback structure means that this could
* be initialised hundreds of times on a loaded system before the first one returned.
*/
exports.worldpaySMSAlertSent = 1;
sms.sendSMS(null, (sms.adminMobile + ',' + sms.backupMobile),
config.worldpayPrimaryGatewayFailure, function(err, smsBalance) {
if (err) {
log.system(
'ERROR',
'Unable to send SMS.',
source,
'',
'System',
'127.0.0.1');
return;
}
/**
* Success.
*/
log.system(
'INFO',
('Worldpay primary gateway failure. SMS sent to admins (SMS balance now ' + smsBalance + ').'),
source,
'',
'System',
'127.0.0.1');
});
}
} else {
exports.primaryFailedComms += 1;
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Some files were not shown because too many files have changed in this diff Show More