@janart19/node-red-fusebox
Version:
A collection of Fusebox-specific custom nodes for Node-RED
309 lines (246 loc) • 13 kB
JavaScript
const http = require("http");
// Custom node to perform CRUD operations via the /calendar endpoint.
module.exports = function (RED) {
function SecurityNode(config) {
RED.nodes.createNode(this, config);
const node = this;
var previousValues = {};
// Retrieve configuration settings
node.name = config.name;
node.topic = config.topic;
node.operationMode = config.operationMode;
node.outputMode = config.outputMode;
node.username = config.username;
node.usernameType = config.usernameType;
node.password = config.password;
node.passwordType = config.passwordType;
node.authorized = config.authorized;
node.authorizedType = config.authorizedType;
node.expiration = config.expiration;
node.expirationType = config.expirationType;
// Retrieve the config node's settings
node.controller = RED.nodes.getNode(config.controller);
// Validate the controller configuration
if (!node.controller || !node.controller.host || (!node.controller.httpPort && !node.controller.udpPort)) {
node.error("Controller configuration invalid");
node.status({ fill: "red", shape: "dot", text: "Controller configuration invalid" });
return;
}
// Check for unnecessary form values
const invalidValues = ["", null, undefined];
const operationModeValid = ["check", "auth"];
const outputModeValid = ["status", "passThroughAuth", "passThroughNotAuth"];
const authroziedValid = [true, false];
// Listen for input messages
node.on("input", function (msg) {
const operationMode = node.operationMode;
const outputMode = node.outputMode;
const topic = node.topic;
const username = evaluate(node.username, node.usernameType, node, msg);
const password = evaluate(node.password, node.passwordType, node, msg);
const authorized = evaluate(node.authorized, node.authorizedType, node, msg);
let expiration = evaluate(node.expiration, node.expirationType, node, msg);
// Convert date objects and ISO strings to timestamps
if (expiration instanceof Date) expiration = expiration.getTime();
if (typeof expiration === "string") expiration = new Date(expiration).getTime();
// Basic validation
if (!operationModeValid.includes(operationMode)) {
node.error(`Operation mode must be one of: ${operationModeValid.join(", ")}`);
node.status({ fill: "red", shape: "dot", text: `Invalid operation mode: ${operationMode}` });
return;
}
if (!outputModeValid.includes(outputMode)) {
node.error(`Operation mode must be one of: ${outputModeValid.join(", ")}`);
node.status({ fill: "red", shape: "dot", text: `Invalid output mode: ${outputMode}` });
return;
}
// Required: title, timestamp, source
// Optional: description, value
if (operationMode === "auth") {
if (invalidValues.includes(username)) {
node.error("Username required");
node.status({ fill: "red", shape: "dot", text: "Username required" });
return;
}
if (invalidValues.includes(password)) {
node.error("Password required");
node.status({ fill: "red", shape: "dot", text: "Password required" });
return;
}
if (!authroziedValid.includes(authorized)) {
node.error(`Authorized must be one of: ${authroziedValid.join(", ")}`);
node.status({ fill: "red", shape: "dot", text: "Invalid authorized variable" });
return;
}
}
const parameters = { username, authorized, expiration, operationMode, outputMode };
// Initialize the previous values object
const previousRequest = getPreviousValue(parameters, "request");
// Skip if a request is already in progress for this row
if (previousRequest) {
node.status({ fill: "yellow", shape: "dot", text: `Request in progress for ${title || "event"} (${formatDate()})` });
return;
}
// Build the POST request
const postData = {
configuration: { username, password, authorized, expiration },
};
setPreviousRequest(parameters, true);
// Send the POST request to the controller
sendAuthOperation(node, postData, parameters)
.then((result) => {
setPreviousRequest(parameters, false);
const success = result?.success;
const outMsg = {
...msg,
...(!invalidValues.includes(topic) && { topic }),
};
if (outputMode === "status") {
node.send({
...outMsg,
payload: result,
parameters,
controller: { id: node.controller.id, uniqueId: node.controller.uniqueId, host: node.controller.host },
});
} else if (outputMode === "passThroughAuth" && success === true) {
node.send(outMsg);
} else if (outputMode === "passThroughNotAuth" && success === false) {
node.send(outMsg);
}
})
.catch((error) => {
node.error(`Error sending authentication: ${error}`, { error });
setPreviousRequest(parameters, false);
const outMsg = {
...msg,
...(!invalidValues.includes(topic) && { topic }),
};
node.send({
...outMsg,
payload: false,
parameters,
controller: { id: node.controller.id, uniqueId: node.controller.uniqueId, host: node.controller.host },
});
});
});
// Method to query additional data via HTTP with retry mechanism
function sendAuthOperation(node, postData = {}, parameters = {}, retries = 0) {
const { operationMode } = parameters;
let path = "/program-auth";
let method = "GET";
if (operationMode === "auth") method = "POST";
const options = {
hostname: node.controller.host,
port: node.controller.httpPort,
path: path,
method: method,
headers: {
"Content-Type": "application/json",
},
};
node.debug(`Querying HTTP: ${JSON.stringify(options)} with body ${JSON.stringify(parameters)}`);
return new Promise((resolve, reject) => {
const req = http.request(options, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
try {
node.debug(`Received HTTP message: ${data}`);
const parsedData = JSON.parse(data);
if (parsedData) {
let fill = "";
let message = "";
if (operationMode === "check") {
message = parsedData.message ?? "Request sent";
fill = parsedData.success ? "green" : "red";
}
if (operationMode === "auth") {
message = parsedData.message ?? "Request sent";
fill = parsedData.success ? "green" : "red";
}
node.status({ fill: fill, shape: "dot", text: `${message} (${formatDate()})` });
resolve(parsedData);
} else {
if (retries > 0) {
node.warn(`Retrying... (${retries} attempts left)`);
node.status({ fill: "yellow", shape: "dot", text: `Retrying sending data (${formatDate()})` });
setTimeout(() => {
resolve(sendAuthOperation(node, postData, parameters, retries - 1));
}, 500);
} else {
node.error(`Failed to send data`, parameters);
node.status({ fill: "red", shape: "dot", text: `Failed to send data (${formatDate()})` });
resolve(false);
}
}
} catch (error) {
node.status({ fill: "red", shape: "dot", text: `Failed to parse HTTP response (${formatDate()})` });
// Retry if necessary
if (retries > 0) {
node.warn(`Retrying... (${retries} attempts left)`);
setTimeout(() => {
resolve(sendAuthOperation(node, postData, parameters, retries - 1));
}, 500);
} else {
node.error(`Failed to parse HTTP response: ${error}`, { error });
reject(error);
}
}
});
});
req.on("error", (error) => {
node.status({ fill: "red", shape: "dot", text: `HTTP request error (${formatDate()})` });
// Retry if necessary
if (retries > 0) {
node.warn(`Retrying... (${retries} attempts left)`);
setTimeout(() => {
resolve(sendAuthOperation(node, postData, parameters, retries - 1));
}, 500);
} else {
node.error(`HTTP request error: ${error}`, { error });
reject(error);
}
});
// Write data to request body
if (method !== "GET") {
req.write(JSON.stringify(postData));
}
req.end();
});
}
// Evaluate the value of a property, catching any errors (e.g. read properties of undefined : msg.payload.success)
function evaluate(value, type, node, msg) {
try {
return RED.util.evaluateNodeProperty(value, type, node, msg);
} catch (err) {
return undefined;
}
}
function getPreviousValue(parameters = {}, key = "title") {
const { title = "_default" } = parameters;
if (!previousValues[title]) previousValues[title] = { title: null, value: null, start: null, request: null };
return previousValues[title][key] ?? null;
}
function setPreviousRequest(parameters = {}, status = null) {
const { title = "_default" } = parameters;
previousValues[title].request = status;
}
// Format the current date and time as DD/MM/YYYY HH:MM:SS
function formatDate() {
const now = new Date();
const options = {
day: "2-digit",
month: "2-digit",
year: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false, // Use 24-hour format
};
return now.toLocaleString("en-GB", options); // 'en-GB' locale for DD/MM/YYYY format
}
}
RED.nodes.registerType("fusebox-security", SecurityNode);
};