node-red-contrib-uibuilder
Version:
Easily create web UI's for Node-RED using any (or no) front-end library. VueJS and bootstrap-vue included but change as desired.
1,131 lines (1,123 loc) • 50.7 kB
JavaScript
"use strict";
const uiblib = require("./uiblib");
const tilib = require("./tilib");
const templateConf = require("../templates/template_dependencies");
const sockets = require("./socket.js");
const web = require("./web.js");
const path = require("path");
const child_process = require("child_process");
const serveStatic = require("serve-static");
const Express = require("express");
const fs = require("fs-extra");
const fg = require("fast-glob");
const uib = {
me: fs.readJSONSync(path.join(__dirname, "..", "package.json")),
moduleName: "uibuilder",
nodeRoot: "",
deployments: {},
instances: {},
masterPackageListFilename: "masterPackageList.json",
packageListFilename: "packageList.json",
installedPackages: {},
masterTemplateFolder: path.join(__dirname, "..", "templates"),
masterTemplate: "vue",
masterStaticDistFolder: path.join(__dirname, "..", "front-end", "dist"),
masterStaticSrcFolder: path.join(__dirname, "..", "front-end", "src"),
rootFolder: null,
configFolder: null,
configFolderName: ".config",
commonFolder: null,
commonFolderName: "common",
sioUseMwName: "sioUse.js",
ioChannels: {control: "uiBuilderControl", client: "uiBuilderClient", server: "uiBuilder"},
nodeVersion: process.version.replace("v", "").split("."),
staticOpts: {},
deleteOnDelete: {},
customServer: {
port: void 0,
type: "http",
host: void 0
}
};
uib.version = uib.me.version;
var dummyLog = {
fatal: function() {
},
error: function() {
},
warn: function() {
},
info: function() {
},
debug: function() {
},
trace: function() {
}
};
var log = dummyLog;
var userDir = "";
module.exports = function(RED) {
userDir = RED.settings.userDir;
uib.rootFolder = path.join(userDir, uib.moduleName);
if (uiblib.getProps(RED, RED.settings.get("editorTheme"), "projects.enabled") === true) {
const currProject = uiblib.getProps(RED, RED.settings.get("projects"), "activeProject", "");
if (currProject !== "")
uib.rootFolder = path.join(userDir, "projects", currProject, uib.moduleName);
}
uib.configFolder = path.join(uib.rootFolder, uib.configFolderName);
uib.commonFolder = path.join(uib.rootFolder, uib.commonFolderName);
log = RED.log;
log.trace("[uibuilder:Module] ----------------- uibuilder - module started -----------------");
var uib_rootFolder_OK = true;
try {
fs.ensureDirSync(uib.configFolder);
} catch (e) {
if (e.code !== "EEXIST") {
RED.log.error(`uibuilder: Custom folder ERROR, path: ${uib.rootFolder}. ${e.message}`);
uib_rootFolder_OK = false;
}
}
try {
fs.accessSync(uib.rootFolder, fs.constants.R_OK | fs.constants.W_OK);
} catch (e) {
RED.log.error(`uibuilder: Root folder is not accessible, path: ${uib.rootFolder}. ${e.message}`);
uib_rootFolder_OK = false;
}
if (uib_rootFolder_OK === true) {
const fsOpts = {"overwrite": false, "preserveTimestamps": true};
try {
fs.copySync(path.join(uib.masterTemplateFolder, uib.configFolderName), uib.configFolder, fsOpts);
} catch (e) {
RED.log.error(`uibuilder: Master .config folder copy ERROR, path: ${uib.masterTemplateFolder}. ${e.message}`);
uib_rootFolder_OK = false;
}
try {
fs.copy(path.join(uib.masterTemplateFolder, uib.commonFolderName), uib.commonFolder, fsOpts, function(err) {
if (err) {
log.error(`[uibuilder] Error copying common template folder from ${path.join(uib.masterTemplateFolder, uib.commonFolderName)} to ${uib.commonFolder}`, err);
} else {
log.trace(`[uibuilder] Copied common template folder to local common folder ${uib.commonFolder} (not overwriting)`);
}
});
} catch (e) {
log.error("[uibuilder] COPY OF COMMON FOLDER FAILED");
}
}
if (uib_rootFolder_OK !== true) {
throw new Error(`uibuilder: Failed to set up uibuilder root folder structure correctly. Check log for additional error messages. Root folder: ${uib.rootFolder}.`);
}
web.setup(RED, uib, log);
sockets.setup(RED, uib, log, web.server);
web.checkInstalledPackages();
RED.log.info("+-----------------------------------------------------");
RED.log.info(`| ${uib.moduleName} initialised:`);
if (uib.customServer.port)
RED.log.info(`| Using custom ${uib.customServer.type} webserver on port ${uib.customServer.port}`);
else
RED.log.info("| Using Node-RED's webserver");
RED.log.info(`| root folder: ${uib.rootFolder}`);
RED.log.info(`| version . .: ${uib.version}`);
RED.log.info(`| packages . : ${Object.keys(uib.installedPackages)}`);
RED.log.info("+-----------------------------------------------------");
function nodeInstance(config) {
RED.nodes.createNode(this, config);
var uibInstance = config.url;
log.trace(`[uibuilder:${uibInstance}] ================ instance registered ================`);
const node = this;
log.trace(`[uibuilder:${uibInstance}] = Keys: this, config =`, {"this": Object.keys(node), "config": Object.keys(config)});
node.name = config.name || "";
node.topic = config.topic || "";
node.url = config.url || "uibuilder";
node.oldUrl = config.oldUrl;
node.fwdInMessages = config.fwdInMessages === void 0 ? false : config.fwdInMessages;
node.allowScripts = config.allowScripts === void 0 ? false : config.allowScripts;
node.allowStyles = config.allowStyles === void 0 ? false : config.allowStyles;
node.copyIndex = config.copyIndex === void 0 ? true : config.copyIndex;
node.templateFolder = config.templateFolder || templateConf.vue.folder;
node.showfolder = config.showfolder === void 0 ? false : config.showfolder;
node.useSecurity = config.useSecurity;
node.sessionLength = Number(config.sessionLength) || 120;
node.jwtSecret = node.credentials.jwtSecret || "thisneedsreplacingwithacredential";
node.tokenAutoExtend = config.tokenAutoExtend === void 0 ? false : config.tokenAutoExtend;
node.reload = config.reload === void 0 ? false : config.reload;
log.trace(`[uibuilder:${uibInstance}] Node instance settings`, {"name": node.name, "topic": node.topic, "url": node.url, "copyIndex": node.copyIndex, "fwdIn": node.fwdInMessages, "allowScripts": node.allowScripts, "allowStyles": node.allowStyles, "showfolder": node.showfolder});
uib.instances[node.id] = node.url;
log.trace(`[uibuilder:${uibInstance}] Node uib.Instances Registered`, uib.instances);
if (Object.prototype.hasOwnProperty.call(uib.deployments, node.id))
uib.deployments[node.id]++;
else
uib.deployments[node.id] = 1;
log.trace(`[uibuilder:${uibInstance}] Number of uib.Deployments`, uib.deployments[node.id]);
node.customFolder = path.join(uib.rootFolder, node.url);
if (node.oldUrl !== void 0 && node.url !== node.oldUrl) {
try {
fs.moveSync(path.join(uib.rootFolder, node.oldUrl), node.customFolder, {overwrite: false});
} catch (e) {
if (e.code !== "ENOENT")
log.error(`[uibuilder] RENAME OF INSTANCE FOLDER FAILED. Fatal. url=${node.url}, oldUrl=${node.oldUrl}, Fldr=${node.customFolder}. Error=${e.message}`, e);
}
}
var customFoldersOK = true;
try {
fs.mkdirSync(node.customFolder);
fs.accessSync(node.customFolder, fs.constants.W_OK);
} catch (e) {
if (e.code !== "EEXIST") {
log.error(`[uibuilder:${uibInstance}] Local custom folder ERROR`, e.message);
customFoldersOK = false;
}
}
try {
fs.mkdirSync(path.join(node.customFolder, "dist"));
fs.mkdirSync(path.join(node.customFolder, "src"));
} catch (e) {
if (e.code !== "EEXIST") {
log.error(`[uibuilder:${uibInstance}] Local custom dist or src folder ERROR`, e.message);
customFoldersOK = false;
}
}
if (uib_rootFolder_OK === true && customFoldersOK === true) {
log.trace(`[uibuilder:${uibInstance}] Using local front-end folders in`, node.customFolder);
if (node.copyIndex) {
const cpyOpts = {"overwrite": false, "preserveTimestamps": true};
try {
fs.copy(path.join(uib.masterTemplateFolder, node.templateFolder), node.customFolder, cpyOpts, function(err) {
if (err) {
log.error(`[uibuilder:${uibInstance}] Error copying template files from ${path.join(__dirname, "templates", node.templateFolder)} to ${node.customFolder} Error=${err.message}`, err);
} else {
log.trace(`[uibuilder:${uibInstance}] Copied template files from ${path.join(__dirname, "templates", node.templateFolder)} to local src (not overwriting)`, node.customFolder);
}
});
} catch (e) {
log.error(`[uibuilder] COPY OF TEMPLATE TO INSTANCE FOLDER FAILED. Fatal. Error=${e.message}`, e);
}
}
} else {
log.error(`[uibuilder:${uibInstance}] Wanted to use local front-end folders in ${node.customFolder} but could not`);
}
web.instanceSetup(node);
const ioNs = sockets.addNS(node);
log.debug(`uibuilder : ${uibInstance} : URL . . . . . : ${tilib.urlJoin(uib.nodeRoot, node.url)}`);
log.debug(`uibuilder : ${uibInstance} : Source files . : ${node.customFolder}`);
uiblib.setNodeStatus({fill: "blue", shape: "dot", text: "Node Initialised"}, node);
function nodeInputHandler(msg, send, done) {
log.trace(`[uibuilder:${uibInstance}] nodeInstance:nodeInputHandler - emit received msg - Namespace: ${node.url}`);
send = send || function() {
node.send.apply(node, arguments);
};
done = done || function() {
if (arguments.length > 0)
node.done.apply(node, arguments);
};
if (msg !== null) {
if (typeof msg !== "object") {
msg = {"payload": msg};
}
if (!Object.prototype.hasOwnProperty.call(msg, "topic") || msg.topic === "") {
if (node.topic !== "")
msg.topic = node.topic;
else
msg.topic = uib.moduleName;
}
}
msg = uiblib.inputHandler(msg, send, done, node, RED, sockets.io, ioNs, log, uib);
}
node.on("input", nodeInputHandler);
node.on("close", function(removed, done) {
log.trace(`[uibuilder:${uibInstance}] nodeInstance:on-close: ${removed ? "Node Removed" : "Node (re)deployed"}`);
node.removeListener("input", nodeInputHandler);
uiblib.instanceClose(node, RED, uib, sockets, web, log, done);
done();
});
RED.httpAdmin.get(`/uibuilder/instance/${node.url}`, function(req, res) {
let page = uiblib.showInstanceDetails(req, node, uib, userDir, RED);
res.status(200).send(page);
});
}
RED.nodes.registerType(uib.moduleName, nodeInstance, {
credentials: {
jwtSecret: {type: "password"}
},
settings: {
uibuilderNodeEnv: {value: process.env.NODE_ENV, exportable: true},
uibuilderTemplates: {value: templateConf, exportable: true},
uibuilderCustomServer: {value: uib.customServer, exportable: true}
}
});
function chkParamUrl(params) {
const res = {"statusMessage": "", "status": 0};
if (params.url === void 0) {
res.statusMessage = "url parameter not provided";
res.status = 500;
return res;
}
if (params.url.length > 20) {
res.statusMessage = `url parameter is too long. Max 20 characters: ${params.url}`;
res.status = 500;
return res;
}
if (params.url.length < 1) {
res.statusMessage = "url parameter is empty, please provide a value";
res.status = 500;
return res;
}
if (params.url.includes("..")) {
res.statusMessage = `url parameter may not contain "..": ${params.url}`;
res.status = 500;
return res;
}
return res;
}
function chkParamFname(params) {
const res = {"statusMessage": "", "status": 0};
const fname = params.fname;
if (fname === void 0) {
res.statusMessage = "file name not provided";
res.status = 500;
return res;
}
if (fname === "") {
res.statusMessage = "file name cannot be blank";
res.status = 500;
return res;
}
if (fname.length > 255) {
res.statusMessage = `file name is too long. Max 255 characters: ${params.fname}`;
res.status = 500;
return res;
}
if (fname.includes("..")) {
res.statusMessage = `file name may not contain "..": ${params.fname}`;
res.status = 500;
return res;
}
return res;
}
function chkParamFldr(params) {
const res = {"statusMessage": "", "status": 0};
let folder = params.folder;
if (folder === void 0) {
res.statusMessage = "folder name not provided";
res.status = 500;
return res;
}
if (folder === "") {
res.statusMessage = "folder name cannot be blank";
res.status = 500;
return res;
}
if (folder.length > 255) {
res.statusMessage = `folder name is too long. Max 255 characters: ${folder}`;
res.status = 500;
return res;
}
if (folder.includes("..")) {
res.statusMessage = `folder name may not contain "..": ${folder}`;
res.status = 500;
return res;
}
return res;
}
RED.httpAdmin.route("/uibuilder/admin/:url").all(function(req, res, next) {
const params = res.allparams = Object.assign({}, req.query, req.body, req.params);
params.type = "all";
const chkUrl = chkParamUrl(params);
if (chkUrl.status !== 0) {
log.error(`[uibuilder:admin-router:ALL] Admin API. ${chkUrl.statusMessage}`);
res.statusMessage = chkUrl.statusMessage;
res.status(chkUrl.status).end();
return;
}
next();
}).get(function(req, res) {
const params = res.allparams;
params.type = "get";
if (params.cmd === "listall") {
log.trace(`[uibuilder:admin-router:GET] Admin API. List all folders and files. url=${params.url}, root fldr=${uib.rootFolder}`);
const out = {"root": []};
const root2 = uib.rootFolder.replace(/\\/g, "/");
fg.stream([`${root2}/${params.url}/**`], {dot: true, onlyFiles: false, deep: 10, followSymbolicLinks: true, markDirectories: true}).on("data", (entry) => {
entry = entry.replace(`${root2}/${params.url}/`, "");
let fldr;
if (entry.endsWith("/")) {
fldr = entry.slice(0, -1);
if (fldr === "")
fldr = "root";
out[fldr] = [];
} else {
let splitEntry = entry.split("/");
let last = splitEntry.pop();
fldr = splitEntry.join("/");
if (fldr === "")
fldr = "root";
out[fldr].push(last);
}
}).on("end", () => {
res.statusMessage = "Folders and Files listed successfully";
res.status(200).json(out);
});
return;
} else if (params.cmd === "checkurls") {
log.trace(`[uibuilder:admin-router:GET:checkurls] Check if URL is already in use. URL: ${params.url}`);
let chkInstances = Object.values(uib.instances).includes(params.url);
let chkFolders = fs.existsSync(path.join(uib.rootFolder, params.url));
res.statusMessage = "Instances and Folders checked";
res.status(200).json(chkInstances || chkFolders);
return;
} else if (params.cmd === "listurls") {
var route, routes = [];
web.app._router.stack.forEach((middleware) => {
if (middleware.route) {
let path2 = middleware.route.path;
let methods = middleware.route.methods;
routes.push({path: path2, methods});
} else if (middleware.name === "router") {
middleware.handle.stack.forEach(function(handler) {
route = handler.route;
route && routes.push(route);
});
}
});
console.log(web.app._router.stack[0]);
log.trace("[uibuilder:admin-router:GET:listurls] Admin API. List of all user urls in use.");
res.statusMessage = "URLs listed successfully";
res.status(200).json(web.app._router.stack);
return;
}
}).put(function(req, res) {
const params = res.allparams;
params.type = "put";
let fullname = path.join(uib.rootFolder, params.url);
if (params.cmd && params.cmd === "deleteondelete") {
log.trace(`[uibuilder:admin-router:PUT:deleteondelete] Admin API. url=${params.url}`);
uib.deleteOnDelete[params.url] = true;
res.statusMessage = "PUT successfully";
res.status(200).json({});
return;
}
log.trace(`[uibuilder:admin-router:PUT] Admin API. url=${params.url}`);
res.statusMessage = "PUT successfully";
res.status(200).json({
"fullname": fullname,
"params": params
});
}).post(function(req, res) {
const params = res.allparams;
params.type = "post";
const chkFldr = chkParamFldr(params);
if (chkFldr.status !== 0) {
log.error(`[uibuilder:admin-router:POST] Admin API. ${chkFldr.statusMessage}. url=${params.url}`);
res.statusMessage = chkFldr.statusMessage;
res.status(chkFldr.status).end();
return;
}
if (!(params.cmd && (params.cmd === "newfolder" || params.cmd === "newfile"))) {
let statusMsg = `cmd parameter not present or wrong value (must be 'newfolder' or 'newfile'). url=${params.url}, cmd=${params.cmd}`;
log.error(`[uibuilder:admin-router:POST] Admin API. ${statusMsg}`);
res.statusMessage = statusMsg;
res.status(500).end();
return;
}
if (params.cmd === "newfile") {
const chkFname = chkParamFname(params);
if (chkFname.status !== 0) {
log.error(`[uibuilder:admin-router:POST] Admin API. ${chkFname.statusMessage}. url=${params.url}`);
res.statusMessage = chkFname.statusMessage;
res.status(chkFname.status).end();
return;
}
}
let fullname = path.join(uib.rootFolder, params.url, params.folder);
if (params.cmd === "newfile") {
fullname = path.join(fullname, params.fname);
}
if (fs.pathExistsSync(fullname)) {
let statusMsg = `selected ${params.cmd === "newfolder" ? "folder" : "file"} already exists. url=${params.url}, cmd=${params.cmd}, folder=${params.folder}`;
log.error(`[uibuilder:admin-router:POST] Admin API. ${statusMsg}`);
res.statusMessage = statusMsg;
res.status(500).end();
return;
}
try {
if (params.cmd === "newfolder") {
fs.ensureDirSync(fullname);
} else {
fs.ensureFileSync(fullname);
}
} catch (e) {
let statusMsg = `could not create ${params.cmd === "newfolder" ? "folder" : "file"}. url=${params.url}, cmd=${params.cmd}, folder=${params.folder}, error=${e.message}`;
log.error(`[uibuilder:admin-router:POST] Admin API. ${statusMsg}`);
res.statusMessage = statusMsg;
res.status(500).end();
return;
}
log.trace(`[uibuilder:admin-router:POST] Admin API. Folder/File create SUCCESS. url=${params.url}, file=${params.folder}/${params.fname}`);
res.statusMessage = "Folder/File created successfully";
res.status(200).json({
"fullname": fullname,
"params": params
});
}).delete(function(req, res) {
const params = res.allparams;
params.type = "delete";
const chkFldr = chkParamFldr(params);
if (chkFldr.status !== 0) {
log.error(`[uibuilder:admin-router:DELETE] Admin API. ${chkFldr.statusMessage}. url=${params.url}`);
res.statusMessage = chkFldr.statusMessage;
res.status(chkFldr.status).end();
return;
}
if (!(params.cmd && (params.cmd === "deletefolder" || params.cmd === "deletefile"))) {
let statusMsg = `cmd parameter not present or wrong value (must be 'deletefolder' or 'deletefile'). url=${params.url}, cmd=${params.cmd}`;
log.error(`[uibuilder:admin-router:DELETE] Admin API. ${statusMsg}`);
res.statusMessage = statusMsg;
res.status(500).end();
return;
}
if (params.cmd === "deletefile") {
const chkFname = chkParamFname(params);
if (chkFname.status !== 0) {
log.error(`[uibuilder:admin-router:DELETE] Admin API. ${chkFname.statusMessage}. url=${params.url}`);
res.statusMessage = chkFname.statusMessage;
res.status(chkFname.status).end();
return;
}
}
let fullname = path.join(uib.rootFolder, params.url, params.folder);
if (params.cmd === "deletefile") {
fullname = path.join(fullname, params.fname);
}
if (!fs.pathExistsSync(fullname)) {
let statusMsg = `selected ${params.cmd === "deletefolder" ? "folder" : "file"} does not exist. url=${params.url}, cmd=${params.cmd}, folder=${params.folder}`;
log.error(`[uibuilder:admin-router:DELETE] Admin API. ${statusMsg}`);
res.statusMessage = statusMsg;
res.status(500).end();
return;
}
try {
fs.removeSync(fullname);
} catch (e) {
let statusMsg = `could not delete ${params.cmd === "deletefolder" ? "folder" : "file"}. url=${params.url}, cmd=${params.cmd}, folder=${params.folder}, error=${e.message}`;
log.error(`[uibuilder:admin-router:DELETE] Admin API. ${statusMsg}`);
res.statusMessage = statusMsg;
res.status(500).end();
return;
}
log.trace(`[uibuilder:admin-router:DELETE] Admin API. Folder/File delete SUCCESS. url=${params.url}, file=${params.folder}/${params.fname}`);
res.statusMessage = "Folder/File deleted successfully";
res.status(200).json({
"fullname": fullname,
"params": params
});
});
RED.httpAdmin.get("/uibgetfile", function(req, res) {
const params = req.query;
const chkUrl = chkParamUrl(params);
if (chkUrl.status !== 0) {
log.error(`[uibuilder:uibgetfile] Admin API. ${chkUrl.statusMessage}`);
res.statusMessage = chkUrl.statusMessage;
res.status(chkUrl.status).end();
return;
}
const chkFname = chkParamFname(params);
if (chkFname.status !== 0) {
log.error(`[uibuilder:uibgetfile] Admin API. ${chkFname.statusMessage}. url=${params.url}`);
res.statusMessage = chkFname.statusMessage;
res.status(chkFname.status).end();
return;
}
const chkFldr = chkParamFldr(params);
if (chkFldr.status !== 0) {
log.error(`[uibuilder:uibgetfile] Admin API. ${chkFldr.statusMessage}. url=${params.url}`);
res.statusMessage = chkFldr.statusMessage;
res.status(chkFldr.status).end();
return;
}
log.trace(`[uibuilder:uibgetfile] Admin API. File get requested. url=${params.url}, file=${params.folder}/${params.fname}`);
if (params.folder === "root")
params.folder = "";
const filePathRoot = path.join(uib.rootFolder, req.query.url, params.folder);
const filePath = path.join(filePathRoot, req.query.fname);
if (fs.existsSync(filePath)) {
res.type("text/plain").sendFile(req.query.fname, {
"root": filePathRoot,
"lastModified": false,
"cacheControl": false,
"dotfiles": "allow"
});
} else {
log.error(`[uibuilder:uibgetfile] Admin API. File does not exist '${filePath}'. url=${params.url}`);
res.statusMessage = "File does not exist";
res.status(500).end();
}
});
RED.httpAdmin.post("/uibputfile", function(req, res) {
const params = req.body;
const chkUrl = chkParamUrl(params);
if (chkUrl.status !== 0) {
log.error(`[uibuilder:uibputfile] Admin API. ${chkUrl.statusMessage}`);
res.statusMessage = chkUrl.statusMessage;
res.status(chkUrl.status).end();
return;
}
const chkFname = chkParamFname(params);
if (chkFname.status !== 0) {
log.error(`[uibuilder:uibputfile] Admin API. ${chkFname.statusMessage}. url=${params.url}`);
res.statusMessage = chkFname.statusMessage;
res.status(chkFname.status).end();
return;
}
const chkFldr = chkParamFldr(params);
if (chkFldr.status !== 0) {
log.error(`[uibuilder:uibputfile] Admin API. ${chkFldr.statusMessage}. url=${params.url}`);
res.statusMessage = chkFldr.statusMessage;
res.status(chkFldr.status).end();
return;
}
log.trace(`[uibuilder:uibputfile] Admin API. File put requested. url=${params.url}, file=${params.folder}/${params.fname}, reload? ${params.reload}`);
const fullname = path.join(uib.rootFolder, params.url, params.folder, params.fname);
fs.writeFile(fullname, req.body.data, function(err, data) {
if (err) {
log.error(`[uibuilder:uibputfile] Admin API. File write FAIL. url=${params.url}, file=${params.folder}/${params.fname}`, err);
res.statusMessage = err;
res.status(500).end();
} else {
log.trace(`[uibuilder:uibputfile] Admin API. File write SUCCESS. url=${params.url}, file=${params.folder}/${params.fname}`);
res.statusMessage = "File written successfully";
res.status(200).end();
if (params.reload) {
sockets.send({
"_uib": {
"reload": true
}
}, params.url);
}
}
});
});
RED.httpAdmin.get("/uibindex", function(req, res) {
log.trace("[uibindex] User Page/API. List all available uibuilder endpoints");
const url = new URL(req.headers.referer);
url.pathname = "";
if (uib.customServer.port) {
url.port = uib.customServer.port;
}
const urlPrefix = url.href;
switch (req.query.type) {
case "json": {
res.json(uib.instances);
break;
}
case "urls": {
res.json(Object.values(uib.instances));
break;
}
default: {
web.checkInstalledPackages();
var otherPaths = [], uibPaths = [];
var urlRe = new RegExp("^" + tilib.escapeRegExp("/^\\/uibuilder\\/vendor\\") + ".*$");
web.app._router.stack.forEach(function(r, i, stack) {
let rUrl = r.regexp.toString().replace(urlRe, "");
if (rUrl === "") {
uibPaths.push({
"name": r.name,
"regex": r.regexp.toString(),
"route": r.route,
"path": r.path,
"params": r.params,
"keys": r.keys,
"method": r.route ? Object.keys(r.route.methods)[0].toUpperCase() : "ANY",
"handle": r.handle.toString()
});
} else {
otherPaths.push({
"name": r.name,
"regex": r.regexp.toString(),
"route": r.route,
"path": r.path,
"params": r.params,
"keys": r.keys,
"method": r.route ? Object.keys(r.route.methods)[0].toUpperCase() : "ANY",
"handle": r.handle.toString()
});
}
});
let page = `
<!doctype html><html lang="en"><head>
<title>Uibuilder Index</title>
<link type="text/css" href="${urlPrefix}${uib.nodeRoot.replace("/", "")}${uib.moduleName}/vendor/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" media="screen">
<link rel="icon" href="${urlPrefix}${uib.nodeRoot.replace("/", "")}${uib.moduleName}/common/images/node-blue.ico">
<style type="text/css" media="all">
h2 { border-top:1px solid silver;margin-top:1em;padding-top:0.5em; }
.col3i tbody>tr>:nth-child(3){ font-style:italic; }
</style>
</head><body><div class="container">
<h1>uibuilder Detailed Information Page</h1>
<p>
Note that this page is only accessible to users with Node-RED admin authority.
</p>
`;
page += `
<h2>Index of uibuilder pages</h2>
<p>'Folders' refer to locations on your Node-RED's server. 'Paths' refer to URL's in the browser.</p>
<table class="table">
<thead><tr>
<th>URL</th>
<th title="Use this to search for the source node in the admin ui">Source Node Instance <a href="#i2"><sup>(2)</sup></th>
<th>Server Filing System Folder</th>
</tr></thead><tbody>
`;
Object.keys(uib.instances).forEach((key) => {
page += `
<tr>
<td><a href="${urlPrefix}${tilib.urlJoin(uib.nodeRoot, uib.instances[key]).replace("/", "")}" target="_blank">${uib.instances[key]}</a></td>
<td>${key}</td>
<td>${path.join(uib.rootFolder, uib.instances[key])}</td>
</tr>
`;
});
page += `
</tbody></table>
<p>Notes:</p>
<ol>
<li><a id="i1"></a>
Each instance of uibuilder uses its own socket.io <i>namespace</i> that matches <code>httpNodeRoot/url</code>.
You can use this to manually send messages to your user interface.
</li>
<li><a id="i2"></a>
Paste the Source Node Instance into the search feature in the Node-RED admin ui to find the instance.
The "Filing System Folder" shows you where the front-end (client browser) code lives.
</li>
</ol>
`;
page += `
<h2>Vendor Client Libraries</h2>
<p>
You can include these libraries in any uibuilder served web page.
Note though that you need to find out the correct file and relative folder either by looking on
your Node-RED server in the location shown or by looking at the packages source online.
</p>
<table class="table">
<thead><tr>
<th>Package</th>
<th>Version</th>
<th>uibuilder URL <a href="#vl1"><sup>(1)</sup></a></th>
<th>Browser Entry Point (est.) <a href="#vl2"><sup>(2)</sup></a></th>
<th>Server Filing System Folder</th>
</tr></thead><tbody>
`;
Object.keys(uib.installedPackages).forEach((packageName) => {
let pj = uib.installedPackages[packageName];
let mainTxt = "<i>Not Supplied</i>";
if (pj.browser !== "") {
mainTxt = `<a href="${urlPrefix}${tilib.urlJoin(uib.nodeRoot, pj.url.replace("..", ""), pj.browser).replace("/", "")}">${pj.url}/${pj.browser}</a>`;
} else if (pj.main !== "") {
mainTxt = `<a href="${urlPrefix}${tilib.urlJoin(uib.nodeRoot, pj.url.replace("..", ""), pj.main).replace("/", "")}">${pj.url}/${pj.main}</a>`;
}
page += `
<tr>
<td><a href="${pj.homepage}">${packageName}</a></td>
<td>${pj.version}</td>
<td>${pj.url}</td>
<td>${mainTxt}</td>
<td>${pj.folder}</td>
</tr>
`;
});
page += `
</tbody></table>
<p>Notes:</p>
<ol>
<li><a id="vl1"></a>
Always use relative URL's. All vendor URL's start <code>../uibuilder/vendor/</code>,
all uibuilder and custom file URL's start <code>./</code>.<br>
Using relative URL's saves you from needing to worry about http(s), ip names/addresses and port numbers.
</li>
<li><a id="vl2"></a>
The 'Main Entry Point' shown is <i>usually</i> a JavaScript file that you will want in your index.html.
However, because this is reported by the authors of the package, it may refer to something completely different,
uibuilder has no way of knowing. Treat it as a hint rather than absolute truth. Check the packages documentation
for the correct library files to load.
</li>
</ol>
`;
page += `
<h2>Configuration</h2>
<h3>uibuilder</h3>
<table class="table col3i">
<tr>
<th>uibuilder Version</th>
<td>${uib.version}</td>
<td></td>
</tr>
<tr>
<th>uib.rootFolder</th>
<td>${uib.rootFolder}</td>
<td>All uibuilder data lives here</td>
</tr>
<tr>
<th>uib.configFolder</th>
<td>${uib.configFolder}</td>
<td>uibuilder Global Configuration Folder</td>
</tr>
<tr>
<th>uib.commonFolder</th>
<td>${uib.commonFolder}</td>
<td>Used for loading common resources between multiple uibuilder instances</td>
</tr>
<tr>
<th>Common URL</th>
<td>../${uib.moduleName}/common</td>
<td>The common folder maps to this URL</td>
</tr>
<tr title="">
<th>uib_socketPath</th>
<td>${sockets.uib_socketPath}</td>
<td>Unique path given to Socket.IO to ensure isolation from other Nodes that might also use it</td>
</tr>
<tr>
<th>uib.masterPackageListFilename</th>
<td>${uib.masterPackageListFilename}</td>
<td>Holds a list of npm packages automatically recognised, uibuilder will add URL's for these</td>
</tr>
<tr>
<th>uib.packageListFilename</th>
<td>${uib.packageListFilename}</td>
<td>The list of npm packages actually being served</td>
</tr>
<tr>
<th>uib.masterTemplateFolder</th>
<td>${uib.masterTemplateFolder}</td>
<td>The source templates, copied to any new instance</td>
</tr>
<tr>
<th>uib.masterTemplate</th>
<td>${uib.masterTemplate}</td>
<td>Which master template is in use</td>
</tr>
</table>
<h3>Configuration Files</h3>
<p>All are kept in the master configuration folder: ${uib.configFolder}</p>
<dl style="margin-left:1em;">
<dt>${uib.masterPackageListFilename}</dt>
<dd>Holds a list of npm packages automatically recognised, uibuilder will add URL's for these.</dd>
<dt>${uib.packageListFilename}</dt>
<dd>The list of npm packages actually installed and being served.</dd>
<dt>${uib.sioUseMwName}</dt>
<dd>Custom Socket.IO Middleware file, also uibMiddleware.js.</dd>
<dt>uibMiddleware.js</dt>
<dd>Custom ExpressJS Middleware file.</dd>
</dl>
<h3>Node-RED</h3>
<p>See the <code><userDir>/settings.js</code> file and the
<a href="https://nodered.org/docs/" target="_blank">Node-RED documentation</a> for details.</p>
<table class="table">
<tr><th>userDir</th><td>${userDir}</td></tr>
<tr><th>httpNodeRoot</th><td>${uib.nodeRoot}</td></tr>
<tr><th>Node-RED Version</th><td>${RED.settings.version}</td></tr>
<tr><th>Min. Version Required by uibuilder</th><td>${uib.me["node-red"].version}</td></tr>
</table>
<h3>Node.js</h3>
<table class="table">
<tr><th>Version</th><td>${uib.nodeVersion.join(".")}</td></tr>
<tr><th>Min. version required by uibuilder</th><td>${uib.me.engines.node}</td></tr>
</table>
<h3>ExpressJS</h3>
<p>
See the <a href="https://expressjs.com/en/api.html#app.settings.table" target="_blank">ExpressJS documentation</a> for details.
Note that ExpressJS Views are not current used by uibuilder
</p>
<table class="table">
<tr><th>Views Folder</th><td>${web.app.get("views")}</td></tr>
<tr><th>Views Engine</th><td>${web.app.get("view engine")}</td></tr>
<tr><th>Views Cache</th><td>${web.app.get("view cache")}</td></tr>
</table>
<h4>app.locals</h4>
<pre>${tilib.syntaxHighlight(web.app.locals)}</pre>
<h4>app.mountpath</h4>
<pre>${tilib.syntaxHighlight(web.app.mountpath)}</pre>
`;
page += `
<h2>Installed Packages</h2>
<p>
These are the front-end libraries uibuilder knows to be installed and made available via ExpressJS serve-static.
This is the raw view of the Vendor Client Libraries table above.
</p>
<pre>${tilib.syntaxHighlight(uib.installedPackages)}</pre>
`;
page += `
<h2>uibuilder Vendor ExpressJS Paths</h2>
<p>
A raw view of the ExpressJS app.use paths currently in use serving vendor packages.
Generally, you will need to interpret the "regex" line to work out what URL is being served.
</p>
<pre>${tilib.syntaxHighlight(uibPaths)}</pre>
`;
page += `
<h2>Other ExpressJS Paths</h2>
<p>A raw view of all other app.use paths being served.</p>
<pre>${tilib.syntaxHighlight(otherPaths)}</pre>
`;
page += "</div></body></html>";
res.send(page);
break;
}
}
});
RED.httpAdmin.get("/uibvendorpackages", function(req, res) {
web.checkInstalledPackages();
res.json(uib.installedPackages);
});
RED.httpAdmin.get("/uibnpmmanage", function(req, res) {
const params = req.query;
if (params.cmd === void 0) {
log.error("[uibuilder/uibnpmmanage] uibuilder Admin API. No command provided for npm management.");
res.statusMessage = "npm command parameter not provided";
res.status(500).end();
return;
}
switch (params.cmd) {
case "install":
case "remove":
case "update":
break;
default:
log.error("[uibuilder/uibnpmmanage] uibuilder Admin API. Invalid command provided for npm management.");
res.statusMessage = "npm command parameter is invalid";
res.status(500).end();
return;
}
if (params.package === void 0) {
log.error("[uibuilder/uibnpmmanage] uibuilder Admin API. package parameter not provided");
res.statusMessage = "package parameter not provided";
res.status(500).end();
return;
}
if (params.package.length > 255) {
log.error("[uibuilder/uibnpmmanage] uibuilder Admin API. package name parameter is too long (>255 characters)");
res.statusMessage = "package name parameter is too long. Max 255 characters";
res.status(500).end();
return;
}
const folder = userDir;
log.info(`[uibuilder/uibnpmmanage] Admin API. Running npm ${params.cmd} for package ${params.package}`);
fs.removeSync(path.join(folder, "package-lock.json"));
var command = "";
switch (params.cmd) {
case "install": {
command = `npm install --no-audit --no-update-notifier --save --production --color=false --no-fund --json ${params.package}@latest`;
break;
}
case "remove": {
command = `npm remove --no-audit --no-update-notifier --color=false --json ${params.package}`;
break;
}
case "update": {
break;
}
}
if (command === "") {
log.error("[uibuilder/uibnpmmanage] uibuilder Admin API. No valid command available for npm management.");
res.statusMessage = "No valid npm command available";
res.status(500).end();
return;
}
var output = [], errOut = null, success = false;
child_process.exec(command, {"cwd": folder}, (error, stdout, stderr) => {
if (error) {
log.warn(`[uibuilder/uibnpmmanage] Admin API. ERROR Running npm ${params.cmd} for package ${params.package}`, error);
}
try {
output.push(JSON.parse(stdout));
} catch (err) {
output.push(stdout.split("\n"));
}
try {
errOut = JSON.parse(stderr);
} catch (err) {
errOut = stderr.split("\n");
}
var result = null;
try {
result = stdout.slice(stdout.search(/^\{/m), stdout.search(/^\}/m) + 1);
} catch (e) {
result = e;
}
var jResult = null;
try {
jResult = JSON.parse(result);
} catch (e) {
jResult = {"ERROR": e, "RESULT": result};
}
uib.installedPackages = web.checkInstalledPackages(params.package);
switch (params.cmd) {
case "install": {
if (Object.prototype.hasOwnProperty.call(uib.installedPackages, params.package))
success = true;
if (success === true) {
web.servePackage(params.package);
}
break;
}
case "remove": {
if (!Object.prototype.hasOwnProperty.call(uib.installedPackages, params.package))
success = true;
if (success === true) {
web.unservePackage(params.package);
}
break;
}
case "update": {
if (Object.prototype.hasOwnProperty.call(uib.installedPackages, params.package))
success = true;
break;
}
}
if (success === true) {
log.info(`[uibuilder/uibnpmmanage] Admin API. npm command success. npm ${params.cmd} for package ${params.package}`);
} else {
log.error(`[uibuilder/uibnpmmanage] Admin API. npm command failed. npm ${params.cmd} for package ${params.package}`, jResult);
}
res.json({"success": success, "result": jResult, "output": output, "errOut": errOut});
return;
});
});
RED.httpAdmin.use("/uibuilder/techdocs", serveStatic(path.join(__dirname, "..", "docs"), uib.staticOpts));
RED.httpAdmin.get("/uibfiles", function(req, res) {
log.warn("[uibuilder:uibfiles] Admin API. THIS API IS DEPRECATED, DO NOT USE, IT WILL BE REMOVED SOON.");
const params = req.query;
const chkUrl = chkParamUrl(params);
if (chkUrl.status !== 0) {
log.error(`[uibuilder:uibfiles] Admin API. ${chkUrl.statusMessage}`);
res.statusMessage = chkUrl.statusMessage;
res.status(chkUrl.status).end();
return;
}
var folder = params.folder || "src";
if (folder !== "src" && folder !== "dist" && folder !== "root") {
log.error("[uibfiles] Admin API. folder parameter is not one of src|dest|root");
res.statusMessage = "folder parameter must be one of src|dest|root";
res.status(500).end();
return;
}
if (folder === "root")
folder = "";
var cpyIdx = params.cpyIdx === "true" ? true : false;
log.trace(`[uibuilder:uibfiles] Admin API. File get requested. url=${params.url}, folder=${folder}`);
const srcFolder = path.join(uib.rootFolder, req.query.url, folder);
if (folder === "src" && cpyIdx === true) {
const cpyOpts = {"overwrite": false, "preserveTimestamps": true};
const fromTemplateFolder = path.join(uib.masterTemplateFolder, uib.masterTemplate);
try {
fs.copySync(fromTemplateFolder, srcFolder, cpyOpts);
log.trace(`[uibuilder:uibfiles] Copied template files from ${fromTemplateFolder} to ${srcFolder} (not overwriting)`);
} catch (err) {
log.error(`[uibuilder:uibfiles] Error copying template files from ${fromTemplateFolder} to ${srcFolder}`, err);
}
}
fs.readdir(srcFolder, {withFileTypes: true}, (err, files) => {
if (err) {
log.error(`[uibfiles] Admin API. readDir failed for folder '${srcFolder}'.`, err);
res.statusMessage = err;
res.status(500).end();
return;
}
if (uib.nodeVersion[0] < 10) {
res.json(files.filter((fname) => {
try {
let stat = fs.statSync(path.join(srcFolder, fname));
return !stat.isDirectory();
} catch (e) {
log.error(`[uibfiles] Admin API. stat failed for '${fname}' in '${srcFolder}'.`, e);
}
}));
} else {
res.json(files.filter((dirent) => !dirent.isDirectory()).map((dirent) => dirent.name));
}
});
});
RED.httpAdmin.get("/uibnewfile", function(req, res) {
log.warn("[uibuilder:uibuilder] Admin API. THIS API IS DEPRECATED, DO NOT USE, IT WILL BE REMOVED SOON.");
const params = req.query;
const chkUrl = chkParamUrl(params);
if (chkUrl.status !== 0) {
log.error(`[uibuilder:uibnewfile] Admin API. ${chkUrl.statusMessage}`);
res.statusMessage = chkUrl.statusMessage;
res.status(chkUrl.status).end();
return;
}
const chkFname = chkParamFname(params);
if (chkFname.status !== 0) {
log.error(`[uibuilder:uibnewfile] Admin API. ${chkFname.statusMessage}. url=${params.url}`);
res.statusMessage = chkFname.statusMessage;
res.status(chkFname.status).end();
return;
}
const chkFldr = chkParamFldr(params);
if (chkFldr.status !== 0) {
log.error(`[uibuilder:uibnewfile] Admin API. ${chkFldr.statusMessage}. url=${params.url}`);
res.statusMessage = chkFldr.statusMessage;
res.status(chkFldr.status).end();
return;
}
log.trace(`[uibuilder:uibnewfile] Admin API. File create requested. url=${params.url}, file=${params.folder}/${params.fname}`);
const fullname = path.join(uib.rootFolder, params.url, params.folder, params.fname);
try {
fs.ensureFileSync(fullname);
log.trace(`[uibuilder:uibnewfile] Admin API. File create SUCCESS. url=${params.url}, file=${params.folder}/${params.fname}`);
res.statusMessage = "File created successfully";
res.status(200).end();
} catch (err) {
log.error(`[uibuilder:uibnewfile] Admin API. File create FAILED. url=${params.url}, file=${params.folder}/${params.fname}`, err);
res.statusMessage = err;
res.status(500).end();
}
});
RED.httpAdmin.get("/uibdeletefile", function(req, res) {
log.warn("[uibuilder:uibdeletefile] Admin API. THIS API IS DEPRECATED, DO NOT USE, IT WILL BE REMOVED SOON.");
const params = req.query;
const chkUrl = chkParamUrl(params);
if (chkUrl.status !== 0) {
log.error(`[uibuilder:uibdeletefile] Admin API. ${chkUrl.statusMessage}`);
res.statusMessage = chkUrl.statusMessage;
res.status(chkUrl.status).end();
return;
}
const chkFname = chkParamFname(params);
if (chkFname.status !== 0) {
log.error(`[uibuilder:uibdeletefile] Admin API. ${chkFname.statusMessage}. url=${params.url}`);
res.statusMessage = chkFname.statusMessage;
res.status(chkFname.status).end();
return;
}
const chkFldr = chkParamFldr(params);
if (chkFldr.status !== 0) {
log.error(`[uibuilder:uibdeletefile] Admin API. ${chkFldr.statusMessage}. url=${params.url}`);
res.statusMessage = chkFldr.statusMessage;
res.status(chkFldr.status).end();
return;
}
log.trace(`[uibuilder:uibdeletefile] Admin API. File delete requested. url=${params.url}, file=${params.folder}/${params.fname}`);
const fullname = path.join(uib.rootFolder, params.url, params.folder, params.fname);
try {
fs.removeSync(fullname);
log.trace(`[uibuilder:uibdeletefile] Admin API. File delete SUCCESS. url=${params.url}, file=${params.folder}/${params.fname}`);
res.statusMessage = "File deleted successfully";
res.status(200).end();
} catch (err) {
log.error(`[uibuilder:uibdeletefile] Admin API. File delete FAILED. url=${params.url}, file=${params.folder}/${params.fname}`, err);
res.statusMessage = err;
res.status(500).end();
}
});
const jsonBodyParser = require("body-parser").json();
const {checkSchema, validationResult} = require("express-validator");
const loginSchema = {
"id": {
in: ["body"],
errorMessage: "User ID is incorrect length",
isLength: {
errorMessage: "User ID must be between 1 and 50 characters long",
options: {min: 1, max: 50}
},
stripLow: true,