UNPKG

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.

901 lines (777 loc) 39.9 kB
/* eslint-disable max-params */ /* eslint-env node es2017 */ /** * Utility library for uibuilder * * Copyright (c) 2017-2021 Julian Knight (Totally Information) * https://it.knightnet.org.uk * * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ 'use strict' //#region --- Type Defs --- // /** * @typedef {import('../typedefs.js')} * @typedef {import('node-red')} Red */ /* * @typedef {Object} _auth The standard auth object used by uibuilder security. See docs for details. * Note that any other data may be passed from your front-end code in the _auth.info object. * _auth.info.error, _auth.info.validJwt * @property {String} id Required. A unique user identifier. * @property {String} [password] Required for login only. * @property {String} [jwt] Required if logged in. Needed for ongoing session validation and management. * @property {Number} [sessionExpiry] Required if logged in. Milliseconds since 1970. Needed for ongoing session validation and management. * @property {boolean} [userValidated] Required after user validation. Whether the input ID (and optional additional data from the _auth object) validated correctly or not. * @property {Object=} [info] Optional metadata about the user. */ /* * @typedef {object} uibNode Local copy of the node instance config + other info * @property {String} id Unique identifier for this instance * @property {String} type What type of node is this an instance of? (uibuilder) * @property {String} name Descriptive name, only used by Editor * @property {String} topic msg.topic overrides incoming msg.topic * @property {String} url The url path (and folder path) to be used by this instance * @property {boolean} fwdInMessages Forward input msgs to output #1? * @property {boolean} allowScripts Allow scripts to be sent to front-end via msg? WARNING: can be a security issue. * @property {boolean} allowStyles Allow CSS to be sent to the front-end via msg? WARNING: can be a security issue. * @property {boolean} copyIndex Copy index.(html|js|css) files from templates if they don't exist? * @property {boolean} showfolder Provide a folder index web page? * @property {boolean} useSecurity Use uibuilder's built-in security features? * @property {boolean} tokenAutoExtend Extend token life when msg's received from client? * @property {Number} sessionLength Lifespan of token (in seconds) * @property {String} jwtSecret Seed string for encryption of JWT * @property {String} customFolder Name of the fs path used to hold custom files & folders for THIS INSTANCE * @property {Number} ioClientsCount How many Socket clients connected to this instance? * @property {Number} rcvMsgCount How many msg's received since last reset or redeploy? * @property {Object} ioChannels The channel names for Socket.IO * @property {String} ioChannels.control SIO Control channel name 'uiBuilderControl' * @property {String} ioChannels.client SIO Client channel name 'uiBuilderClient' * @property {String} ioChannels.server SIO Server channel name 'uiBuilder' * @property {String} ioNamespace Make sure each node instance uses a separate Socket.IO namespace * @property {Function} send Send a Node-RED msg to an output port * @property {Function=} done Dummy done function for pre-Node-RED 1.0 servers * @property {Function=} on Event handler * @property {Function=} removeListener Event handling * z, wires */ //#endregion --- Type Defs --- // const path = require('path') const fs = require('fs-extra') const tilib = require('./tilib.js') // NOTE: Don't add socket.js here otherwise it will stop working because it references this module // Make sure that we only work out where the security.js file exists only ONCE - see the logon() function let securitySrc = '' let securityjs = null let jsonwebtoken = null /** Gives us a standard _auth object to work with * @type MsgAuth */ const dummyAuth = { id: null, jwt: undefined, sessionExpiry: undefined, userValidated: false, info: { error: undefined, message: undefined, validJwt: undefined, }, } module.exports = { /** Complex, custom code when processing an incoming msg to uibuilder node input should go here * Needs to return the msg object. Not for processing msgs coming back from front-end. */ inputHandler: function(msg, send, done, node, RED, io, ioNs, log, uib) { node.rcvMsgCount++ log.trace(`[uiblib:${node.url}] msg received via FLOW. ${node.rcvMsgCount} messages received`, msg) // If the input msg is a uibuilder control msg, then drop it to prevent loops if ( Object.prototype.hasOwnProperty.call(msg, 'uibuilderCtrl') ) return null //setNodeStatus({fill: 'yellow', shape: 'dot', text: 'Message Received #' + node.rcvMsgCount}, node) // Remove script/style content if admin settings don't allow if ( node.allowScripts !== true ) { if ( Object.prototype.hasOwnProperty.call(msg, 'script') ) delete msg.script } if ( node.allowStyles !== true ) { if ( Object.prototype.hasOwnProperty.call(msg, 'style') ) delete msg.style } // pass the complete msg object to the uibuilder client // TODO: This should have some safety validation on it! if (msg._socketId) { //! TODO If security is active ... // ...If socketId not validated as having a current session, don't send log.trace(`[${node.url}] msg sent on to client ${msg._socketId}. Channel: ${uib.ioChannels.server}`, msg) ioNs.to(msg._socketId).emit(uib.ioChannels.server, msg) } else { //? - is there any way to prevent sending to clients not logged in? log.trace(`[${node.url}] msg sent on to ALL clients. Channel: ${uib.ioChannels.server}`, msg) ioNs.emit(uib.ioChannels.server, msg) } if (node.fwdInMessages) { // Send on the input msg to output send(msg) done() log.trace(`[${node.url}] msg passed downstream to next node`, msg) } return msg }, // ---- End of inputHandler function ---- // /** Do any complex, custom node closure code here * @param {uibNode} node Reference to the node instance object * @param {runtimeRED} RED Reference to the Node-RED API * @param {Object} uib Reference to the uibuilder master config object * @param {Object} sockets - Instance of Socket.IO handler singleton * @param {Object} web - Instance of ExpressJS handler singleton * @param {Object} log - Winston logging instance * @param {function|null} done Default=null, internal node-red function to indicate processing is complete */ instanceClose: function(node, RED, uib, sockets, web, log, done = null) { log.trace(`[${node.url}] nodeGo:on-close:processClose`) /** @type {Object} instances[] Reference to the currently defined instances of uibuilder */ const instances = uib.instances this.setNodeStatus({fill: 'red', shape: 'ring', text: 'CLOSED'}, node) // Let all the clients know we are closing down sockets.sendControl({ 'uibuilderCtrl': 'shutdown' }, node, undefined, false) // Disconnect all Socket.IO clients for this node instance sockets.removeNS(node) web.removeInstanceMiddleware(node) // Remove url folder if requested if ( uib.deleteOnDelete[node.url] === true ) { log.trace(`[uibuilder:uiblib:processClose] Deleting instance folder. URL: ${node.url}`) // Remove the flag in case someone recreates the same url! delete uib.deleteOnDelete[node.url] fs.remove(path.join(uib.rootFolder, node.url)) .catch(err => { log.error(`[uibuilder:uiblib:processClose] Deleting instance folder failed. URL=${node.url}, Error: ${err.message}`) }) } // Keep a log of the active instances @since 2019-02-02 delete instances[node.id] // = undefined /* // This code borrowed from the http nodes // THIS DOESN'T ACTUALLY WORK!!! Static routes don't set route.route app._router.stack.forEach(function(route,i,routes) { if ( route.route && route.route.path === node.url ) { routes.splice(i,1) } }); */ // This should be executed last if present. `done` is the data returned from the 'close' // event and is used to resolve async callbacks to allow Node-RED to close if (done) done() }, // ---- End of processClose function ---- // /** Get property values from an Object. * Can list multiple properties, the first found (or the default return) will be returned * Makes use of RED.util.getMessageProperty * @param {Object} RED - RED * @param {Object} myObj - the parent object to search for the props * @param {string|string[]} props - one or a list of property names to retrieve. * Can be nested, e.g. 'prop1.prop1a' * Stops searching when the first property is found * @param {any} defaultAnswer - if the prop can't be found, this is returned * @return {any} The first found property value or the default answer */ getProps: function(RED,myObj,props,defaultAnswer = []) { if ( (typeof props) === 'string' ) { // @ts-ignore props = [props] } if ( ! Array.isArray(props) ) { return undefined } let ans for (var i = 0; i < props.length; i++) { try { // errors if an intermediate property doesn't exist ans = RED.util.getMessageProperty(myObj, props[i]) if ( typeof ans !== 'undefined' ) { break } } catch(e) { // do nothing } } return ans || defaultAnswer }, // ---- End of getProps ---- // /** Output a control msg * Sends to all connected clients & outputs a msg to port 2 if required * @param {Object} msg The message to output * @param {Object} ioNs Socket.IO instance to use * @param {Object} node The node object * @param {Object} uib Reference to the uibuilder configuration object * @param {string=} socketId Optional. If included, only send to specific client id * @param {boolean=} output Optional. If included, also output to port #2 of the node @since 2020-01-03 */ sendControl: function(msg, ioNs, node, uib, socketId, output) { if (output === undefined || output === null) output = true msg.from = 'server' if (socketId) msg._socketId = socketId // Send to specific client if required if (msg._socketId) ioNs.to(msg._socketId).emit(uib.ioChannels.control, msg) else ioNs.emit(uib.ioChannels.control, msg) if ( (! Object.prototype.hasOwnProperty.call(msg, 'topic')) && (node.topic !== '') ) msg.topic = node.topic // copy msg to output port #2 if required if ( output === true ) node.send([null, msg]) }, // ---- End of getProps ---- // /** Simple fn to set a node status in the admin interface * fill: red, green, yellow, blue or grey * @param {Object|string} status * @param {Object} node */ setNodeStatus: function( status, node ) { if ( typeof status !== 'object' ) status = {fill: 'grey', shape: 'ring', text: status} node.status(status) }, // ---- End of setNodeStatus ---- // /** Validate a url query parameter - DEPRECATED in v3.1.0 * @deprecated * @param {string} url uibuilder URL to check (not a full url, the name used by uibuilder) * @param {import("express").Response} res The ExpressJS response variable * @param {string} caller A string indicating the calling function - used for logging only * @param {Object} log The uibuilder log Object * @return {boolean} True if the url is valid, false otherwise (having set the response object) */ checkUrl: function (url, res, caller, log) { log.warn(`[uibuilder:checkUrl] FUNCTION DEPRECATED - DO NOT USE. url=${url}, caller=${caller}`) // We have to have a url to work with if ( url === undefined ) { log.error(`[uiblib.checkUrl:${caller}] Admin API. url parameter not provided`) res.statusMessage = 'url parameter not provided' res.status(500).end() return false } // URL must not exceed 20 characters if ( url.length > 20 ) { log.error(`[uiblib.checkUrl:${caller}] Admin API. url parameter is too long (>20 characters)`) res.statusMessage = 'url parameter is too long. Max 20 characters' res.status(500).end() return false } // URL must be more than 0 characters if ( url.length < 1 ) { log.error(`[uiblib.checkUrl:${caller}] Admin API. url parameter is empty`) res.statusMessage = 'url parameter is empty, please provide a value' res.status(500).end() return false } // URL cannot contain .. to prevent escaping sub-folder structure if ( url.includes('..') ) { log.error('[uibdeletefile] Admin API. url parameter contains ..') res.statusMessage = 'url parameter may not contain ..' res.status(500).end() return false } return true }, // ---- End of checkUrl ---- // /** Check authorisation validity - called for every msg received from client if security is on * @param {Object} msg The input message from the client * @param {SocketIO.Namespace} ioNs Socket.IO instance to use * @param {uibNode} node The node object * @param {SocketIO.Socket} socket * @param {Object} log Custom logger instance * @param {Object} uib Reference to the core uibuilder config object * @returns {_auth} An updated _auth object */ authCheck: function(msg, ioNs, node, socket, log, uib) { // eslint-disable-line no-unused-vars /** @type MsgAuth */ var _auth = dummyAuth // Has the client included msg._auth? If not, send back an unauth msg // TODO: Only send if msg was on std channel NOT on control channel if (!msg._auth) { _auth.info.error = 'Client did not provide an _auth' this.sendControl({ 'uibuilderCtrl': 'Auth Failure', 'topic': node.topic || undefined, /** @type _auth */ '_auth': _auth, }, ioNs, node, uib, socket.id, false) return _auth } // Has the client included msg._auth.id? If not, send back an unauth msg // TODO: Only send if msg was on std channel NOT on control channel if (!msg._auth.id) { _auth.info.error = 'Client did not provide an _auth.id' this.sendControl({ 'uibuilderCtrl': 'Auth Failure', 'topic': node.topic || undefined, /** @type _auth */ '_auth': _auth, }, ioNs, node, uib, socket.id, false) return _auth } // TODO: remove log output console.log('[uibuilder:socket.on.control] Use Security _auth: ', msg._auth, `. Node ID: ${node.id}`) // does the client have a valid session? // if not, return a not logged in control msg _auth = this.checkToken(msg._auth, node) //console.log('[uibuilder:socket.on.control] result of checkToken _auth: ', _auth) // if (_auth.info.validJwt === true) { // uiblib.sendControl({ // 'uibuilderCtrl': 'session valid', // 'topic': node.topic || undefined, // '_auth': _auth // }, ioNs, node, socket.id, false, uib) // } else { // uiblib.sendControl({ // 'uibuilderCtrl': 'session invalid', // 'topic': node.topic || undefined, // '_auth': _auth // }, ioNs, node, socket.id, false, uib) // } return _auth }, // ---- End of authCheck ---- // /** Create a new JWT token based on a user id, session length and security string * @param {MsgAuth} _auth The unique id that identifies the user. * @param {uibNode} node Reference to the calling uibuilder node instance. * @returns {MsgAuth} Updated _auth including a signed JWT token string, expiry date/time & info flag. */ createToken: function(_auth, node) { // If anything fails, ensure that the token is invalidated try { if (jsonwebtoken === null) jsonwebtoken = require('jsonwebtoken') const sessionExpiry = Math.floor(Number(Date.now()) / 1000) + Number(node.sessionLength) const jwtData = { // When does the token expire? Value is seconds since 1970 exp: sessionExpiry, // Subject = unique id to identify user sub: _auth.id, // Issuer iss: 'uibuilder', } _auth.jwt = jsonwebtoken.sign(jwtData, node.jwtSecret) _auth.sessionExpiry = sessionExpiry * 1000 // Javascript ms not unix sec if (!_auth.info) _auth.info = {} _auth.info.validJwt = true } catch(e) { _auth.jwt = undefined _auth.sessionExpiry = undefined _auth.userValidated = false if (!_auth.info) _auth.info = {} _auth.info.validJwt = false _auth.info.error = 'Could not create JWT' } return _auth }, // ---- End of createToken ---- // /** Check whether a received JWT token is valid. If it is, then try to update it. * @param {_auth} token A base64 encoded, signed JWT token string. * @param {uibNode} node Reference to the calling uibuilder node instance. * @returns {_auth} { valid: [boolean], data: [object], newToken: [string], err: [object] } */ checkToken: function(_auth, node) { if (jsonwebtoken === null) jsonwebtoken = require('jsonwebtoken') const options = { issuer: 'uibuilder', clockTimestamp: Math.floor(Date.now() / 1000), // seconds since 1970 //clockTolerance: 10, // seconds //maxAge: "7d", } /** @type _auth */ var response = { id: _auth.id, jwt: undefined, info: { validJwt: false, error: undefined, }, } try { response.info.verify = jsonwebtoken.verify(_auth.jwt, node.jwtSecret, options) // , callback]) response = this.createToken(response, node) //response.info.validJwt = true // set in createToken, also the jwt & expiry } catch(err) { response.info.error = err response.info.validJwt = false } console.log('[uibuilder:uiblib.js:checkToken] response: ', response) return response }, // ---- End of checkToken ---- // /** Process a logon request * msg._auth contains any extra data needed for the login * @param {Object} msg The input message from the client * @param {SocketIO.Namespace} ioNs Socket.IO instance to use * @param {uibNode} node The node object * @param {SocketIO.Socket} socket * @param {Object} log Custom logger instance * @param {Object} uib Constants from uibuilder.js * @returns {boolean} True = user logged in, false = user not logged in */ logon: function(msg, ioNs, node, socket, log, uib) { /** @type MsgAuth */ var _auth = msg._auth || dummyAuth if (!_auth.info) _auth.info = {} _auth.userValidated = false // Only process if security is turned on. Otherwise output info to log, inform client and exit if ( node.useSecurity !== true ) { log.info('[uibuilder:uiblib:logon] Security is not turned on, ignoring logon attempt.') _auth.info.error = 'Security is not turned on for this uibuilder instance' this.sendControl({ uibuilderCtrl: 'authorisation failure', topic: msg.topic || node.topic, '_auth': _auth, }, ioNs, node, uib, socket.id, false) return _auth.userValidated } // Check if using TLS - if not, send warning to log & inform client and exit if ( socket.handshake.secure !== true ) { if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev') { _auth.info.warning = ` +---------------------------------------------------------------+ | uibuilder security warning: | | A logon is being processed without TLS security turned on. | | This works, with warnings, in a development environment. | | It will NOT work for non-development environments. | | See the uibuilder security docs for details. | +---------------------------------------------------------------+ ` log.warn(`[uibuilder:uiblib:logon] **WARNING** ${_auth.info.warning}`) } else { _auth.info.error = ` +---------------------------------------------------------------+ | uibuilder security warning: | | A logon is being processed without TLS security turned on. | | This IS NOT PERMITTED for non-development environments. | | See the uibuilder security docs for details. | +---------------------------------------------------------------+ ` log.error(`[uibuilder:uiblib:logon] **ERROR** ${_auth.info.error}`) // Report fail to client but don't output to port #2 as error msg already sent _auth.userValidated = false _auth.info.error = 'Logons cannot be processed without TLS in non-development environments' this.sendControl({ uibuilderCtrl: 'authorisation failure', topic: msg.topic || node.topic, '_auth': _auth, }, ioNs, node, uib, socket.id, false) return _auth.userValidated } } // Make sure that we at least have a user id, if not, inform client and exit if ( ! _auth.id ) { log.warn('[uibuilder:uiblib.js:logon] No _auth.id provided') //TODO ?? record fail ?? _auth.userValidated = false _auth.info.error = 'Logon failed. No id provided' // Report fail to client & Send output to port #2 this.sendControl({ uibuilderCtrl: 'authorisation failure', topic: msg.topic || node.topic, '_auth': _auth, }, ioNs, node, uib, socket.id, true) return _auth.userValidated } /** Attempt logon */ // If an instance specific version of the security module exists, use it or use master copy or fail // On fail, output to NR log & exit the logon - don't tell the client as that would be leaking security info if ( securitySrc === '' ) { // make sure this only runs once securitySrc = path.join(node.customFolder,'security.js') if ( ! fs.existsSync(securitySrc) ) { // Otherwise try to use the central version in uibRoot/.config securitySrc = path.join(uib.rootFolder, uib.configFolderName,'security.js') if ( ! fs.existsSync(securitySrc) ) { // Otherwise use the template version from ./templates/.config securitySrc = path.join(__dirname, 'templates', '.config', 'security.js') // And output a warning if in dev mode, fail in production mode if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev') { log.warn('[uibuilder:uiblib:logon] Security is ON but no `security.js` found. Using master template. Please replace with your own code.') if (node.copyIndex === true) { // eslint-disable-line max-depth log.warn('[uibuilder:uiblib:logon] copyIndex flag is ON so copying master template `security.js` to the <usbRoot>/.config` folder.') fs.copy(securitySrc, path.join(uib.configFolderName,'security.js'), {overwrite:false, preserveTimestamps:true}, err => { if (err) log.error('[uibuilder:uiblib:logon] Copy of master template `security.js` FAILED.', err) else { log.warn('[uibuilder:uiblib:logon] Copy of master template `security.js` SUCCEEDED. Please restart Node-RED to use it.') } }) } } else { // In production mode, don't allow insecure processes - fail now log.error('[uibuilder:uiblib:logon] Security is ON but no `security.js` found. Cannot process logon in non-development mode without a custom security.js file. See uibuilder security docs for details.') return _auth.userValidated } } } try { securityjs = require( securitySrc ) } catch (e) { log.error('[uibuilder:uiblib:logon] Security is ON but `security.js` could not be `required`. Cannot process logons. Is security.js a valid Node.js module?', e) return _auth.userValidated } } // Make sure that securityjs has the correct functions available or log and exit if ( ! securityjs.userValidate ) { log.error('[uibuilder:uiblib:logon] Security is ON but `security.js` does not contain the required function(s). Cannot process logon. Check docs and change file.') return _auth.userValidated } // Make sure that _auth.info exists if ( ! Object.prototype.hasOwnProperty.call(_auth, 'info') ) _auth.info = {} // Use security module to validate user - updates _auth _auth = securityjs.userValidate(_auth) // Ensure that _auth.password is not present delete _auth.password // Validate the _auth object - full ensure the following props exist: id, userValidated, info. And ensures that password DOES NOT EXIST if ( ! this.chkAuth(_auth, 'full') ) { log.error(`[uibuilder:uiblib:logon] _auth is not valid, logon cancelled. Please check 'userValidate()' in '${uib.configFolder}/security.js'.\n\t\tIt MUST return an object with at least id, userValidated, info props. info must be an object.`) console.log('[uibuilder:uiblib:logon] _auth=',_auth) // NB: leave this console log in place for error reporting return false } console.log('[uibuilder:uiblib.js:logon] Updated _auth: ', _auth) // Send responses //TODO Should output to port #2 be an option? Should less data be sent? if ( _auth.userValidated === true ) { // Record session details // Add token to _auth - created here not in user function to ensure consistency _auth = this.createToken(_auth, node) // Check that we have a valid token if ( _auth.info.jwtValid === true ) { // Add success reason and add any optional data from the user validation _auth.info.message = 'Logon successful' } else { _auth.userValidated = false } // Report success & send token to client & to port #2 this.sendControl({ 'uibuilderCtrl': 'authorised', 'topic': msg.topic || node.topic, '_auth': _auth, }, ioNs, node, uib, socket.id, true) // Send output to port #2 manually (because we only include a subset of _auth) /* node.send([null, { uibuilderCtrl: 'authorised', topic: msg.topic || node.topic, _socketId: socket.id, from: 'server', '_auth': { // Try to show some usefull info without revealing too much id: _auth.id, authTokenExpiry: _auth.authTokenExpiry, // Optional data from the client uid: _auth.uid, user: _auth.user, name: _auth.name, }, }]) */ } else { // _auth.userValidated <> true _auth.info.error = 'Logon failed. Invalid id or password' // NB _auth.info is created further up if it doesn't already exist, it is validated as an object // Report fail to client & Send output to port #2 this.sendControl({ uibuilderCtrl: 'authorisation failure', topic: msg.topic || node.topic, '_auth': _auth, }, ioNs, node, uib, socket.id, true) } return _auth.userValidated }, // ---- End of logon ---- // /** Process a logoff request * msg._auth contains any extra data needed for the login * @param {Object} msg The input message from the client * @param {SocketIO.Namespace} ioNs Socket.IO instance to use * @param {uibNode} node The node object * @param {SocketIO.Socket} socket * @param {Object} log Custom logger instance * @returns {_auth} Updated _auth */ logoff: function(msg, ioNs, node, socket, log, uib) { // eslint-disable-line no-unused-vars /** @type MsgAuth */ var _auth = msg._auth || dummyAuth // Check that request is valid (has valid token) // Check that session exists // delete session entry _auth.jwt = undefined _auth.sessionExpiry = undefined _auth.userValidated = false if (!_auth.info) _auth.info = {} _auth.info.validJwt = false _auth.info.message = 'Logoff successful' // confirm logoff to client & Send output to port #2 this.sendControl({ uibuilderCtrl: 'logged off', topic: msg.topic || node.topic, '_auth': _auth, }, ioNs, node, uib, socket.id, true) return _auth }, // ---- End of logoff ---- // /** Check an _auth object for the correct schema * @param {MsgAuth} _auth The _auth object to check * @param {String=} type Optional. 'short' or 'full'. How much checking to do * @returns {Boolean} */ chkAuth: function(_auth, type='short') { // Has to be an object if ( ! (_auth!== null && _auth.constructor.name === 'Object') ) { return false } let chk = false let chk1, chk2, chk3 // --- REQUIRED --- // // ID? (user id) try { if ( _auth.id !== '' ) chk = true } catch (e) { chk = false } // --- FULL CHECK --- // if ( type === 'full' ) { // userValidated if ( _auth.userValidated === true || _auth.userValidated === false ) chk1 = true else chk1 = false // info - exists and is an object if ( _auth.info && _auth.info !== null && _auth.info.constructor.name === 'Object' ) chk2 = true else chk2 = false // MUST NOT EXIST password if ( ! _auth.password ) chk3 = true else chk3 = false } if ( chk && chk1 && chk2 && chk3 ) return true return false }, // ---- End of chkAuth() ---- // /** Create instance details web page * @param {import("express").Request} req ExpressJS Request object * @param {Object} node configuration data for this instance * @param {Object} uib uibuilder "globals" common to all instances * @param {string} userDir The Node-RED userDir folder * @param {runtimeRED} RED The Node-RED runtime object * @return {string} page html */ showInstanceDetails: function(req, node, uib, userDir, RED) { let page = '' // If using own Express server, correct the URL's const url = new URL(req.headers.referer) url.pathname = '' if (uib.port && uib.port !== RED.settings.uiPort) { url.port = uib.port } const urlPrefix = url.href page += ` <!doctype html><html lang="en"><head> <title>uibuilder Instance Debug Page</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 Instance Debug Page</h1> <p> Note that this page is only accessible to users with Node-RED admin authority. </p> ` page += ` <h2>Instance Information for '${node.url}'</h2> <table class="table"> <tbody> <tr> <th>The node id for this instance</th> <td>${node.id}<br> This can be used to search for the node in the Editor. </td> </tr> <tr> <th>Filing system path to front-end resources</th> <td>${node.customFolder}<br> Contains all of your UI code and other resources. Folders and files can be viewed, edited, created and deleted using the "Edit Files" button. You <b>MUST</b> keep at least the <code>src</code> and <code>dist</code> folders otherwise things may not work. </td> </tr> <tr> <th>URL for the front-end resources</th> <td><a href="${urlPrefix}${tilib.urlJoin(uib.nodeRoot, node.url).replace('/','')}" target="_blank">.${tilib.urlJoin(uib.nodeRoot, node.url)}/</a><br>Index.html page will be shown if you click.</td> </tr> <tr> <th>Node-RED userDir folder</th> <td>${userDir}<br> Also the location for any installed vendor resources (installed library packages) and your other nodes. </td> </tr> <tr> <th>URL for vendor resources</th> <td>../uibuilder/vendor/<br> See the <a href="../../uibindex" target="_blank">Detailed Information Page</a> for more details. </td> </tr> <tr> <th>Filing system path to common (shared) front-end resources</th> <td>${uib.commonFolder}<br> Resource files in this folder are accessible from the main URL. </td> </tr> <tr> <th>Filing system path to common uibuilder configuration resource files</th> <td>${uib.configFolder}<br> Contains the package list, master package list, authentication and authorisation middleware. </td> </tr> <tr> <th>Filing system path to uibuilder master template files</th> <td>${uib.masterTemplateFolder}<br> These are copied to any new instance of the uibuilder node. If you keep the copy flag turned on they are re-copied if deleted. </td> </tr> <tr> <th>uibuilder version</th> <td>${uib.version}</td> </tr> <tr> <th>Node-RED version</th> <td>${RED.settings.version}<br> Minimum version required by uibuilder is ${uib.me['node-red'].version} </td> </tr> <tr> <th>Node.js version</th> <td>${uib.nodeVersion.join('.')}<br> Minimum version required by uibuilder is ${uib.me.engines.node} </td> </tr> </tbody> </table> ` const nodeKeys = [ 'id', 'type', 'name', 'wires', '_wireCount', 'credentials', 'topic', 'url', 'fwdInMessages', 'allowScripts', 'allowStyles', 'copyIndex', 'showfolder', 'useSecurity', 'sessionLength', 'tokenAutoExtend', 'customFolder', 'ioClientsCount', 'rcvMsgCount', 'ioChannels', 'ioNamespace' ] // functions: ['_closeCallbacks', '_inputCallback', '_inputCallbacks', 'send', ] // Keep secret: ['jwtSecret', ] page += ` <h2>Node Instance Configuration Items</h2> <p> Shows the internal configuration. </p> <table class="table"> <tbody> ` nodeKeys.sort().forEach( item => { let info = node[item] try { if ( info !== null && info.constructor.name === 'Object' ) info = JSON.stringify(info) } catch (e) { RED.log.warn(`[uibuilder:uiblib:showInstanceDetails] ${node.id}, ${url}: Item '${item}' failed to stringify. ${e.message}`) } page += ` <tr> <th>${item}</th> <td>${info}</td> </tr> ` }) page += ` </tbody> </table> ` page += '' // eslint-disable-line no-implicit-coercion page += '<div></div>' page += '</body></html>' return page }, // ---- End of showInstanceDetails() ---- // } // ---- End of module.exports ---- //