From 37560124e243cc46ab323421906adbf7ce6f8471 Mon Sep 17 00:00:00 2001 From: Michael Grieco <30534878+michaelg29@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:15:02 -0400 Subject: [PATCH] Clean up files and organize methods --- ota-portal/app.js | 22 +++-- ota-portal/public/javascripts/nodes/tools.js | 10 +- ota-portal/routes/auth.js | 36 +++++-- ota-portal/routes/list.js | 0 ota-portal/routes/networkChannels.js | 51 ++++++++++ ota-portal/routes/networkRegistrations.js | 53 +++++++++++ ota-portal/routes/networks.js | 42 +-------- ota-portal/routes/nodeAuth.js | 67 ------------- ota-portal/routes/nodeList.js | 88 +++++++++++++++++ ota-portal/routes/nodes.js | 99 ++++++++------------ ota-portal/utils/channel.js | 27 ++++++ ota-portal/utils/network.js | 73 +-------------- ota-portal/utils/network_passphrase.js | 72 +++++++++++++- ota-portal/views/node/list.pug | 4 +- 14 files changed, 383 insertions(+), 261 deletions(-) delete mode 100644 ota-portal/routes/list.js create mode 100644 ota-portal/routes/networkChannels.js create mode 100644 ota-portal/routes/networkRegistrations.js delete mode 100644 ota-portal/routes/nodeAuth.js create mode 100644 ota-portal/routes/nodeList.js create mode 100644 ota-portal/utils/channel.js diff --git a/ota-portal/app.js b/ota-portal/app.js index 7900636..f35dc7f 100644 --- a/ota-portal/app.js +++ b/ota-portal/app.js @@ -4,14 +4,18 @@ const logger = require("morgan"); const path = require("path"); const errors = require("./utils/httperror"); +// middleware and router includes const auth = require("./routes/auth"); -const nodesRouter = require("./routes/nodes"); -const nodeAuthRouter = require("./routes/nodeAuth"); +const nodeListRouter = require("./routes/nodeList"); +const nodeRouter = require("./routes/nodes"); const networksRouter = require("./routes/networks"); +const networkChannelsRouter = require("./routes/networkChannels"); +const networkRegistrationsRouter = require("./routes/networkRegistrations"); +// create application var app = express(); -// middleware +// basic middleware app.use(logger('dev')); app.use(express.text({ type: "text/*" @@ -27,7 +31,7 @@ app.use(express.static(path.join(__dirname, 'public'))); app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'pug'); -// authorization +// authorization middleware app.use(auth); // index redirect @@ -37,10 +41,14 @@ app.get("/", (req, res) => { }); }); -app.use("/nodes", nodesRouter); -app.use("/nodes", nodeAuthRouter); +// register middleware +app.use("/nodes", nodeRouter); +app.use("/nodes", nodeListRouter); app.use("/networks", networksRouter); +app.use("/networks", networkChannelsRouter); +app.use("/networks", networkRegistrationsRouter); +// not found handler app.use(function (req, res, next) { next(errors.errorObj(404, "Not found.")); }); @@ -55,9 +63,11 @@ app.use(function (err, req, res, next) { res.locals.message = err.message; res.locals.error = isDevelopment ? err : {}; + // set status code err.statusCode = err.statusCode || 500; res.status(err.statusCode); + // wrap content in specific content type const returnType = req.headers.accept; if (returnType === "application/json") { res.send({ diff --git a/ota-portal/public/javascripts/nodes/tools.js b/ota-portal/public/javascripts/nodes/tools.js index c196bb3..de8ec90 100644 --- a/ota-portal/public/javascripts/nodes/tools.js +++ b/ota-portal/public/javascripts/nodes/tools.js @@ -10,19 +10,19 @@ async function deleteNode(nodeId) { await dataRequest("DELETE", `nodes/${nodeId}`); } -async function uploadFile() { +async function uploadFile(networkId) { // get node IDs to upload to let nodeSelectElements = document.querySelectorAll("input.node-selector"); - let node_ids = []; + let nodeIds = []; for (let el of nodeSelectElements) { if (!!el.checked) { - node_ids.push(el.id.substring("select-".length)); + nodeIds.push(el.id.substring("select-".length)); } } // get channel ID to upload file - let channelRes = await dataRequest("POST", "file_channel", undefined, { - "node_ids": node_ids + let channelRes = await dataRequest("POST", `networks/${networkId}/channel`, undefined, { + "nodeIds": nodeIds }); return; diff --git a/ota-portal/routes/auth.js b/ota-portal/routes/auth.js index fbc782a..c3692a6 100644 --- a/ota-portal/routes/auth.js +++ b/ota-portal/routes/auth.js @@ -1,8 +1,14 @@ +/** + * Authorization middleware. + */ + const express = require('express'); const router = express.Router(); const errors = require('../utils/httperror'); const rclient = require('../utils/redis-client'); +const base64url = require('base64url').default; +// encryption and decryption parameters and keys const crypto = require('crypto'); const algorithm = 'aes-256-cbc'; const key = !!process.env.PORTAL_TOKEN_ENC_KEY @@ -12,11 +18,10 @@ const iv = !!process.env.PORTAL_TOKEN_ENC_IV ? Buffer.from(process.env.PORTAL_TOKEN_ENC_IV) : crypto.randomBytes(8); +// cookie names const tokenCookieName = "jamota-token"; const tokenExpiryCookieName = "jamota-token-expiry"; -const base64url = require('base64url').default; - function encryptToken(token) { // get buffer of string var b = Buffer.from(JSON.stringify(token)); @@ -78,7 +83,9 @@ async function logout(req, res) { } } -// access token decoder +/** + * Access token decoder. To be executed before route-specific function. + */ router.use(errors.asyncWrap(async function(req, res, next) { if (req.url === "/login" || req.url === "/createAccount") { next(); @@ -117,6 +124,9 @@ router.use(errors.asyncWrap(async function(req, res, next) { next(); })); +/** + * Request a refreshed access token. + */ router.post("/refreshToken", async function(req, res, next) { if (req.user) { const userKey = "user:" + req.user.username; @@ -132,12 +142,16 @@ router.post("/refreshToken", async function(req, res, next) { } }); -// registration form +/** + * Form to create an account with the OTA system. + */ router.get("/createAccount", function(req, res, next) { res.render("createAccount"); }); -// create account request +/** + * Register an account with the OTA system. + */ router.post("/createAccount", errors.asyncWrap(async function(req, res, next) { if (!req.body || !req.body.email || !req.body.username || !req.body.password || !req.body.name) { errors.error(400, "Invalid input."); @@ -175,7 +189,9 @@ router.post("/createAccount", errors.asyncWrap(async function(req, res, next) { res.redirect("/login"); })); -// login form +/** + * Get the login form. + */ router.get("/login", function(req, res, next) { if (req.user) { res.redirect("/"); @@ -185,7 +201,9 @@ router.get("/login", function(req, res, next) { } }); -// login request +/** + * Validate a login request. + */ router.post("/login", errors.asyncWrap(async function(req, res, next) { if (req.user) { res.sendStatus(200); @@ -226,7 +244,9 @@ router.post("/login", errors.asyncWrap(async function(req, res, next) { res.redirect("/"); })); -// logout +/** + * Logout the user. + */ router.all("/logout", async function(req, res, next) { logout(req, res); res.redirect("/login"); diff --git a/ota-portal/routes/list.js b/ota-portal/routes/list.js deleted file mode 100644 index e69de29..0000000 diff --git a/ota-portal/routes/networkChannels.js b/ota-portal/routes/networkChannels.js new file mode 100644 index 0000000..fad9ec1 --- /dev/null +++ b/ota-portal/routes/networkChannels.js @@ -0,0 +1,51 @@ +/** + * Authorized requests for network channels in the database. + */ + +const express = require("express"); +const router = express.Router(); + +const errors = require("../utils/httperror"); +const request = require("../utils/request"); +const rclient = require("../utils/redis-client"); + +const network = require("../utils/network"); +const channel = require("../utils/channel"); +const node = require("../utils/node"); + +/** + * Create a channel to communicate with multiple nodes. Each network can only have one active channel. + */ +router.post("/:id/channel", errors.asyncWrap(async function(req, res) { + // parse request + const networkId = req.params.id; + const channelReq = request.validateBody(req, ["nodeIds?"]); + + // validate request + await network.getNetworkFromOwner(req, networkId); + let [err, networkNodeIds] = await rclient.getSetMembers(node.networkNodesKey(networkId)); + + let nodeIds = []; + if ("nodeIds" in channelReq && channelReq.nodeIds.length > 0) { + // filter nodes to the current network + for (let nodeId of channelReq.nodeIds) { + if (networkNodeIds.indexOf(nodeId) != -1) { + nodeIds.push(nodeId); + } + } + } + else { + // if none provided, select all nodes in the network + nodeIds = networkNodeIds; + } + + console.log(nodeIds); + + // create channel + await channel.newChannelObj(networkId, nodeIds); + + // return response + res.status(200); +})); + +module.exports = router; diff --git a/ota-portal/routes/networkRegistrations.js b/ota-portal/routes/networkRegistrations.js new file mode 100644 index 0000000..e54f0d8 --- /dev/null +++ b/ota-portal/routes/networkRegistrations.js @@ -0,0 +1,53 @@ +/** + * Authorized requests for network registrations in the database. + */ + +const express = require("express"); +const router = express.Router(); + +const errors = require("../utils/httperror"); +const request = require("../utils/request"); + +const network = require("../utils/network"); +const passphrases = require("../utils/network_passphrase"); + +/** + * Get the node registration form. + */ +router.get("/:id/node", errors.asyncWrap(async function(req, res, next) { + [err, redisRes] = await network.getNetworkFromOwner(req, req.params.id); + + res.render("network/register", { + netId: req.params.id + }); +})); + +/** + * Prepare a node passphrase. + */ +router.post("/:id/node", errors.asyncWrap(async function(req, res, next) { + const nodeReq = request.validateBody(req, ["name", "pass"]); + + passphrases.validateNetworkPassphrase(nodeReq.pass); + + // check requested network + const networkId = req.params.id; + await network.getNetworkFromOwner(req, networkId); + + // set in database + await passphrases.addNetworkPassphrase(networkId, nodeReq.name, nodeReq.pass); + + res.redirect("/nodes?network-id=" + req.params.id); +})); + +/** + * Clear all unused passphrases. + */ +router.delete("/:id/passphrases", errors.asyncWrap(async function(req, res, next) { + const networkId = req.params.id; + await network.getNetworkFromOwner(req, networkId); + await passphrases.clearPassphrases(networkId); + res.sendStatus(204); +})); + +module.exports = router; diff --git a/ota-portal/routes/networks.js b/ota-portal/routes/networks.js index efb8afe..fc67d72 100644 --- a/ota-portal/routes/networks.js +++ b/ota-portal/routes/networks.js @@ -11,7 +11,6 @@ const request = require("../utils/request"); const ijam_types = require("../utils/ijam_types"); const network = require("../utils/network"); -const passphrases = require("../utils/network_passphrase"); const node = require("../utils/node"); /** @@ -108,7 +107,7 @@ router.get("/", errors.asyncWrap(async function(req, res, next) { var data = await filterNetworkEntries(networkIds); if (req.headers.accept === "application/json") { - res.send(data) + res.send(data); } else { res.render("network/list", { @@ -117,43 +116,4 @@ router.get("/", errors.asyncWrap(async function(req, res, next) { } })); -/** - * Get the node registration form. - */ -router.get("/:id/node", errors.asyncWrap(async function(req, res, next) { - [err, redisRes] = await network.getNetworkFromOwner(req, req.params.id); - - res.render("network/register", { - netId: req.params.id - }); -})); - -/** - * Prepare a node passphrase. - */ -router.post("/:id/node", errors.asyncWrap(async function(req, res, next) { - const nodeReq = request.validateBody(req, ["name", "pass"]); - - passphrases.validateNetworkPassphrase(nodeReq.pass); - - // check requested network - const networkId = req.params.id; - await network.getNetworkFromOwner(req, networkId); - - // set in database - await network.addNetworkPassphrase(networkId, nodeReq.name, nodeReq.pass); - - res.redirect("/nodes?network-id=" + req.params.id); -})); - -/** - * Clear all unused passphrases. - */ -router.delete("/:id/passphrases", errors.asyncWrap(async function(req, res, next) { - const networkId = req.params.id; - await network.getNetworkFromOwner(req, networkId); - await network.clearPassphrases(networkId); - res.sendStatus(204); -})); - module.exports = router; diff --git a/ota-portal/routes/nodeAuth.js b/ota-portal/routes/nodeAuth.js deleted file mode 100644 index 3455a1b..0000000 --- a/ota-portal/routes/nodeAuth.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Authorized requests to transition nodes in the database. These - * requests come from the website, where a user must have an - * active logged-in session. - */ - -const express = require("express"); -const router = express.Router(); - -const rclient = require("./../utils/redis-client"); -const errors = require("./../utils/httperror"); -const request = require("./../utils/request"); -const ssh = require("./../utils/ssh"); - -const node = require("./../utils/node"); -const network = require("./../utils/network"); - -/** - * Revoke a node. - */ -router.purge("/:id", errors.asyncWrap(async function(req, res, next) { - // get requested node - const nodeId = req.params.id; - await node.getNodeFromOwner(req, nodeId); - - // update node entry in DB - await node.obj.revoke(nodeId); - - res.status(200).send(); -})); - -/** - * Hard delete a node from the database. - */ -router.delete("/:id", errors.asyncWrap(async function(req, res, next) { - // get requested node - const nodeId = req.params.id; - [redisRes, nodeKey] = await node.getNodeFromOwner(req, nodeId); - - // can only delete if revoked or expired - node.validateNodeTransition(redisRes, "deleted", [node.statuses.REVOKED, node.statuses.EXPIRED]); - await rclient.removeFromSet(node.networkNodesKey(redisRes.networkId), nodeId); - await rclient.del(nodeKey); - - res.sendStatus(204); -})); - -/** - * Ping a node. - */ -router.notify("/:id", errors.asyncWrap(async function(req, res, next) { - // get requested node - const nodeId = req.params.id; - [redisRes, nodeKey] = await node.getNodeFromOwner(req, nodeId); - - // can ping only if online - if (redisRes.status !== node.statuses.ONLINE) { - errors.error(403, "Node not online."); - } - - // ping the node - await ssh.pingSSH(redisRes.sshUser, redisRes.ip); - - res.status(200).send(); -})); - -module.exports = router; diff --git a/ota-portal/routes/nodeList.js b/ota-portal/routes/nodeList.js new file mode 100644 index 0000000..73e4a69 --- /dev/null +++ b/ota-portal/routes/nodeList.js @@ -0,0 +1,88 @@ +/** + * Authorized requests to manipulate and fetch multiple nodes. + */ + +const express = require("express"); +const router = express.Router(); + +const rclient = require("./../utils/redis-client"); +const errors = require("./../utils/httperror"); + +const node = require("./../utils/node"); +const network = require("./../utils/network"); + +function applyFilter(nodeObj) { + return true; +} + +function map(network, nodeObj) { + const createdOn = Number.parseInt(nodeObj.createdOn); + const lastRegisteredOn = Number.parseInt(nodeObj.lastRegisteredOn); + + return { + name: nodeObj.name, + type: nodeObj.type, + status: nodeObj.status, + networkName: network.name, + createdOn: new Date(createdOn).toDateString(), + lastRegisteredOn: (lastRegisteredOn === 0 + ? 'Not registered yet' + : new Date(lastRegisteredOn).toDateString()), + arch: nodeObj.arch + }; +} + +async function filterNodeEntries(req, networkIds) { + let nodes = {}; + const requestedNetworkId = req.query["network-id"] || false; + + for (let networkId of networkIds) { + if (requestedNetworkId) { + // query for requested network + if (networkId != requestedNetworkId) continue; + + try { + await network.belongsToOwner(req, networkId); + } catch { + continue; + } + } + + let [networkObj, _] = await network.getNetwork(networkId); + let [err, nodeIds] = await rclient.getSetMembers(node.networkNodesKey(networkId)); + + for (let nodeId of nodeIds) { + [err, nodeObj] = await rclient.getObj(node.nodeKey(nodeId)); + if (nodeObj && applyFilter(nodeObj)) { + nodes[nodeId] = map(networkObj, nodeObj); + } + } + } + + return nodes; +} + +/** + * Get all nodes. + */ +router.get("/", errors.asyncWrap(async function(req, res) { + let [err, networkIds] = await rclient.getSetMembers(network.userNetworksKeyFromReq(req)); + + var data = await filterNodeEntries(req, networkIds); + var networkId = undefined; + if ("network-id" in req.query) { + networkId = req.query["network-id"]; + } + + if (req.headers.accept === "application/json") { + res.send(data) + } + else { + res.render("node/list", { + data: data, + networkId: networkId + }); + } +})); + +module.exports = router; diff --git a/ota-portal/routes/nodes.js b/ota-portal/routes/nodes.js index 4351f45..1fc4dec 100644 --- a/ota-portal/routes/nodes.js +++ b/ota-portal/routes/nodes.js @@ -1,5 +1,7 @@ /** - * Authorized requests to manipulate and fetch multiple nodes. + * Authorized requests to transition nodes in the database. These + * requests come from the website, where a user must have an + * active logged-in session. */ const express = require("express"); @@ -7,77 +9,56 @@ const router = express.Router(); const rclient = require("./../utils/redis-client"); const errors = require("./../utils/httperror"); +const request = require("./../utils/request"); const node = require("./../utils/node"); -const network = require("./../utils/network"); -function applyFilter(nodeObj) { - return true; -} - -function map(network, nodeObj) { - const createdOn = Number.parseInt(nodeObj.createdOn); - const lastRegisteredOn = Number.parseInt(nodeObj.lastRegisteredOn); - - return { - name: nodeObj.name, - type: nodeObj.type, - status: nodeObj.status, - networkName: network.name, - createdOn: new Date(createdOn).toDateString(), - lastRegisteredOn: (lastRegisteredOn === 0 - ? 'Not registered yet' - : new Date(lastRegisteredOn).toDateString()), - arch: nodeObj.arch - }; -} - -async function filterNodeEntries(req, networkIds) { - let nodes = {}; - const requestedNetworkId = req.query["network-id"] || false; +/** + * Revoke a node. + */ +router.purge("/:id", errors.asyncWrap(async function(req, res, next) { + // get requested node + const nodeId = req.params.id; + await node.getNodeFromOwner(req, nodeId); - for (let networkId of networkIds) { - if (requestedNetworkId) { - // query for requested network - if (networkId != requestedNetworkId) continue; + // update node entry in DB + await node.obj.revoke(nodeId); - try { - await network.belongsToOwner(req, networkId); - } catch { - continue; - } - } + res.status(200).send(); +})); - let [networkObj, _] = await network.getNetwork(networkId); - let [err, nodeIds] = await rclient.getSetMembers(node.networkNodesKey(networkId)); +/** + * Hard delete a node from the database. + */ +router.delete("/:id", errors.asyncWrap(async function(req, res, next) { + // get requested node + const nodeId = req.params.id; + [redisRes, nodeKey] = await node.getNodeFromOwner(req, nodeId); - for (let nodeId of nodeIds) { - [err, nodeObj] = await rclient.getObj(node.nodeKey(nodeId)); - if (nodeObj && applyFilter(nodeObj)) { - nodes[nodeId] = map(networkObj, nodeObj); - } - } - } + // can only delete if revoked or expired + node.validateNodeTransition(redisRes, "deleted", [node.statuses.REVOKED, node.statuses.EXPIRED]); + await rclient.removeFromSet(node.networkNodesKey(redisRes.networkId), nodeId); + await rclient.del(nodeKey); - return nodes; -} + res.sendStatus(204); +})); /** - * Get all nodes. + * Ping a node. */ -router.get("/", errors.asyncWrap(async function(req, res) { - [err, networkIds] = await rclient.getSetMembers(network.userNetworksKeyFromReq(req)); +router.notify("/:id", errors.asyncWrap(async function(req, res, next) { + // get requested node + const nodeId = req.params.id; + [redisRes, nodeKey] = await node.getNodeFromOwner(req, nodeId); + + // can ping only if online + if (redisRes.status !== node.statuses.ONLINE) { + errors.error(403, "Node not online."); + } - var data = await filterNodeEntries(req, networkIds); + // ping the node - if (req.headers.accept === "application/json") { - res.send(data) - } - else { - res.render("node/list", { - data: data - }); - } + res.status(200).send(); })); module.exports = router; diff --git a/ota-portal/utils/channel.js b/ota-portal/utils/channel.js new file mode 100644 index 0000000..c712b41 --- /dev/null +++ b/ota-portal/utils/channel.js @@ -0,0 +1,27 @@ + +const errors = require("./httperror"); +const rclient = require("./redis-client"); + +const networkChannelKey = (networkId) => "network:" + networkId + ":channel"; + +/** + * Create a channel for a network. + * @param {string} networkId The network ID. + * @param {Array} nodeIds The list of node IDs. + */ +const newChannelObj = async function(networkId, nodeIds) { + const setKey = networkChannelKey(networkId); + console.log(networkId, nodeIds); + + // delete existing channel + await rclient.del(setKey); + + // add new node keys + for (let nodeId of nodeIds) { + await rclient.addToSet(setKey, nodeId); + } +}; + +module.exports = { + newChannelObj: newChannelObj +}; diff --git a/ota-portal/utils/network.js b/ota-portal/utils/network.js index 4305c1b..6441d6b 100644 --- a/ota-portal/utils/network.js +++ b/ota-portal/utils/network.js @@ -1,6 +1,5 @@ const errors = require("./httperror"); const rclient = require("./redis-client"); -const passphrases = require("./network_passphrase"); /** Get the network key. */ const networkKey = (networkId) => "network:" + networkId; @@ -8,10 +7,6 @@ const networkKey = (networkId) => "network:" + networkId; /** Get the key mapping to the user's network list. */ const userNetworksKeyFromReq = (req) => "user:" + req.user.username + ":networks"; -/** Get the key mapping to the network's active passphrases. */ -const networkPassphrasesKey = (networkId) => "network:" + networkId + ":passphrases"; -const networkPassphraseNamesKey = (networkId) => "network:" + networkId + ":passphraseNames"; - /** * Create a new network object. * @param {string} id Guid of the network. @@ -86,68 +81,6 @@ const getNetworkFromOwner = async function(req, netId) { return await getNetwork(netId); } -/** - * Clear all passphrases. - * @param {string} netId The network ID. - */ -const clearPassphrases = async function(netId) { - await rclient.del(networkPassphraseNamesKey(netId)); - await rclient.del(networkPassphrasesKey(netId)); -} - -/** - * Add a single-use passphrase for a node to use to register with the network. - * @param {string} netId The network ID to add the key to. - * @param {string} nodeName The name of the node that will be registered with this key. - * @param {string} passphrase Passphrase for the node to use in registration. - */ -const addNetworkPassphrase = async function(netId, nodeName, passphrase) { - passphrases.validateNetworkPassphrase(passphrase); - - // add passphrase to set - await rclient.addToSet(networkPassphrasesKey(netId), passphrase); - - // set associated name - let obj = {}; - obj[passphrase] = nodeName; - await rclient.setObjOrThrow(networkPassphraseNamesKey(netId), obj); -} - -/** - * Check that a passphrase matches the network then delete it. - * @param {string} netId The network ID. - * @param {string} passphrase The passphrase to check. - * @returns [Network object, the name of the node matching the passphrase]. - */ -const matchNetworkPassphrase = async function(netId, passphrase) { - passphrases.validateNetworkPassphrase(passphrase); - - if (await rclient.isInSet(networkPassphrasesKey(netId), passphrase)) { - let [err, redisRes] = await rclient.getObjField(networkPassphraseNamesKey(netId), passphrase); - if (!err && !!redisRes) { - // delete from database - await rclient.removeFromSet(networkPassphrasesKey(netId), passphrase); - await rclient.delObjField(networkPassphraseNamesKey(netId), passphrase); - let nodeName = redisRes; - [redisRes, _] = await getNetwork(netId); - - // return node name - return [redisRes, nodeName]; - } - } - - errors.error(401, "Invalid passphrase."); -} - -/** - * Get the number of passphrases for a network. - * @param {string} netId Network ID. - * @returns The number of valid passphrases for the network. - */ -const getNumberOfPassphrases = async function(netId) { - return await rclient.getSetSize(networkPassphrasesKey(netId)); -} - module.exports = { networkKey: networkKey, obj: { @@ -157,9 +90,5 @@ module.exports = { networkExists: networkExists, belongsToOwner: belongsToOwner, getNetworkFromOwner: getNetworkFromOwner, - userNetworksKeyFromReq: userNetworksKeyFromReq, - clearPassphrases: clearPassphrases, - addNetworkPassphrase: addNetworkPassphrase, - matchNetworkPassphrase: matchNetworkPassphrase, - getNumberOfPassphrases: getNumberOfPassphrases + userNetworksKeyFromReq: userNetworksKeyFromReq }; diff --git a/ota-portal/utils/network_passphrase.js b/ota-portal/utils/network_passphrase.js index e39a1d7..f1079cd 100644 --- a/ota-portal/utils/network_passphrase.js +++ b/ota-portal/utils/network_passphrase.js @@ -1,6 +1,10 @@ const maxPassphraseLength = 16; +/** Get the key mapping to the network's active passphrases. */ +const networkPassphrasesKey = (networkId) => "network:" + networkId + ":passphrases"; +const networkPassphraseNamesKey = (networkId) => "network:" + networkId + ":passphraseNames"; + /** * Check the passphrase is only alphanumeric characters. * @param {string} passphrase The passphrase to validate. @@ -19,8 +23,74 @@ const validateNetworkPassphraseBuf = function(buf) { validateNetworkPassphrase(buf.toString("utf-8")); } +/** + * Clear all passphrases. + * @param {string} netId The network ID. + */ +const clearPassphrases = async function(netId) { + await rclient.del(networkPassphraseNamesKey(netId)); + await rclient.del(networkPassphrasesKey(netId)); +} + +/** + * Add a single-use passphrase for a node to use to register with the network. + * @param {string} netId The network ID to add the key to. + * @param {string} nodeName The name of the node that will be registered with this key. + * @param {string} passphrase Passphrase for the node to use in registration. + */ +const addNetworkPassphrase = async function(netId, nodeName, passphrase) { + validateNetworkPassphrase(passphrase); + + // add passphrase to set + await rclient.addToSet(networkPassphrasesKey(netId), passphrase); + + // set associated name + let obj = {}; + obj[passphrase] = nodeName; + await rclient.setObjOrThrow(networkPassphraseNamesKey(netId), obj); +} + +/** + * Check that a passphrase matches the network then delete it. + * @param {string} netId The network ID. + * @param {string} passphrase The passphrase to check. + * @returns [Network object, the name of the node matching the passphrase]. + */ +const matchNetworkPassphrase = async function(netId, passphrase) { + validateNetworkPassphrase(passphrase); + + if (await rclient.isInSet(networkPassphrasesKey(netId), passphrase)) { + let [err, redisRes] = await rclient.getObjField(networkPassphraseNamesKey(netId), passphrase); + if (!err && !!redisRes) { + // delete from database + await rclient.removeFromSet(networkPassphrasesKey(netId), passphrase); + await rclient.delObjField(networkPassphraseNamesKey(netId), passphrase); + let nodeName = redisRes; + [redisRes, _] = await getNetwork(netId); + + // return node name + return [redisRes, nodeName]; + } + } + + errors.error(401, "Invalid passphrase."); +} + +/** + * Get the number of passphrases for a network. + * @param {string} netId Network ID. + * @returns The number of valid passphrases for the network. + */ +const getNumberOfPassphrases = async function(netId) { + return await rclient.getSetSize(networkPassphrasesKey(netId)); +} + module.exports = { maxPassphraseLength: maxPassphraseLength, validateNetworkPassphrase: validateNetworkPassphrase, - validateNetworkPassphraseBuf: validateNetworkPassphraseBuf + validateNetworkPassphraseBuf: validateNetworkPassphraseBuf, + clearPassphrases: clearPassphrases, + addNetworkPassphrase: addNetworkPassphrase, + matchNetworkPassphrase: matchNetworkPassphrase, + getNumberOfPassphrases: getNumberOfPassphrases } diff --git a/ota-portal/views/node/list.pug b/ota-portal/views/node/list.pug index c9e37fb..ebbe418 100644 --- a/ota-portal/views/node/list.pug +++ b/ota-portal/views/node/list.pug @@ -8,7 +8,7 @@ block content div label Select file to upload to selected nodes input(type="file" id="file-input") - input(type="submit" value="Upload", onclick="uploadFile()") + input(type="submit" value="Upload", onclick="uploadFile('" + networkId + "')") h2 Table of nodes @@ -30,7 +30,7 @@ block content each value, key in data tr(class='type-' + value.type + ' status-' + value.status) td - if value.status == "online" + if value.status == "online" && !!networkId input(type="checkbox" class="node-selector" id="select-" + key) td= i++ td #{key}