diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4b6f1ab..d77cb1f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [14, 16, 18, 20, 22] + node-version: [18, 20, 22] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} @@ -28,7 +28,7 @@ jobs: npm run test npm run coverage - name: Publish to coveralls.io - if: ${{ matrix.node-version == 16 }} + if: ${{ matrix.node-version == 20 }} uses: coverallsapp/github-action@v2 with: github-token: ${{ github.token }} diff --git a/CHANGELOG.md b/CHANGELOG.md index cde08d4..2b9de1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +### 4.0.0 + + - Update template settings for 4.0 + - Update dependencies + - Replace bcrypt with @node-rs/bcrypt + - Set minimum node.js version to 18 + ### 3.1.3 - Bump axios to 1.6.8 @knolleary diff --git a/lib/commands/hash.js b/lib/commands/hash.js index 02273c3..5985ab3 100644 --- a/lib/commands/hash.js +++ b/lib/commands/hash.js @@ -15,21 +15,17 @@ **/ var prompt = require("../prompt"); -try { bcrypt = require('bcrypt'); } +try { bcrypt = require('@node-rs/bcrypt'); } catch(e) { bcrypt = require('bcryptjs'); } -function command(argv,result) { +async function command(argv,result) { if (argv.json) { console.warn("hash-pw command does not support json format output"); } - return new Promise(resolve => { - prompt.read({prompt:"Password:",silent: true},function(err, password) { - if (password) { - result.log(bcrypt.hashSync(password, 8)); - } - resolve(); - }); - }); + const password = await prompt.read({prompt:"Password:",silent: true}) + if (password) { + result.log(bcrypt.hashSync(password, 8)); + } } command.alias = "hash-pw"; command.usage = command.alias; diff --git a/lib/commands/init/index.js b/lib/commands/init/index.js index 368232e..ca3a7e0 100644 --- a/lib/commands/init/index.js +++ b/lib/commands/init/index.js @@ -22,7 +22,7 @@ const fs = require("fs"); const path = require("path"); let bcrypt; -try { bcrypt = require('bcrypt'); } +try { bcrypt = require('@node-rs/bcrypt'); } catch(e) { bcrypt = require('bcryptjs'); } function prompt(opts) { diff --git a/lib/commands/init/resources/settings.js.mustache b/lib/commands/init/resources/settings.js.mustache index 8b46e15..8b66161 100644 --- a/lib/commands/init/resources/settings.js.mustache +++ b/lib/commands/init/resources/settings.js.mustache @@ -136,11 +136,13 @@ module.exports = { * - httpServerOptions * - httpAdminRoot * - httpAdminMiddleware + * - httpAdminCookieOptions * - httpNodeRoot * - httpNodeCors * - httpNodeMiddleware * - httpStatic - * - httpStaticRoot + * - httpStaticRoot + * - httpStaticCors ******************************************************************************/ /** the tcp port that the Node-RED web server is listening on */ @@ -181,10 +183,15 @@ module.exports = { // next(); // }, + /** The following property can be used to set addition options on the session + * cookie used as part of adminAuth authentication system + * Available options are documented here: https://www.npmjs.com/package/express-session#cookie + */ + // httpAdminCookieOptions: { }, /** Some nodes, such as HTTP In, can be used to listen for incoming http requests. * By default, these are served relative to '/'. The following property - * can be used to specifiy a different root path. If set to false, this is + * can be used to specify a different root path. If set to false, this is * disabled. */ //httpNodeRoot: '/red-nodes', @@ -226,10 +233,18 @@ module.exports = { * to move httpAdminRoot */ //httpStatic: '/home/nol/node-red-static/', //single static source - /* OR multiple static sources can be created using an array of objects... */ + /** + * OR multiple static sources can be created using an array of objects... + * Each object can also contain an options object for further configuration. + * See https://expressjs.com/en/api.html#express.static for available options. + * They can also contain an option `cors` object to set specific Cross-Origin + * Resource Sharing rules for the source. `httpStaticCors` can be used to + * set a default cors policy across all static routes. + */ //httpStatic: [ // {path: '/home/nol/pics/', root: "/img/"}, // {path: '/home/nol/reports/', root: "/doc/"}, + // {path: '/home/nol/videos/', root: "/vid/", options: {maxAge: '1d'}} //], /** @@ -242,9 +257,26 @@ module.exports = { */ //httpStaticRoot: '/static/', + /** The following property can be used to configure cross-origin resource sharing + * in the http static routes. + * See https://github.com/troygoode/node-cors#configuration-options for + * details on its contents. The following is a basic permissive set of options: + */ + //httpStaticCors: { + // origin: "*", + // methods: "GET,PUT,POST,DELETE" + //}, + + /** The following property can be used to modify proxy options */ + // proxyOptions: { + // mode: "legacy", // legacy mode is for non-strict previous proxy determination logic (node-red < v4 compatible) + // }, + /******************************************************************************* * Runtime Settings * - lang + * - runtimeState + * - diagnostics * - logging * - contextStorage * - exportGlobalContextKeys @@ -257,6 +289,31 @@ module.exports = { */ // lang: "de", + /** Configure diagnostics options + * - enabled: When `enabled` is `true` (or unset), diagnostics data will + * be available at http://localhost:1880/diagnostics + * - ui: When `ui` is `true` (or unset), the action `show-system-info` will + * be available to logged in users of node-red editor + */ + diagnostics: { + /** enable or disable diagnostics endpoint. Must be set to `false` to disable */ + enabled: true, + /** enable or disable diagnostics display in the node-red editor. Must be set to `false` to disable */ + ui: true, + }, + /** Configure runtimeState options + * - enabled: When `enabled` is `true` flows runtime can be Started/Stopped + * by POSTing to available at http://localhost:1880/flows/state + * - ui: When `ui` is `true`, the action `core:start-flows` and + * `core:stop-flows` will be available to logged in users of node-red editor + * Also, the deploy menu (when set to default) will show a stop or start button + */ + runtimeState: { + /** enable or disable flows/state endpoint. Must be set to `false` to disable */ + enabled: false, + /** show or hide runtime stop/start options in the node-red editor. Must be set to `false` to hide */ + ui: false, + }, /** Configure the logging output */ logging: { /** Only console logging is currently supported */ @@ -308,19 +365,22 @@ module.exports = { * will install/load. It can use '*' as a wildcard that matches anything. */ externalModules: { - // autoInstall: false, /** Whether the runtime will attempt to automatically install missing modules */ - // autoInstallRetry: 30, /** Interval, in seconds, between reinstall attempts */ - // palette: { /** Configuration for the Palette Manager */ - // allowInstall: true, /** Enable the Palette Manager in the editor */ - // allowUpload: true, /** Allow module tgz files to be uploaded and installed */ - // allowList: [], - // denyList: [] - // }, - // modules: { /** Configuration for node-specified modules */ - // allowInstall: true, - // allowList: [], - // denyList: [] - // } + // autoInstall: false, /** Whether the runtime will attempt to automatically install missing modules */ + // autoInstallRetry: 30, /** Interval, in seconds, between reinstall attempts */ + // palette: { /** Configuration for the Palette Manager */ + // allowInstall: true, /** Enable the Palette Manager in the editor */ + // allowUpdate: true, /** Allow modules to be updated in the Palette Manager */ + // allowUpload: true, /** Allow module tgz files to be uploaded and installed */ + // allowList: ['*'], + // denyList: [], + // allowUpdateList: ['*'], + // denyUpdateList: [] + // }, + // modules: { /** Configuration for node-specified modules */ + // allowInstall: true, + // allowList: [], + // denyList: [] + // } }, @@ -346,6 +406,12 @@ module.exports = { * a collection of themes to chose from. */ {{^editorTheme}}//{{/editorTheme}}theme: "{{editorTheme}}", + + /** To disable the 'Welcome to Node-RED' tour that is displayed the first + * time you access the editor for each release of Node-RED, set this to false + */ + //tours: false, + palette: { /** The following property can be used to order the categories in the editor * palette. If a node's category is not in the list, the category will get @@ -367,6 +433,7 @@ module.exports = { mode: "{{projects.workflow}}" } }, + codeEditor: { /** Select the text editor component used by the editor. * As of Node-RED V3, this defaults to "monaco", but can be set to "ace" if desired @@ -387,7 +454,19 @@ module.exports = { //fontFamily: "Cascadia Code, Fira Code, Consolas, 'Courier New', monospace", //fontLigatures: true, } - } + }, + markdownEditor: { + mermaid: { + /** enable or disable mermaid diagram in markdown document + */ + enabled: true + } + }, + + multiplayer: { + /** To enable the Multiplayer feature, set this value to true */ + enabled: false + }, }, /******************************************************************************* @@ -400,6 +479,7 @@ module.exports = { * - ui (for use with Node-RED Dashboard) * - debugUseColors * - debugMaxLength + * - debugStatusLength * - execMaxBufferSize * - httpRequestTimeout * - mqttReconnectTime @@ -455,6 +535,9 @@ module.exports = { /** The maximum length, in characters, of any message sent to the debug sidebar tab */ debugMaxLength: 1000, + /** The maximum length, in characters, of status messages under the debug node */ + //debugStatusLength: 32, + /** Maximum buffer size for the exec node. Defaults to 10Mb */ //execMaxBufferSize: 10000000, @@ -488,7 +571,7 @@ module.exports = { */ //tlsConfigDisableLocalFiles: true, - /** The following property can be used to verify websocket connection attempts. + /** The following property can be used to verify WebSocket connection attempts. * This allows, for example, the HTTP request headers to be checked to ensure * they include valid authentication information. */ diff --git a/lib/commands/login.js b/lib/commands/login.js index d9aca19..a95b86e 100644 --- a/lib/commands/login.js +++ b/lib/commands/login.js @@ -18,40 +18,31 @@ var request = require("../request"); var config = require("../config"); var prompt = require("../prompt"); -function command(argv,result) { +async function command(argv,result) { config.tokens(null); - return request.request('/auth/login',{}).then(function(resp) { - return new Promise((resolve,reject) => { - if (resp.type) { - if (resp.type == "credentials") { - prompt.read({prompt:"Username:"},function(err, username) { - prompt.read({prompt:"Password:",silent: true},function(err, password) { - request.request('/auth/token', { - method: "POST", - data: { - client_id: 'node-red-admin', - grant_type: 'password', - scope: '*', - username: username, - password: password - } - }).then(function(resp) { - config.tokens(resp); - result.log("Logged in"); - resolve(); - }).catch(function(resp) { - reject("Login failed"); - }); - }); - }); - } else { - reject("Unsupported login type"); + const resp = await request.request('/auth/login',{}) + if (resp.type) { + if (resp.type == "credentials") { + const username = await prompt.read({prompt:"Username:"}) + const password = await prompt.read({prompt:"Password:",silent: true}) + const loginResp = await request.request('/auth/token', { + method: "POST", + data: { + client_id: 'node-red-admin', + grant_type: 'password', + scope: '*', + username: username, + password: password } - } else { - resolve(); - } - }); - }); + }).catch(resp => { + throw new Error("Login failed"); + }) + config.tokens(loginResp); + result.log("Logged in"); + } else { + throw new Error("Unsupported login type"); + } + } } command.alias = "login"; diff --git a/lib/prompt.js b/lib/prompt.js index c6cb65b..38b50bb 100644 --- a/lib/prompt.js +++ b/lib/prompt.js @@ -14,7 +14,7 @@ * limitations under the License. **/ -var read = require("read"); +var { read } = require("read"); // This exists purely to provide a place to hook in a unit test mock of the // read module diff --git a/package.json b/package.json index b207590..da921cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red-admin", - "version": "3.1.3", + "version": "4.0.0", "description": "The Node-RED admin command line interface", "homepage": "https://nodered.org", "bugs": { @@ -13,7 +13,7 @@ }, "main": "lib/index.js", "engines": { - "node": ">=14" + "node": ">=18" }, "contributors": [ { @@ -29,23 +29,23 @@ }, "dependencies": { "ansi-colors": "^4.1.3", - "axios": "^1.6.8", + "axios": "^1.7.2", "bcryptjs": "^2.4.3", "cli-table": "^0.3.11", "enquirer": "^2.3.6", "minimist": "^1.2.8", "mustache": "^4.2.0", - "read": "^1.0.7" + "read": "^3.0.1" }, "devDependencies": { - "mocha": "^10.2.0", + "mocha": "^10.4.0", "nyc": "^15.1.0", "should": "^13.2.3", "sinon": "^15.2.0", - "sinon-test": "^3.1.5" + "sinon-test": "^3.1.6" }, "optionalDependencies": { - "bcrypt": "5.1.1" + "@node-rs/bcrypt": "1.10.4" }, "bin": { "node-red-admin": "node-red-admin.js" diff --git a/test/lib/commands/hash_spec.js b/test/lib/commands/hash_spec.js index d5a0d88..8d2783b 100644 --- a/test/lib/commands/hash_spec.js +++ b/test/lib/commands/hash_spec.js @@ -34,9 +34,7 @@ describe("commands/hash-pw", function() { prompt.read.restore(); }); it('generates a bcrypt hash of provided password',function(done) { - sinon.stub(prompt,"read").callsFake(function(opts,callback) { - callback(null,"a-test-password"); - }); + sinon.stub(prompt,"read").resolves("a-test-password"); command({},result).then(function() { result.log.calledOnce.should.be.true(); @@ -48,9 +46,7 @@ describe("commands/hash-pw", function() { }); }); it('ignores blank password',function(done) { - sinon.stub(prompt,"read").callsFake(function(opts,callback) { - callback(null,""); - }); + sinon.stub(prompt,"read").resolves("") command({},result).then(function() { result.log.called.should.be.false(); @@ -58,9 +54,7 @@ describe("commands/hash-pw", function() { }); }); it('ignores null password',function(done) { - sinon.stub(prompt,"read").callsFake(function(opts,callback) { - callback(null,null); - }); + sinon.stub(prompt,"read").resolves(null) command({},result).then(function() { result.log.called.should.be.false(); diff --git a/test/lib/commands/login_spec.js b/test/lib/commands/login_spec.js index dac0379..1140bbc 100644 --- a/test/lib/commands/login_spec.js +++ b/test/lib/commands/login_spec.js @@ -25,14 +25,14 @@ var request = require("../../../lib/request"); var config = require("../../../lib/config"); var result = require("./result_helper"); -describe("commands/list", function() { +describe("commands/login", function() { beforeEach(function() { sinon.stub(config,"tokens").callsFake(function(token) {}); - sinon.stub(prompt,"read").callsFake(function(opts,callback) { + sinon.stub(prompt,"read").callsFake(function(opts) { if (/Username/.test(opts.prompt)) { - callback(null,"username"); + return Promise.resolve("username") } else if (/Password/.test(opts.prompt)) { - callback(null,"password"); + return Promise.resolve("password") } }); }); @@ -68,7 +68,10 @@ describe("commands/list", function() { /Logged in/.test(result.log.args[0][0]).should.be.true(); done(); - }).catch(done); + }).catch(err=> { + console.log("CAUGH", err); + done(err) + }); }); it('handles unsupported login type', function(done) {