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

731 lines
22 KiB
JavaScript

/* eslint-disable promise/always-return */
/* eslint-disable promise/catch-or-return */
/* eslint-disable no-negated-condition */
/**
* Controller to manage the devices functions
*/
'use strict';
const _ = require('lodash');
const Q = require('q');
const httpStatus = require('http-status-codes');
const mongodb = require('mongodb');
const debug = require('debug')('webconsole-api:controllers:devices');
const mainDB = require(global.pathPrefix + 'mainDB.js');
const utils = require(global.pathPrefix + 'utils.js');
const swaggerUtils = require(global.pathPrefix + '../utils/swaggerUtils.js');
const promiseUtils = require(global.pathPrefix + '../utils/promises.js');
const anon = require(global.pathPrefix + '../utils/anon.js');
const addDevice = require('./api_devices_controllers/api_addDevice.js');
const setPin = require('./api_devices_controllers/api_setPin.js');
module.exports = {
setPin: setPin.setPin,
addDevice: addDevice.addDevice,
getDevices,
getDevice,
updateDevice,
suspendDevice,
resumeDevice,
deleteDevice,
reportLost
};
/**
* Get the device history
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function getDevices(req, res) {
//
// Get the query params from the request and the session
//
const clientID = req.session.data.clientID;
const limit = req.swagger.params.limit.value;
const skip = req.swagger.params.skip.value;
const minDate = req.swagger.params.minDate.value;
const maxDate = req.swagger.params.maxDate.value;
const query = {
ClientID: clientID
};
//
// Add date limits if included
//
if (minDate || maxDate) {
query.LastUpdate = {};
if (minDate) {
query.LastUpdate.$gte = minDate;
}
if (maxDate) {
query.LastUpdate.$lte = maxDate;
}
}
//
// Define the projection based on the Swagger definition
//
const projection = swaggerUtils.swaggerToMongoProjection(
req.swagger.operation,
true // include _id
);
//
// Make the query. Not limit & skip have defaults defined in the
// swagger definition, so will always exist even if not requested
//
mainDB.collectionDevice.find(query, projection)
.skip(skip)
.limit(limit)
.sort({LastUpdate: -1}) // Hard-coded reverse sort by time
.toArray((err, items) => {
if (err) {
debug('- failed to getDevices', err);
res.status(httpStatus.BAD_GATEWAY).json({
code: 197,
info: 'Database offline'
});
} else {
_.forEach(
items,
(value) => {
anon.anonymiseDevice(value);
//
// Rename _id to DeviceId
//
value.DeviceID = value._id;
delete value._id;
});
//
// Null any nullable fields
//
swaggerUtils.getAndApplyNullableFields(req.swagger.operation, items);
res.status(httpStatus.OK).json(items);
}
});
}
/**
* Gets the device details for a specific device.
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function getDevice(req, res) {
//
// Get the query params from the request and the session
//
const clientID = req.session.data.clientID;
const deviceId = req.swagger.params.objectId.value;
//
// Build the query. The limits are:
// - Must match the id of the item we are looking for
// - Current user must be the owner (for security, to protect
// against Insecure Direct Object References).
//
const query = {
_id: mongodb.ObjectID(deviceId),
ClientID: clientID
};
//
// Define the projection based on the Swagger definition
//
const projection = swaggerUtils.swaggerToMongoProjection(
req.swagger.operation,
true // include _id to reflect back to the user.
);
//
// Build the options to encapsulate the projection
//
const options = {
fields: projection,
comment: 'WebConsole:getDevice' // For profiler logs use
};
//
// Make the request
//
mainDB.findOneObject(mainDB.collectionDevice, query, options, false,
(err, item) => {
if (err) {
debug('- failed to getDevice', err);
res.status(httpStatus.BAD_GATEWAY).json({
code: 197,
info: 'Database offline'
});
} else if (item === null) {
//
// Nothing found
//
res.status(httpStatus.NOT_FOUND).json({
code: 30001,
info: 'Not found'
});
} else {
anon.anonymiseDevice(item);
//
// Rename _id to DeviceId
//
item.DeviceID = item._id;
delete item._id;
//
// Null any nullable fields
//
swaggerUtils.getAndApplyNullableFields(req.swagger.operation, item);
res.status(httpStatus.OK).json(item);
}
});
}
/**
* Updates editable device details for a specific device.
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function updateDevice(req, res) {
//
// Get the query params from the request and the session
//
const clientID = req.session.data.clientID;
const deviceId = req.swagger.params.objectId.value;
//
// Get the optional parameters for the update
//
const deviceName = req.swagger.params.body.value.DeviceName;
let defaultAccount = req.swagger.params.body.value.DefaultAccount;
if (deviceName === undefined && defaultAccount === undefined) {
//
// Nothing to update
//
res.status(httpStatus.BAD_REQUEST).json({
code: 30004,
info: 'No update parameters included'
});
return;
}
//
// If we have been given a default account we need to check it actually
// belongs to the user. This requires a database lookup so we use a
// promise to wait for the result.
//
const defer = Q.defer();
const promise = defer.promise;
if (defaultAccount) {
const accQuery = {
_id: mongodb.ObjectID(defaultAccount), // The id given
ClientID: clientID, // Must be *my* account
AccountStatus: {
// jshint -W016
$bitsAllClear: utils.AccountDeleted | utils.AccountApiCreated
// jshint +W016
}
};
const fields = {}; // Don't want any fields, just checking existence
const options = {
fields,
comment: 'WebConsole:updateDevice'
};
mainDB.findOneObject(mainDB.collectionAccount, accQuery, options, false,
(err, item) => {
if (err) {
//
// Database query failed
//
const queryError = promiseUtils.returnChainedError(
err,
httpStatus.BAD_GATEWAY,
30005,
'Failed to confirm default account'
);
defer.resolve(queryError);
} else if (item === null) {
//
// Didn't find a matching account (doesn't exist, or
// doesn't belong to client).
//
const accountError = promiseUtils.returnChainedError(
err,
httpStatus.BAD_REQUEST,
30006,
'Invalid account id'
);
defer.resolve(accountError);
} else {
//
// Item was found so it exists and belongs to this client
//
defer.resolve();
}
});
} else {
//
// Haven't asked to update default account, so nothing to check
//
defer.resolve();
}
//
// Wait for the account query (if any), then update the device
//
promise
.then(() => {
//
// Account verification passed (or was not needed)
//
const query = {
_id: mongodb.ObjectID(deviceId), // The device to update
ClientID: clientID // Must be *my* device
};
const updates = {$set: {}};
if (deviceName !== undefined) {
updates.$set.DeviceName = deviceName;
}
if (defaultAccount !== undefined) {
//
// Special case: a `null` defaultAccount must be saved
// in the database as an empty string
//
if (defaultAccount === null) {
defaultAccount = '';
}
updates.$set.DefaultAccount = defaultAccount;
}
const options = {
upsert: false,
multi: false
};
mainDB.updateObject(
mainDB.collectionDevice,
query,
updates,
options,
false,
(err, result) => {
if (err) {
res.status(httpStatus.BAD_GATEWAY).json({
code: 30007,
info: 'Failed to update device'
});
} else if (result.result.n === 0) {
//
// No documents matched the criteria
//
res.status(httpStatus.NOT_FOUND).json({
code: 30008,
info: 'Invalid device'
});
} else {
res.status(httpStatus.OK).json();
}
});
})
.catch((error) => {
//
// Handle any errors from account query
//
promiseUtils.sendErrorResponse(res, error);
});
}
/**
* Suspends the specified device so that it can't be used for transactions.
* The suspended device can be returned to service with `Resume()`
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function suspendDevice(req, res) {
//
// Get the query params from the request and the session
//
const clientID = req.session.data.clientID;
const deviceId = req.swagger.params.objectId.value;
//
// Suspend the device (if it exists and belongs to me)
//
const query = {
_id: mongodb.ObjectID(deviceId), // The device to update
ClientID: clientID // Must be *my* device
};
const updates = {
$set: {
SessionToken: '' // Clear session token
},
$bit: {
DeviceStatus: {or: utils.DeviceSuspendedMask} // Set suspended bits
}
};
const options = {
upsert: false,
multi: false
};
mainDB.updateObject(
mainDB.collectionDevice,
query,
updates,
options,
false,
(err, result) => {
if (err) {
res.status(httpStatus.BAD_GATEWAY).json({
code: 30009,
info: 'Database unavailable'
});
} else if (result.result.n === 0) {
//
// No documents matched the criteria
//
res.status(httpStatus.NOT_FOUND).json({
code: 30008,
info: 'Invalid device'
});
} else {
res.status(httpStatus.OK).json();
}
});
}
/**
* Re-enables the suspended device so that it can be used for transactions again.
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function resumeDevice(req, res) {
//
// Get the query params from the request and the session
//
const clientID = req.session.data.clientID;
const deviceId = req.swagger.params.objectId.value;
//
// Resume access to the device (if it exists and belongs to me)
//
const query = {
_id: mongodb.ObjectID(deviceId), // The device to update
ClientID: clientID // Must be *my* device
};
const updates = {
$bit: {
// jshint -W016
DeviceStatus: {and: ~utils.DeviceSuspendedMask} // clear suspended bits
// jshint +W016
}
};
const options = {
upsert: false,
multi: false
};
mainDB.updateObject(
mainDB.collectionDevice,
query,
updates,
options,
false,
(err, result) => {
if (err) {
res.status(httpStatus.BAD_GATEWAY).json({
code: 30009,
info: 'Database unavailable'
});
} else if (result.result.n === 0) {
//
// No documents matched the criteria
//
res.status(httpStatus.NOT_FOUND).json({
code: 30008,
info: 'Invalid device'
});
} else {
res.status(httpStatus.OK).json();
}
});
}
/**
* Deletes a device such that it can no longer be used in the system.
* What it actually does is copies the document to the DeviceArchive collection
* then deletes it from the Device collection.
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function deleteDevice(req, res) {
//
// Get the query params from the request and the session
//
const clientID = req.session.data.clientID;
const deviceId = req.swagger.params.objectId.value;
//
// Find the device we want to delete
//
const query = {
_id: mongodb.ObjectID(deviceId), // The device to update
ClientID: clientID // Must be *my* device
};
const options = {
comment: 'WebConsole: find for deleteDevice'
};
//
// Step 1: Find the device. This includes checking ownership by the
// the user with the current session.
//
const findOriginalPromise = Q.nfcall(
mainDB.findOneObject,
mainDB.collectionDevice,
query,
options,
false
);
//
// Step 2: Add a copy of the device document to the archive
//
const NOT_FOUND = 'BRIDGE: NOT FOUND';
const DEVICE_BARRED = 'BRIDGE: DEVICE BARRED';
let oldId = null;
const addArchivePromise = findOriginalPromise.then((item) => {
//
// DB query ran ok, but need to check if there were any results
//
if (!item) {
return Q.reject({name: NOT_FOUND});
} else if (
item &&
(!item.hasOwnProperty('DeviceStatus') ||
utils.bitsAllSet(item.DeviceStatus, utils.DeviceBarredMask))
) {
// Barred devices can't be deleted until unbarred
return Q.reject({name: DEVICE_BARRED});
} else {
const insertObject = _.clone(item);
oldId = item._id;
insertObject.DeviceIndex = item._id.toString(); // Backup the old index
insertObject.DeviceAuthorisation = '';
insertObject.DeviceSalt = '';
insertObject.CurrentHMAC = '';
insertObject.PendingHMAC = '';
delete insertObject._id; // Then delete it to get a new one
//
// Set LastUpdate to the current date
//
insertObject.LastUpdate = new Date();
//
// And insert into the database
//
return Q.nfcall(
mainDB.addObject,
mainDB.collectionDeviceArchive,
insertObject,
undefined,
false
);
}
});
//
// Step 3: Delete the original
//
const NOT_ARCHIVED = 'BRIDGE: NOT ARCHIVED';
const deleteOriginalPromise = addArchivePromise.then((result) => {
if (!_.isObject(result)) {
return Q.reject({name: NOT_ARCHIVED});
} else {
debug('Deleting orginal: ', oldId);
const deleteQuery = {
_id: oldId
};
return Q.nfcall(
mainDB.removeObject,
mainDB.collectionDevice,
deleteQuery,
undefined,
false
);
}
});
//
// Run them all in sequence and check the result
//
Q.all([findOriginalPromise, addArchivePromise, deleteOriginalPromise])
.then(() => {
//
// Succeeded
//
res.status(200).json();
})
.catch((error) => {
debug('-- error deleting device: ', error);
if (
error &&
error.hasOwnProperty('name')
) {
switch (error.name) {
case NOT_FOUND:
//
// Device not found in the DB (or doesn't belong to
// this user)
//
res.status(httpStatus.NOT_FOUND).json({
code: 370,
info: 'Device not found'
});
break;
case DEVICE_BARRED:
//
// Device is barred
//
res.status(httpStatus.FORBIDDEN).json({
code: 371,
info: 'Device barred'
});
break;
case NOT_ARCHIVED:
//
// Item failed to archive
//
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
code: 222,
info: 'Failed to archive device'
});
break;
case 'MongoError':
//
// Mongo Error
//
res.status(httpStatus.BAD_GATEWAY).json({
code: 365,
info: 'Database Unavailable'
});
break;
default:
//
// Unknown error
//
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
code: -1,
info: 'Unexpected error'
});
break;
}
} else {
//
// Unknown error
//
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
code: -1,
info: 'Unexpected error'
});
}
})
.done(); // Catch all
}
/**
* Reports a device as lost, which suspends the device. This function is very
* similar to suspendDevice, except it is designed to be used by a user who
* does not have full permision to access the system - i.e. users that are
* waiting for a 2-factor confirmation that they can't give because they have
* lost their device. As they are not fully authorised we don't give them a
* list of devices to pick from. Instead we require them to pass in the full
* phone number of the device that should be suspended.
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function reportLost(req, res) {
//
// Get the query params from the request and the session
// Note that we don't exclude devices that have already been suspended
// because it doesn't make any difference and could lead to confusing
// error messages.
//
const clientID = req.session.data.clientID;
const DeviceNumber = req.swagger.params.body.value.DeviceNumber;
//
// Suspend the device (if it exists and belongs to me)
//
const query = {
ClientID: clientID, // Must be *my* device
DeviceNumber // Must have a matching number
};
const updates = {
$set: {
SessionToken: '' // Clear session token
},
$bit: {
DeviceStatus: {or: utils.DeviceSuspendedMask} // Set suspended bits
}
};
const options = {
upsert: false,
multi: false
};
mainDB.updateObject(
mainDB.collectionDevice,
query,
updates,
options,
false,
(err, result) => {
if (err) {
res.status(httpStatus.BAD_GATEWAY).json({
code: 30401,
info: 'Database unavailable'
});
} else if (result.result.n === 0) {
//
// No documents matched the criteria
//
res.status(httpStatus.NOT_FOUND).json({
code: 30402,
info: 'Invalid device'
});
} else {
res.status(httpStatus.OK).json();
}
});
}