diff --git a/red/api/admin/index.js b/red/api/admin/index.js index 81b733aee..57c79eb62 100644 --- a/red/api/admin/index.js +++ b/red/api/admin/index.js @@ -19,7 +19,6 @@ var express = require("express"); var nodes = require("./nodes"); var flows = require("./flows"); var flow = require("./flow"); -var info = require("./info"); var auth = require("../auth"); var apiUtil = require("../util"); @@ -28,7 +27,6 @@ module.exports = { init: function(runtime) { flows.init(runtime); flow.init(runtime); - info.init(runtime); nodes.init(runtime); var needsPermission = auth.needsPermission; @@ -54,9 +52,6 @@ module.exports = { adminApp.get(/\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.read"),nodes.getSet,apiUtil.errorHandler); adminApp.put(/\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.write"),nodes.putSet,apiUtil.errorHandler); - // Settings - adminApp.get("/settings",needsPermission("settings.read"),info.settings,apiUtil.errorHandler); - return adminApp; } } diff --git a/red/api/editor/index.js b/red/api/editor/index.js index 20461ee52..b9f4d539a 100644 --- a/red/api/editor/index.js +++ b/red/api/editor/index.js @@ -19,6 +19,7 @@ var path = require('path'); var comms = require("./comms"); var library = require("./library"); +var info = require("./settings"); var auth = require("../auth"); var needsPermission = auth.needsPermission; @@ -41,7 +42,7 @@ module.exports = { log = runtime.log; var settings = runtime.settings; if (!settings.disableEditor) { - + info.init(runtime); comms.init(server,runtime); var ui = require("./ui"); @@ -89,6 +90,14 @@ module.exports = { credentials.init(runtime); editorApp.get('/credentials/:type/:id', needsPermission("credentials.read"),credentials.get,apiUtil.errorHandler); + // Settings + editorApp.get("/settings",needsPermission("settings.read"),info.runtimeSettings,apiUtil.errorHandler); + // User Settings + editorApp.get("/settings/user",needsPermission("settings.read"),info.userSettings,apiUtil.errorHandler); + // User Settings + editorApp.post("/settings/user",needsPermission("settings.write"),info.updateUserSettings,apiUtil.errorHandler); + + return editorApp; } }, diff --git a/red/api/admin/info.js b/red/api/editor/settings.js similarity index 64% rename from red/api/admin/info.js rename to red/api/editor/settings.js index 8a5efa0ff..d4675902c 100644 --- a/red/api/admin/info.js +++ b/red/api/editor/settings.js @@ -23,7 +23,7 @@ module.exports = { runtime = _runtime; settings = runtime.settings; }, - settings: function(req,res) { + runtimeSettings: function(req,res) { var safeSettings = { httpNodeRoot: settings.httpNodeRoot||"/", version: settings.version, @@ -51,5 +51,29 @@ module.exports = { settings.exportNodeSettings(safeSettings); res.json(safeSettings); + }, + userSettings: function(req, res) { + var username; + if (!req.user || req.user.anonymous) { + username = '_'; + } else { + username = req.user.username; + } + res.json(settings.getUserSettings(username)||{}); + }, + updateUserSettings: function(req,res) { + var username; + if (!req.user || req.user.anonymous) { + username = '_'; + } else { + username = req.user.username; + } + settings.setUserSettings(username, req.body).then(function() { + log.audit({event: "settings.update",username:username},req); + res.status(204).end(); + }).otherwise(function(err) { + log.audit({event: "settings.update",username:username,error:err.code||"unexpected_error",message:err.toString()},req); + res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); + }); } } diff --git a/red/api/util.js b/red/api/util.js index 256bb1b92..bb3e2be47 100644 --- a/red/api/util.js +++ b/red/api/util.js @@ -24,6 +24,7 @@ module.exports = { i18n = _runtime.i18n; }, errorHandler: function(err,req,res,next) { + console.error(err.stack); if (err.message === "request entity too large") { log.error(err); } else { diff --git a/red/runtime/settings.js b/red/runtime/settings.js index 02db28e9c..b6fda36c5 100644 --- a/red/runtime/settings.js +++ b/red/runtime/settings.js @@ -20,22 +20,29 @@ var assert = require("assert"); var log = require("./log"); var util = require("./util"); -var userSettings = null; +// localSettings are those provided in the runtime settings.js file +var localSettings = null; +// globalSettings are provided by storage - .config.json on localfilesystem var globalSettings = null; +// nodeSettings are those settings that a node module defines as being available var nodeSettings = null; + +// A subset of globalSettings that deal with per-user settings +var userSettings = null; + var disableNodeSettings = null; var storage = null; var persistentSettings = { init: function(settings) { - userSettings = settings; + localSettings = settings; for (var i in settings) { /* istanbul ignore else */ if (settings.hasOwnProperty(i) && i !== 'load' && i !== 'get' && i !== 'set' && i !== 'available' && i !== 'reset') { // Don't allow any of the core functions get replaced via settings (function() { var j = i; - persistentSettings.__defineGetter__(j,function() { return userSettings[j]; }); + persistentSettings.__defineGetter__(j,function() { return localSettings[j]; }); persistentSettings.__defineSetter__(j,function() { throw new Error("Property '"+j+"' is read-only"); }); })(); } @@ -48,11 +55,15 @@ var persistentSettings = { storage = _storage; return storage.getSettings().then(function(_settings) { globalSettings = _settings; + userSettings = globalSettings.users || {}; }); }, get: function(prop) { - if (userSettings.hasOwnProperty(prop)) { - return clone(userSettings[prop]); + if (prop === 'users') { + throw new Error("Do not access user settings directly. Use settings.getUserSettings"); + } + if (localSettings.hasOwnProperty(prop)) { + return clone(localSettings[prop]); } if (globalSettings === null) { throw new Error(log._("settings.not-available")); @@ -61,7 +72,10 @@ var persistentSettings = { }, set: function(prop,value) { - if (userSettings.hasOwnProperty(prop)) { + if (prop === 'users') { + throw new Error("Do not access user settings directly. Use settings.setUserSettings"); + } + if (localSettings.hasOwnProperty(prop)) { throw new Error(log._("settings.property-read-only", {prop:prop})); } if (globalSettings === null) { @@ -77,7 +91,7 @@ var persistentSettings = { } }, delete: function(prop) { - if (userSettings.hasOwnProperty(prop)) { + if (localSettings.hasOwnProperty(prop)) { throw new Error(log._("settings.property-read-only", {prop:prop})); } if (globalSettings === null) { @@ -95,14 +109,15 @@ var persistentSettings = { }, reset: function() { - for (var i in userSettings) { + for (var i in localSettings) { /* istanbul ignore else */ - if (userSettings.hasOwnProperty(i)) { + if (localSettings.hasOwnProperty(i)) { delete persistentSettings[i]; } } - userSettings = null; + localSettings = null; globalSettings = null; + userSettings = null; storage = null; }, registerNodeSettings: function(type, opts) { @@ -126,8 +141,8 @@ var persistentSettings = { if (setting.exportable) { if (safeSettings.hasOwnProperty(property)) { // Cannot overwrite existing setting - } else if (userSettings.hasOwnProperty(property)) { - safeSettings[property] = userSettings[property]; + } else if (localSettings.hasOwnProperty(property)) { + safeSettings[property] = localSettings[property]; } else if (setting.hasOwnProperty('value')) { safeSettings[property] = setting.value; } @@ -148,6 +163,21 @@ var persistentSettings = { types.forEach(function(type) { disableNodeSettings[type] = true; }); + }, + getUserSettings: function(username) { + console.log(username); + return userSettings[username]; + }, + setUserSettings: function(username,settings) { + var current = userSettings[username]; + userSettings[username] = settings; + try { + assert.deepEqual(current,settings); + return when.resolve(); + } catch(err) { + globalSettings.user = userSettings; + return storage.saveSettings(globalSettings); + } } } diff --git a/test/red/api/admin/index_spec.js b/test/red/api/admin/index_spec.js index 3bdc54175..00941d0fa 100644 --- a/test/red/api/admin/index_spec.js +++ b/test/red/api/admin/index_spec.js @@ -24,7 +24,6 @@ var auth = require("../../../../red/api/auth"); var nodes = require("../../../../red/api/admin/nodes"); var flows = require("../../../../red/api/admin/flows"); var flow = require("../../../../red/api/admin/flow"); -var info = require("../../../../red/api/admin/info"); /** * Ensure all API routes are correctly mounted, with the expected permissions checks @@ -33,7 +32,7 @@ describe("api/admin/index", function() { describe("Ensure all API routes are correctly mounted, with the expected permissions checks", function() { var app; var mockList = [ - flows,flow,info,nodes + flows,flow,nodes ] var permissionChecks = {}; var lastRequest; @@ -68,8 +67,6 @@ describe("api/admin/index", function() { sinon.stub(nodes,"getSet",stubApp); sinon.stub(nodes,"putSet",stubApp); - sinon.stub(info,"settings",stubApp); - }); after(function() { mockList.forEach(function(m) { @@ -91,7 +88,6 @@ describe("api/admin/index", function() { nodes.getSet.restore(); nodes.putSet.restore(); - info.settings.restore(); }); before(function() { @@ -285,15 +281,5 @@ describe("api/admin/index", function() { done(); }) }); - - it('GET /settings', function(done) { - request(app).get("/settings").expect(200).end(function(err,res) { - if (err) { - return done(err); - } - permissionChecks.should.have.property('settings.read',1); - done(); - }) - }); }); }); diff --git a/test/red/api/editor/index_spec.js b/test/red/api/editor/index_spec.js index 9bd27c81c..079a1f38a 100644 --- a/test/red/api/editor/index_spec.js +++ b/test/red/api/editor/index_spec.js @@ -20,6 +20,7 @@ var request = require("supertest"); var express = require("express"); var editorApi = require("../../../../red/api/editor"); var comms = require("../../../../red/api/editor/comms"); +var info = require("../../../../red/api/editor/settings"); describe("api/editor/index", function() { @@ -27,9 +28,11 @@ describe("api/editor/index", function() { describe("disabled the editor", function() { beforeEach(function() { sinon.stub(comms,'init', function(){}); + sinon.stub(info,'init', function(){}); }); afterEach(function() { comms.init.restore(); + info.init.restore(); }); it("disables the editor", function() { var editorApp = editorApi.init({},{ @@ -37,6 +40,7 @@ describe("api/editor/index", function() { }); should.not.exist(editorApp); comms.init.called.should.be.false(); + info.init.called.should.be.false(); }); }); describe("enables the editor", function() { @@ -61,9 +65,10 @@ describe("api/editor/index", function() { before(function() { app = editorApi.init({},{ log:{audit:function(){},error:function(msg){errors.push(msg)}}, - settings:{httpNodeRoot:true, httpAdminRoot: true,disableEditor:false}, + settings:{httpNodeRoot:true, httpAdminRoot: true,disableEditor:false,exportNodeSettings:function(){}}, events:{on:function(){},removeListener:function(){}}, - isStarted: function() { return isStarted; } + isStarted: function() { return isStarted; }, + nodes: {paletteEditorEnabled: function() { return false }} }); }); it('serves the editor', function(done) { @@ -105,5 +110,14 @@ describe("api/editor/index", function() { done(); }); }); + it('GET /settings', function(done) { + request(app).get("/settings").expect(200).end(function(err,res) { + if (err) { + return done(err); + } + // permissionChecks.should.have.property('settings.read',1); + done(); + }) + }); }); }); diff --git a/test/red/api/admin/info_spec.js b/test/red/api/editor/settings_spec.js similarity index 92% rename from test/red/api/admin/info_spec.js rename to test/red/api/editor/settings_spec.js index 1fdb0ba6b..cb864d38b 100644 --- a/test/red/api/admin/info_spec.js +++ b/test/red/api/editor/settings_spec.js @@ -21,15 +21,15 @@ var sinon = require('sinon'); var when = require('when'); var app = express(); -var info = require("../../../../red/api/admin/info"); +var info = require("../../../../red/api/editor/settings"); var theme = require("../../../../red/api/editor/theme"); -describe("api/admin/info", function() { +describe("api/editor/settings", function() { describe("settings handler", function() { before(function() { sinon.stub(theme,"settings",function() { return { test: 456 };}); app = express(); - app.get("/settings",info.settings); + app.get("/settings",info.runtimeSettings); }); after(function() { @@ -49,7 +49,8 @@ describe("api/admin/info", function() { }, nodes: { paletteEditorEnabled: function() { return true; } - } + }, + log: { error: console.error } }); request(app) .get("/settings") @@ -78,7 +79,8 @@ describe("api/admin/info", function() { }, nodes: { paletteEditorEnabled: function() { return false; } - } + }, + log: { error: console.error } }); request(app) .get("/settings")