database/models/device.js

/**
 * Created by eliwinkelman on 9/20/19.
 */

const { DatabaseInterface } = require("../db") ;

const ObjectID = require('mongodb').ObjectID;
const ClientCertFactory = require('../../lib/ClientCertFactory');
const pem = require('pem');
const getKeys = require("../../lib/manageKeys");
const Permissions = require("../permissions");

/**
 * Model for Device object, used to create new Device
 * @class
 */
class DeviceModel {
	constructor(deviceData) {
		this.name = deviceData.name;
		this.device_id = deviceData.device_id;
		this.fingerprint = deviceData.fingerprint;
		this.owner = deviceData.owner;
		this.coordinator = deviceData.coordinator;
		this.coordinator_id = deviceData.coordinator_id;
		this.network = deviceData.network;
		//adding dataRun to the constructor
		this.num_dataRuns = deviceData.num_dataRuns;
	}
}

/**
 * Manages database storage for Device objects
 * @implements DatabaseInterface
 * @class
 */
class DeviceDatabase extends DatabaseInterface {

	/**
	 * Helper function to get the collection of devices.
	 * @returns {Object} The MongoDB collection for devices
	 */
	static async getCollection() {
		const collection = await super.getCollection("Devices");
		return collection;
	}

    /**
	 * Override the default permission checking on the device object and defer to the network instead
     * @param {string} id - the database id of the object to check permissions for
     * @param {string[]} permission_names - An array of permission names to look for
     * @param {Object} user - the user to check permissions for
     * @returns {Promise.<void>}
     */
	static async checkPermissions(id, permission_names, user) {
		const device = await this.get(id);
		const network_id = device.network;

		const Networks = await super.getCollection("Networks");
		const networks = await Networks.find({_id: new ObjectID(network_id)}).toArray();
		const network = networks[0];
		return super.checkPermissions(null, permission_names, user, network);
	}

	/**
	 * Determines if user owns the device with id
	 * @param {string} id - The id of the device to check for ownership.
	 * @param {Object} user - The user to check for ownership.
	 * @returns {boolean} True if the user does own the device, false otherwise.
	 */
	static async owns(id, user) {
		return user.devices.includes(id.toString())
	}

	/**
	 * Gets all the devices belonging to this user.
	 * @param {Object} user - A user object
	 * @returns {Array} An array of devices belonging to the user.
	 */
	static async getByUser(user) {

		// check which user role we have to handle
		let usersDevices = [];
		const Devices = await this.getCollection();
		if (user.role === "user") {

			const deviceArray = user.devices.map(device => {
				return ObjectID(device)
			});

			usersDevices = await Devices.find({device_id: {$in: deviceArray}}).toArray().catch(err => {
				throw err;
			});
		}

		else if (user.role === "admin") {
			usersDevices = await Devices.find({}).toArray().catch(err => {
				throw err;
			});
		}

		return usersDevices;
	}

	static async getMany(list) {
		const deviceArray = list.map(device => {
			return ObjectID(device)
		});

		const Devices = await this.getCollection();
		
		const devices = await Devices.find({device_id: {$in: deviceArray}}).toArray().catch(err => {
			throw err;
		});
		
		return devices;
	}
	
	/**
	 * Gets the device with id.
	 * @param {string} id - The id of the device.
	 * @returns {Object} A device object from the MongoDB.
	 */
	static async get(id) {

		const Devices = await this.getCollection();

		const devices = await Devices.find({device_id: new ObjectID(id)}).toArray().catch(err => {
			throw err
		});
		
		return devices[0];
	}

	/**
	 * Updates a device.
	 * @param {string} id - The id of the device to update.
	 * @param {Object} update - The update to apply.
	 * @param {Object} user - The user requesting the update.
	 * @returns {Object} The updated device.
	 *
	 * @throws User must own the device.
	 */
	static async update(id, update) {

		const Devices = await this.getCollection();

		let device = {};

		device = await Devices.updateOne({device_id: new ObjectID(id)}, update);

		return device;
	}

	/**
	 * Deletes a device.
	 * @param {string} id - The id of the device to delete.
	 * @param {Object} user - The user attempting to delete the device.
	 * @returns {boolean} True if the device was deleted.
	 *
	 * @throws User must own the device.
	 */
	static async del(id) {

		const Devices = await this.getCollection();
		
		// delete the device
		Devices.deleteOne({device_id: new ObjectID(id)});

		//delete the device data
		const DeviceData = await super.getCollection(id.toString());
		DeviceData.deleteOne({device_id: new ObjectID(id)});

		return true;
	}

	/**
	 * Creates a new device in the database which belongs to user.
	 *
	 * 1. Generate a client certificate and device id for the new device.
	 * 2. Add the new device object to the MongoDB "Devices" collection.
	 *
	 * @param {string} name - The name of the device.
	 * @param {Object} coordinator_id - The id of the coordinator these device is under, should pass null if this is supposed to be a coordinator device.
	 * @param {Object} user - The user creating the device.
	 * @param {string} network_id - The id of the network where the device will be added.
	 * @returns {{device_id: string, certificate: string?, private_key: string?}} An object containing the authentication information for the device.
	 */

	static async create(name, coordinator_id, user, network_id) {
		let coordinator = false;

		let device_id = new ObjectID();
		if (coordinator_id === null) {
			// then this is a coordinator
			coordinator = true;
			coordinator_id = device_id;
		}

		let new_device = new DeviceModel({
			name: name,
			device_id: device_id,
			owner: user._id,
			coordinator: coordinator,
			coordinator_id: coordinator_id,
			network: network_id,
			num_dataRuns: 0
		});

		const Devices = await this.getCollection();

		let response = {
			device_id: device_id
		};
		
		if (coordinator) {

			//======= Generate Client Certificate =======//
			// get the certificate authority keys
			const keys = await getKeys();

			// generate a client certificate
			const certFactory = new ClientCertFactory(process.env.OPENSSL_BINARY_PATH, keys.ca.certificate);
			const clientCert = await certFactory.create_cert(keys.ca.key, "Spool Client", false).catch((err) => {
				throw err
			});

			//======= Add the new device to the database =======//
			response.certificate = clientCert.certificate;
			response.private_key = clientCert.key;
			new_device.fingerprint = clientCert.fingerprint;
		}

		let devices = await Devices.insertOne(new_device).catch(err => {
			throw err;
		});

		//add device to user array
		const Users = await super.getCollection("Users");

		if (Array.isArray(user.devices)) {
			user.devices.push(device_id.toString());
		}
		else {
			user.devices = [device_id.toString()]
		}

		let userUpdate = Users.updateOne({_id: new ObjectID(user._id)}, {$set: {devices: user.devices}}).catch(err => {
			throw err
		});

		return response;
	}
}

module.exports = DeviceDatabase;