731 lines
22 KiB
JavaScript
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();
|
|
}
|
|
});
|
|
}
|
|
|