Compare commits

...

34 Commits

Author SHA1 Message Date
Nick O'Leary
06abe63fb1 Merge branch 'master' into runtime-api 2018-07-30 10:13:51 +01:00
Nick O'Leary
9d507b09ca Skip context tests until they migrate to runtimeAPI structure 2018-07-29 23:54:43 +01:00
Nick O'Leary
9c4a712dc7 Merge branch 'master' into runtime-api 2018-07-29 23:47:19 +01:00
Nick O'Leary
0835fdd0d1 Merge branch '0.19' into runtime-api 2018-06-06 21:59:46 +01:00
Nick O'Leary
522360dcb7 merge to latest 2018-05-23 12:45:29 +01:00
Nick O'Leary
979713c4db merge 0.19 2018-05-21 12:28:06 +01:00
Nick O'Leary
e41d5c249f WIP - url rewriting to support debug 2018-05-14 14:32:58 +01:00
Nick O'Leary
f82a779817 merge master 2018-05-14 09:14:35 +01:00
Nick O'Leary
df8a8ea204 Connect comms to apiRootUrl 2018-05-11 22:13:13 +01:00
Nick O'Leary
8957d33e49 Merge branch 'master' into runtime-api 2018-05-11 14:17:46 +01:00
Nick O'Leary
28fe1e4c8f Allow the editor to use a custom admin api url root 2018-05-11 13:26:26 +01:00
Nick O'Leary
0c7f4e2168 Merge 0.18.5 2018-05-10 21:45:25 +01:00
Nick O'Leary
b22956bd99 Remove old locales test 2018-05-01 12:28:16 +01:00
Nick O'Leary
42516206d9 Move module message catalogs under runtime api 2018-05-01 12:28:16 +01:00
Nick O'Leary
fc4edde6e6 Add runtime-api tests 2018-05-01 12:28:15 +01:00
Nick O'Leary
54cc04fd96 Tweak the initialisation of the editor js 2018-05-01 12:28:15 +01:00
Nick O'Leary
80062b6a62 Move type editors into their own files 2018-05-01 12:28:15 +01:00
Nick O'Leary
99af79fcf3 Add missing test resources 2018-05-01 12:28:15 +01:00
Nick O'Leary
11d87205d7 Move node registry to its own top level dir 2018-05-01 12:28:15 +01:00
Nick O'Leary
5866d414ce Replace some instances of when with Promise 2018-05-01 12:28:15 +01:00
Nick O'Leary
9a972b0b8a Increase test coverage 2018-05-01 12:28:15 +01:00
Nick O'Leary
e6aeeea8c1 Add better docs tasks 2018-05-01 12:28:15 +01:00
Nick O'Leary
5d064aa1d7 Fixup all the tests 2018-05-01 12:28:15 +01:00
Nick O'Leary
34832d5942 Fix up runtime tests 2018-05-01 12:28:15 +01:00
Nick O'Leary
e3b1179a21 Start bringing the tests back from the brink 2018-05-01 12:28:15 +01:00
Nick O'Leary
f94a36613c Split comms across api and runtime 2018-05-01 12:28:14 +01:00
Nick O'Leary
efc3cc24f4 Fixup projects import after file move 2018-05-01 12:28:14 +01:00
Nick O'Leary
b47f8aaf70 Rename projects.js 2018-05-01 12:28:14 +01:00
Nick O'Leary
94ca4607bc Add projects to runtime-api 2018-05-01 12:28:14 +01:00
Nick O'Leary
2dab1d3e6e Fix up merge issue on api/nodes 2018-05-01 12:28:14 +01:00
Nick O'Leary
825b0fb22f Update locales module to new structure 2018-05-01 12:28:14 +01:00
Nick O'Leary
1cdb039ea2 Move log and i18n to their own utils module 2018-05-01 12:28:14 +01:00
Nick O'Leary
7409cb3abb Separate library api and runtime components 2018-05-01 12:28:14 +01:00
Nick O'Leary
e8e8f70c27 WIP: create new runtime-api 2018-05-01 12:28:14 +01:00
158 changed files with 9730 additions and 4452 deletions

View File

@@ -414,6 +414,24 @@ module.exports = function(grunt) {
cwd: '<%= paths.dist %>/',
src: ['node-red-<%= pkg.version %>/**']
}
},
jsdoc : {
runtimeAPI: {
src: 'red/runtime-api/*.js',
options: {
destination: 'docs',
configure: './jsdoc.json'
}
}
},
jsdoc2md: {
runtimeAPI: {
options: {
separators: true
},
src: 'red/runtime-api/*.js',
dest: 'docs/runtime-api.md'
}
}
});
@@ -432,6 +450,8 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-jsonlint');
grunt.loadNpmTasks('grunt-mocha-istanbul');
grunt.loadNpmTasks('grunt-webdriver');
grunt.loadNpmTasks('grunt-jsdoc');
grunt.loadNpmTasks('grunt-jsdoc-to-markdown');
grunt.registerMultiTask('attachCopyright', function() {
var files = this.data.src;
@@ -514,4 +534,8 @@ module.exports = function(grunt) {
grunt.registerTask('coverage',
'Run Istanbul code test coverage task',
['build','mocha_istanbul:all']);
grunt.registerTask('docs',
'Generates API documentation',
['jsdoc','jsdoc2md']);
};

View File

@@ -28,14 +28,24 @@ RED.comms = (function() {
function connectWS() {
active = true;
var path = location.hostname;
var port = location.port;
if (port.length !== 0) {
path = path+":"+port;
var wspath;
if (RED.settings.apiRootUrl) {
var m = /^(https?):\/\/(.*)$/.exec(RED.settings.apiRootUrl);
if (m) {
console.log(m);
wspath = "ws"+(m[1]==="https"?"s":"")+"://"+m[2]+"comms";
}
} else {
var path = location.hostname;
var port = location.port;
if (port.length !== 0) {
path = path+":"+port;
}
path = path+document.location.pathname;
path = path+(path.slice(-1) == "/"?"":"/")+"comms";
wspath = "ws"+(document.location.protocol=="https:"?"s":"")+"://"+path;
}
path = path+document.location.pathname;
path = path+(path.slice(-1) == "/"?"":"/")+"comms";
path = "ws"+(document.location.protocol=="https:"?"s":"")+"://"+path;
var auth_tokens = RED.settings.get("auth-tokens");
pendingAuth = (auth_tokens!=null);
@@ -48,7 +58,7 @@ RED.comms = (function() {
}
}
ws = new WebSocket(path);
ws = new WebSocket(wspath);
ws.onopen = function() {
reconnectAttempts = 0;
if (errornotification) {

View File

@@ -16,10 +16,13 @@
RED.i18n = (function() {
var apiRootUrl;
return {
init: function(done) {
init: function(options, done) {
apiRootUrl = options.apiRootUrl||"";
i18n.init({
resGetPath: 'locales/__ns__?lng=__lng__',
resGetPath: apiRootUrl+'locales/__ns__?lng=__lng__',
dynamicLoad: false,
load:'current',
ns: {
@@ -36,7 +39,7 @@ RED.i18n = (function() {
}
},
loadCatalog: function(namespace,done) {
loadNodeCatalog: function(namespace,done) {
var languageList = i18n.functions.toLanguages(i18n.detectLanguage());
var toLoad = languageList.length;
languageList.forEach(function(lang) {
@@ -45,7 +48,7 @@ RED.i18n = (function() {
"Accept":"application/json"
},
cache: false,
url: 'locales/'+namespace+'?lng='+lang,
url: apiRootUrl+'nodes/'+namespace+'/messages?lng='+lang,
success: function(data) {
i18n.addResourceBundle(lang,namespace,data);
toLoad--;
@@ -68,7 +71,7 @@ RED.i18n = (function() {
"Accept":"application/json"
},
cache: false,
url: 'locales/nodes?lng='+lang,
url: apiRootUrl+'nodes/messages?lng='+lang,
success: function(data) {
var namespaces = Object.keys(data);
namespaces.forEach(function(ns) {

View File

@@ -13,489 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
(function() {
function appendNodeConfig(nodeConfig) {
var m = /<!-- --- \[red-module:(\S+)\] --- -->/.exec(nodeConfig.trim());
var moduleId;
if (m) {
moduleId = m[1];
} else {
moduleId = "unknown";
}
try {
$("body").append(nodeConfig);
} catch(err) {
RED.notify(RED._("notification.errors.failedToAppendNode",{module:moduleId, error:err.toString()}),{
type: "error",
timeout: 10000
});
console.log("["+moduleId+"] "+err.toString());
}
$(function() {
if ((window.location.hostname !== "localhost") && (window.location.hostname !== "127.0.0.1")) {
document.title = document.title+" : "+window.location.hostname;
}
function loadNodeList() {
$.ajax({
headers: {
"Accept":"application/json"
},
cache: false,
url: 'nodes',
success: function(data) {
RED.nodes.setNodeList(data);
RED.i18n.loadNodeCatalogs(function() {
loadIconList(loadNodes);
});
}
});
}
function loadIconList(done) {
$.ajax({
headers: {
"Accept":"application/json"
},
cache: false,
url: 'icons',
success: function(data) {
RED.nodes.setIconSets(data);
if (done) {
done();
}
}
});
}
function loadNodes() {
$.ajax({
headers: {
"Accept":"text/html"
},
cache: false,
url: 'nodes',
success: function(data) {
var configs = data.trim().split(/(?=<!-- --- \[red-module:\S+\] --- -->)/);
configs.forEach(function(data) {
appendNodeConfig(data);
});
$("body").i18n();
$("#palette > .palette-spinner").hide();
$(".palette-scroll").removeClass("hide");
$("#palette-search").removeClass("hide");
loadFlows(function() {
if (RED.settings.theme("projects.enabled",false)) {
RED.projects.refresh(function(activeProject) {
RED.sidebar.info.refresh()
if (!activeProject) {
// Projects enabled but no active project
RED.menu.setDisabled('menu-item-projects-open',true);
RED.menu.setDisabled('menu-item-projects-settings',true);
if (activeProject === false) {
// User previously decline the migration to projects.
} else { // null/undefined
RED.projects.showStartup();
}
}
completeLoad();
});
} else {
// Projects disabled by the user
RED.sidebar.info.refresh()
completeLoad();
}
});
}
});
}
function loadFlows(done) {
$.ajax({
headers: {
"Accept":"application/json",
},
cache: false,
url: 'flows',
success: function(nodes) {
if (nodes) {
var currentHash = window.location.hash;
RED.nodes.version(nodes.rev);
RED.nodes.import(nodes.flows);
RED.nodes.dirty(false);
RED.view.redraw(true);
if (/^#flow\/.+$/.test(currentHash)) {
RED.workspaces.show(currentHash.substring(6));
}
}
done();
}
});
}
function completeLoad() {
var persistentNotifications = {};
RED.comms.subscribe("notification/#",function(topic,msg) {
var parts = topic.split("/");
var notificationId = parts[1];
if (notificationId === "runtime-deploy") {
// handled in ui/deploy.js
return;
}
if (notificationId === "node") {
// handled below
return;
}
if (notificationId === "project-update") {
RED.nodes.clear();
RED.history.clear();
RED.view.redraw(true);
RED.projects.refresh(function() {
loadFlows(function() {
var project = RED.projects.getActiveProject();
var message = {
"change-branch": RED._("notification.project.change-branch", {project: project.git.branches.local}),
"merge-abort": RED._("notification.project.merge-abort"),
"loaded": RED._("notification.project.loaded", {project: msg.project}),
"updated": RED._("notification.project.updated", {project: msg.project}),
"pull": RED._("notification.project.pull", {project: msg.project}),
"revert": RED._("notification.project.revert", {project: msg.project}),
"merge-complete": RED._("notification.project.merge-complete")
}[msg.action];
RED.notify("<p>"+message+"</p>");
RED.sidebar.info.refresh()
});
});
return;
}
if (msg.text) {
msg.default = msg.text;
var text = RED._(msg.text,msg);
var options = {
type: msg.type,
fixed: msg.timeout === undefined,
timeout: msg.timeout,
id: notificationId
}
if (notificationId === "runtime-state") {
if (msg.error === "missing-types") {
text+="<ul><li>"+msg.types.join("</li><li>")+"</li></ul>";
if (!!RED.projects.getActiveProject()) {
options.buttons = [
{
text: RED._("notification.label.manage-project-dep"),
click: function() {
persistentNotifications[notificationId].hideNotification();
RED.projects.settings.show('deps');
}
}
]
// } else if (RED.settings.theme('palette.editable') !== false) {
} else {
options.buttons = [
{
text: RED._("common.label.close"),
click: function() {
persistentNotifications[notificationId].hideNotification();
}
}
]
}
} else if (msg.error === "credentials_load_failed") {
if (RED.settings.theme("projects.enabled",false)) {
// projects enabled
if (RED.user.hasPermission("projects.write")) {
options.buttons = [
{
text: RED._("notification.label.setup-cred"),
click: function() {
persistentNotifications[notificationId].hideNotification();
RED.projects.showCredentialsPrompt();
}
}
]
}
} else {
options.buttons = [
{
text: RED._("common.label.close"),
click: function() {
persistentNotifications[notificationId].hideNotification();
}
}
]
}
} else if (msg.error === "missing_flow_file") {
if (RED.user.hasPermission("projects.write")) {
options.buttons = [
{
text: RED._("notification.label.setup-project"),
click: function() {
persistentNotifications[notificationId].hideNotification();
RED.projects.showFilesPrompt();
}
}
]
}
} else if (msg.error === "missing_package_file") {
if (RED.user.hasPermission("projects.write")) {
options.buttons = [
{
text: RED._("notification.label.create-default-package"),
click: function() {
persistentNotifications[notificationId].hideNotification();
RED.projects.createDefaultPackageFile();
}
}
]
}
} else if (msg.error === "project_empty") {
if (RED.user.hasPermission("projects.write")) {
options.buttons = [
{
text: RED._("notification.label.no-thanks"),
click: function() {
persistentNotifications[notificationId].hideNotification();
}
},
{
text: RED._("notification.label.create-default-project"),
click: function() {
persistentNotifications[notificationId].hideNotification();
RED.projects.createDefaultFileSet();
}
}
]
}
} else if (msg.error === "git_merge_conflict") {
RED.nodes.clear();
RED.sidebar.versionControl.refresh(true);
if (RED.user.hasPermission("projects.write")) {
options.buttons = [
{
text: RED._("notification.label.show-merge-conflicts"),
click: function() {
persistentNotifications[notificationId].hideNotification();
RED.sidebar.versionControl.showLocalChanges();
}
}
]
}
}
}
if (!persistentNotifications.hasOwnProperty(notificationId)) {
persistentNotifications[notificationId] = RED.notify(text,options);
} else {
persistentNotifications[notificationId].update(text,options);
}
} else if (persistentNotifications.hasOwnProperty(notificationId)) {
persistentNotifications[notificationId].close();
delete persistentNotifications[notificationId];
}
});
RED.comms.subscribe("status/#",function(topic,msg) {
var parts = topic.split("/");
var node = RED.nodes.node(parts[1]);
if (node) {
if (msg.hasOwnProperty("text")) {
if (msg.text[0] !== ".") {
msg.text = node._(msg.text.toString(),{defaultValue:msg.text.toString()});
}
}
node.status = msg;
node.dirty = true;
RED.view.redraw();
}
});
RED.comms.subscribe("notification/node/#",function(topic,msg) {
var i,m;
var typeList;
var info;
if (topic == "notification/node/added") {
var addedTypes = [];
msg.forEach(function(m) {
var id = m.id;
RED.nodes.addNodeSet(m);
addedTypes = addedTypes.concat(m.types);
RED.i18n.loadCatalog(id, function() {
$.get('nodes/'+id, function(data) {
appendNodeConfig(data);
});
});
});
if (addedTypes.length) {
typeList = "<ul><li>"+addedTypes.join("</li><li>")+"</li></ul>";
RED.notify(RED._("palette.event.nodeAdded", {count:addedTypes.length})+typeList,"success");
}
loadIconList();
} else if (topic == "notification/node/removed") {
for (i=0;i<msg.length;i++) {
m = msg[i];
info = RED.nodes.removeNodeSet(m.id);
if (info.added) {
typeList = "<ul><li>"+m.types.join("</li><li>")+"</li></ul>";
RED.notify(RED._("palette.event.nodeRemoved", {count:m.types.length})+typeList,"success");
}
}
loadIconList();
} else if (topic == "notification/node/enabled") {
if (msg.types) {
info = RED.nodes.getNodeSet(msg.id);
if (info.added) {
RED.nodes.enableNodeSet(msg.id);
typeList = "<ul><li>"+msg.types.join("</li><li>")+"</li></ul>";
RED.notify(RED._("palette.event.nodeEnabled", {count:msg.types.length})+typeList,"success");
} else {
$.get('nodes/'+msg.id, function(data) {
appendNodeConfig(data);
typeList = "<ul><li>"+msg.types.join("</li><li>")+"</li></ul>";
RED.notify(RED._("palette.event.nodeAdded", {count:msg.types.length})+typeList,"success");
});
}
}
} else if (topic == "notification/node/disabled") {
if (msg.types) {
RED.nodes.disableNodeSet(msg.id);
typeList = "<ul><li>"+msg.types.join("</li><li>")+"</li></ul>";
RED.notify(RED._("palette.event.nodeDisabled", {count:msg.types.length})+typeList,"success");
}
} else if (topic == "node/upgraded") {
RED.notify(RED._("palette.event.nodeUpgraded", {module:msg.module,version:msg.version}),"success");
RED.nodes.registry.setModulePendingUpdated(msg.module,msg.version);
}
// Refresh flow library to ensure any examples are updated
RED.library.loadFlowLibrary();
});
}
function showAbout() {
$.get('red/about', function(data) {
var aboutHeader = '<div style="text-align:center;">'+
'<img width="50px" src="red/images/node-red-icon.svg" />'+
'</div>';
RED.sidebar.info.set(aboutHeader+marked(data));
RED.sidebar.info.show();
});
}
function loadEditor() {
var menuOptions = [];
if (RED.settings.theme("projects.enabled",false)) {
menuOptions.push({id:"menu-item-projects-menu",label:RED._("menu.label.projects"),options:[
{id:"menu-item-projects-new",label:RED._("menu.label.projects-new"),disabled:false,onselect:"core:new-project"},
{id:"menu-item-projects-open",label:RED._("menu.label.projects-open"),disabled:false,onselect:"core:open-project"},
{id:"menu-item-projects-settings",label:RED._("menu.label.projects-settings"),disabled:false,onselect:"core:show-project-settings"}
]});
}
menuOptions.push({id:"menu-item-view-menu",label:RED._("menu.label.view.view"),options:[
// {id:"menu-item-view-show-grid",setting:"view-show-grid",label:RED._("menu.label.view.showGrid"),toggle:true,onselect:"core:toggle-show-grid"},
// {id:"menu-item-view-snap-grid",setting:"view-snap-grid",label:RED._("menu.label.view.snapGrid"),toggle:true,onselect:"core:toggle-snap-grid"},
// {id:"menu-item-status",setting:"node-show-status",label:RED._("menu.label.displayStatus"),toggle:true,onselect:"core:toggle-status", selected: true},
//null,
// {id:"menu-item-bidi",label:RED._("menu.label.view.textDir"),options:[
// {id:"menu-item-bidi-default",toggle:"text-direction",label:RED._("menu.label.view.defaultDir"),selected: true, onselect:function(s) { if(s){RED.text.bidi.setTextDirection("")}}},
// {id:"menu-item-bidi-ltr",toggle:"text-direction",label:RED._("menu.label.view.ltr"), onselect:function(s) { if(s){RED.text.bidi.setTextDirection("ltr")}}},
// {id:"menu-item-bidi-rtl",toggle:"text-direction",label:RED._("menu.label.view.rtl"), onselect:function(s) { if(s){RED.text.bidi.setTextDirection("rtl")}}},
// {id:"menu-item-bidi-auto",toggle:"text-direction",label:RED._("menu.label.view.auto"), onselect:function(s) { if(s){RED.text.bidi.setTextDirection("auto")}}}
// ]},
// null,
{id:"menu-item-sidebar",label:RED._("menu.label.sidebar.show"),toggle:true,onselect:"core:toggle-sidebar", selected: true},
null
]});
menuOptions.push(null);
menuOptions.push({id:"menu-item-import",label:RED._("menu.label.import"),options:[
{id:"menu-item-import-clipboard",label:RED._("menu.label.clipboard"),onselect:"core:show-import-dialog"},
{id:"menu-item-import-library",label:RED._("menu.label.library"),options:[]}
]});
menuOptions.push({id:"menu-item-export",label:RED._("menu.label.export"),options:[
{id:"menu-item-export-clipboard",label:RED._("menu.label.clipboard"),onselect:"core:show-export-dialog"},
{id:"menu-item-export-library",label:RED._("menu.label.library"),disabled:true,onselect:"core:library-export"}
]});
menuOptions.push(null);
menuOptions.push({id:"menu-item-search",label:RED._("menu.label.search"),onselect:"core:search"});
menuOptions.push(null);
menuOptions.push({id:"menu-item-config-nodes",label:RED._("menu.label.displayConfig"),onselect:"core:show-config-tab"});
menuOptions.push({id:"menu-item-workspace",label:RED._("menu.label.flows"),options:[
{id:"menu-item-workspace-add",label:RED._("menu.label.add"),onselect:"core:add-flow"},
{id:"menu-item-workspace-edit",label:RED._("menu.label.rename"),onselect:"core:edit-flow"},
{id:"menu-item-workspace-delete",label:RED._("menu.label.delete"),onselect:"core:remove-flow"}
]});
menuOptions.push({id:"menu-item-subflow",label:RED._("menu.label.subflows"), options: [
{id:"menu-item-subflow-create",label:RED._("menu.label.createSubflow"),onselect:"core:create-subflow"},
{id:"menu-item-subflow-convert",label:RED._("menu.label.selectionToSubflow"),disabled:true,onselect:"core:convert-to-subflow"},
]});
menuOptions.push(null);
if (RED.settings.theme('palette.editable') !== false) {
menuOptions.push({id:"menu-item-edit-palette",label:RED._("menu.label.editPalette"),onselect:"core:manage-palette"});
menuOptions.push(null);
}
menuOptions.push({id:"menu-item-user-settings",label:RED._("menu.label.settings"),onselect:"core:show-user-settings"});
menuOptions.push(null);
menuOptions.push({id:"menu-item-keyboard-shortcuts",label:RED._("menu.label.keyboardShortcuts"),onselect:"core:show-help"});
menuOptions.push({id:"menu-item-help",
label: RED.settings.theme("menu.menu-item-help.label",RED._("menu.label.help")),
href: RED.settings.theme("menu.menu-item-help.url","http://nodered.org/docs")
});
menuOptions.push({id:"menu-item-node-red-version", label:"v"+RED.settings.version, onselect: "core:show-about" });
RED.view.init();
RED.userSettings.init();
RED.user.init();
RED.library.init();
RED.keyboard.init();
RED.palette.init();
if (RED.settings.theme('palette.editable') !== false) {
RED.palette.editor.init();
} else {
console.log("Palette editor disabled");
}
RED.sidebar.init();
if (RED.settings.theme("projects.enabled",false)) {
RED.projects.init();
} else {
console.log("Projects disabled");
}
RED.subflow.init();
RED.workspaces.init();
RED.clipboard.init();
RED.search.init();
RED.editor.init();
RED.diff.init();
RED.menu.init({id:"btn-sidemenu",options: menuOptions});
RED.deploy.init(RED.settings.theme("deployButton",null));
RED.notifications.init();
RED.actions.add("core:show-about", showAbout);
RED.nodes.init();
RED.comms.connect();
$("#main-container").show();
$(".header-toolbar").show();
loadNodeList();
}
$(function() {
if ((window.location.hostname !== "localhost") && (window.location.hostname !== "127.0.0.1")) {
document.title = document.title+" : "+window.location.hostname;
}
ace.require("ace/ext/language_tools");
RED.i18n.init(function() {
RED.settings.init(loadEditor);
})
RED.init({
apiRootUrl: ""
});
})();
});

View File

@@ -13,4 +13,521 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var RED = {};
var RED = (function() {
function appendNodeConfig(nodeConfig,done) {
done = done || function(){};
var m = /<!-- --- \[red-module:(\S+)\] --- -->/.exec(nodeConfig.trim());
var moduleId;
if (m) {
moduleId = m[1];
} else {
moduleId = "unknown";
}
try {
var hasDeferred = false;
var nodeConfigEls = $("<div>"+nodeConfig+"</div>");
nodeConfigEls.find("script").each(function(i,el) {
var srcUrl = $(el).attr('src');
if (srcUrl && !/^\s*(https?:|\/|\.)/.test(srcUrl)) {
$(el).remove();
var newScript = document.createElement("script");
newScript.onload = function() { $("body").append(nodeConfigEls); done() }
$('body').append(newScript);
newScript.src = RED.settings.apiRootUrl+srcUrl;
hasDeferred = true;
}
})
if (!hasDeferred) {
$("body").append(nodeConfigEls);
done();
}
} catch(err) {
RED.notify(RED._("notification.errors.failedToAppendNode",{module:moduleId, error:err.toString()}),{
type: "error",
timeout: 10000
});
console.log("["+moduleId+"] "+err.toString());
done();
}
}
function loadNodeList() {
$.ajax({
headers: {
"Accept":"application/json"
},
cache: false,
url: 'nodes',
success: function(data) {
RED.nodes.setNodeList(data);
RED.i18n.loadNodeCatalogs(function() {
loadIconList(loadNodes);
});
}
});
}
function loadIconList(done) {
$.ajax({
headers: {
"Accept":"application/json"
},
cache: false,
url: 'icons',
success: function(data) {
RED.nodes.setIconSets(data);
if (done) {
done();
}
}
});
}
function loadNodes() {
$.ajax({
headers: {
"Accept":"text/html"
},
cache: false,
url: 'nodes',
success: function(data) {
var configs = data.trim().split(/(?=<!-- --- \[red-module:\S+\] --- -->)/);
var stepConfig = function() {
if (configs.length === 0) {
$("body").i18n();
$("#palette > .palette-spinner").hide();
$(".palette-scroll").removeClass("hide");
$("#palette-search").removeClass("hide");
loadFlows(function() {
if (RED.settings.theme("projects.enabled",false)) {
RED.projects.refresh(function(activeProject) {
RED.sidebar.info.refresh()
if (!activeProject) {
// Projects enabled but no active project
RED.menu.setDisabled('menu-item-projects-open',true);
RED.menu.setDisabled('menu-item-projects-settings',true);
if (activeProject === false) {
// User previously decline the migration to projects.
} else { // null/undefined
RED.projects.showStartup();
}
}
completeLoad();
});
} else {
// Projects disabled by the user
RED.sidebar.info.refresh()
completeLoad();
}
});
} else {
var config = configs.shift();
appendNodeConfig(config,stepConfig);
}
}
stepConfig();
}
});
}
function loadFlows(done) {
$.ajax({
headers: {
"Accept":"application/json",
},
cache: false,
url: 'flows',
success: function(nodes) {
if (nodes) {
var currentHash = window.location.hash;
RED.nodes.version(nodes.rev);
RED.nodes.import(nodes.flows);
RED.nodes.dirty(false);
RED.view.redraw(true);
if (/^#flow\/.+$/.test(currentHash)) {
RED.workspaces.show(currentHash.substring(6));
}
}
done();
}
});
}
function completeLoad() {
var persistentNotifications = {};
RED.comms.subscribe("notification/#",function(topic,msg) {
var parts = topic.split("/");
var notificationId = parts[1];
if (notificationId === "runtime-deploy") {
// handled in ui/deploy.js
return;
}
if (notificationId === "node") {
// handled below
return;
}
if (notificationId === "project-update") {
RED.nodes.clear();
RED.history.clear();
RED.view.redraw(true);
RED.projects.refresh(function() {
loadFlows(function() {
var project = RED.projects.getActiveProject();
var message = {
"change-branch": RED._("notification.project.change-branch", {project: project.git.branches.local}),
"merge-abort": RED._("notification.project.merge-abort"),
"loaded": RED._("notification.project.loaded", {project: msg.project}),
"updated": RED._("notification.project.updated", {project: msg.project}),
"pull": RED._("notification.project.pull", {project: msg.project}),
"revert": RED._("notification.project.revert", {project: msg.project}),
"merge-complete": RED._("notification.project.merge-complete")
}[msg.action];
RED.notify("<p>"+message+"</p>");
RED.sidebar.info.refresh()
});
});
return;
}
if (msg.text) {
msg.default = msg.text;
var text = RED._(msg.text,msg);
var options = {
type: msg.type,
fixed: msg.timeout === undefined,
timeout: msg.timeout,
id: notificationId
}
if (notificationId === "runtime-state") {
if (msg.error === "missing-types") {
text+="<ul><li>"+msg.types.join("</li><li>")+"</li></ul>";
if (!!RED.projects.getActiveProject()) {
options.buttons = [
{
text: RED._("notification.label.manage-project-dep"),
click: function() {
persistentNotifications[notificationId].hideNotification();
RED.projects.settings.show('deps');
}
}
]
// } else if (RED.settings.theme('palette.editable') !== false) {
} else {
options.buttons = [
{
text: RED._("common.label.close"),
click: function() {
persistentNotifications[notificationId].hideNotification();
}
}
]
}
} else if (msg.error === "credentials_load_failed") {
if (RED.settings.theme("projects.enabled",false)) {
// projects enabled
if (RED.user.hasPermission("projects.write")) {
options.buttons = [
{
text: "Setup credentials",
click: function() {
persistentNotifications[notificationId].hideNotification();
RED.projects.showCredentialsPrompt();
}
}
]
}
} else {
options.buttons = [
{
text: "Close",
click: function() {
persistentNotifications[notificationId].hideNotification();
}
}
]
}
} else if (msg.error === "missing_flow_file") {
if (RED.user.hasPermission("projects.write")) {
options.buttons = [
{
text: "Setup project files",
click: function() {
persistentNotifications[notificationId].hideNotification();
RED.projects.showFilesPrompt();
}
}
]
}
} else if (msg.error === "missing_package_file") {
if (RED.user.hasPermission("projects.write")) {
options.buttons = [
{
text: "Create default package file",
click: function() {
persistentNotifications[notificationId].hideNotification();
RED.projects.createDefaultPackageFile();
}
}
]
}
} else if (msg.error === "project_empty") {
if (RED.user.hasPermission("projects.write")) {
options.buttons = [
{
text: "No thanks",
click: function() {
persistentNotifications[notificationId].hideNotification();
}
},
{
text: "Create default project files",
click: function() {
persistentNotifications[notificationId].hideNotification();
RED.projects.createDefaultFileSet();
}
}
]
}
} else if (msg.error === "git_merge_conflict") {
RED.nodes.clear();
RED.sidebar.versionControl.refresh(true);
if (RED.user.hasPermission("projects.write")) {
options.buttons = [
{
text: "Show merge conflicts",
click: function() {
persistentNotifications[notificationId].hideNotification();
RED.sidebar.versionControl.showLocalChanges();
}
}
]
}
}
}
if (!persistentNotifications.hasOwnProperty(notificationId)) {
persistentNotifications[notificationId] = RED.notify(text,options);
} else {
persistentNotifications[notificationId].update(text,options);
}
} else if (persistentNotifications.hasOwnProperty(notificationId)) {
persistentNotifications[notificationId].close();
delete persistentNotifications[notificationId];
}
});
RED.comms.subscribe("status/#",function(topic,msg) {
var parts = topic.split("/");
var node = RED.nodes.node(parts[1]);
if (node) {
if (msg.hasOwnProperty("text")) {
if (msg.text[0] !== ".") {
msg.text = node._(msg.text.toString(),{defaultValue:msg.text.toString()});
}
}
node.status = msg;
node.dirty = true;
RED.view.redraw();
}
});
RED.comms.subscribe("notification/node/#",function(topic,msg) {
var i,m;
var typeList;
var info;
if (topic == "notification/node/added") {
var addedTypes = [];
msg.forEach(function(m) {
var id = m.id;
RED.nodes.addNodeSet(m);
addedTypes = addedTypes.concat(m.types);
RED.i18n.loadNodeCatalog(id, function() {
$.get('nodes/'+id, function(data) {
appendNodeConfig(data);
});
});
});
if (addedTypes.length) {
typeList = "<ul><li>"+addedTypes.join("</li><li>")+"</li></ul>";
RED.notify(RED._("palette.event.nodeAdded", {count:addedTypes.length})+typeList,"success");
}
loadIconList();
} else if (topic == "notification/node/removed") {
for (i=0;i<msg.length;i++) {
m = msg[i];
info = RED.nodes.removeNodeSet(m.id);
if (info.added) {
typeList = "<ul><li>"+m.types.join("</li><li>")+"</li></ul>";
RED.notify(RED._("palette.event.nodeRemoved", {count:m.types.length})+typeList,"success");
}
}
loadIconList();
} else if (topic == "notification/node/enabled") {
if (msg.types) {
info = RED.nodes.getNodeSet(msg.id);
if (info.added) {
RED.nodes.enableNodeSet(msg.id);
typeList = "<ul><li>"+msg.types.join("</li><li>")+"</li></ul>";
RED.notify(RED._("palette.event.nodeEnabled", {count:msg.types.length})+typeList,"success");
} else {
$.get('nodes/'+msg.id, function(data) {
appendNodeConfig(data);
typeList = "<ul><li>"+msg.types.join("</li><li>")+"</li></ul>";
RED.notify(RED._("palette.event.nodeAdded", {count:msg.types.length})+typeList,"success");
});
}
}
} else if (topic == "notification/node/disabled") {
if (msg.types) {
RED.nodes.disableNodeSet(msg.id);
typeList = "<ul><li>"+msg.types.join("</li><li>")+"</li></ul>";
RED.notify(RED._("palette.event.nodeDisabled", {count:msg.types.length})+typeList,"success");
}
} else if (topic == "node/upgraded") {
RED.notify(RED._("palette.event.nodeUpgraded", {module:msg.module,version:msg.version}),"success");
RED.nodes.registry.setModulePendingUpdated(msg.module,msg.version);
}
// Refresh flow library to ensure any examples are updated
RED.library.loadFlowLibrary();
});
}
function showAbout() {
$.get('red/about', function(data) {
var aboutHeader = '<div style="text-align:center;">'+
'<img width="50px" src="red/images/node-red-icon.svg" />'+
'</div>';
RED.sidebar.info.set(aboutHeader+marked(data));
RED.sidebar.info.show();
});
}
function loadEditor() {
var menuOptions = [];
if (RED.settings.theme("projects.enabled",false)) {
menuOptions.push({id:"menu-item-projects-menu",label:"Projects",options:[
{id:"menu-item-projects-new",label:"New",disabled:false,onselect:"core:new-project"},
{id:"menu-item-projects-open",label:"Open",disabled:false,onselect:"core:open-project"},
{id:"menu-item-projects-settings",label:"Project Settings",disabled:false,onselect:"core:show-project-settings"}
]});
}
menuOptions.push({id:"menu-item-view-menu",label:RED._("menu.label.view.view"),options:[
// {id:"menu-item-view-show-grid",setting:"view-show-grid",label:RED._("menu.label.view.showGrid"),toggle:true,onselect:"core:toggle-show-grid"},
// {id:"menu-item-view-snap-grid",setting:"view-snap-grid",label:RED._("menu.label.view.snapGrid"),toggle:true,onselect:"core:toggle-snap-grid"},
// {id:"menu-item-status",setting:"node-show-status",label:RED._("menu.label.displayStatus"),toggle:true,onselect:"core:toggle-status", selected: true},
//null,
// {id:"menu-item-bidi",label:RED._("menu.label.view.textDir"),options:[
// {id:"menu-item-bidi-default",toggle:"text-direction",label:RED._("menu.label.view.defaultDir"),selected: true, onselect:function(s) { if(s){RED.text.bidi.setTextDirection("")}}},
// {id:"menu-item-bidi-ltr",toggle:"text-direction",label:RED._("menu.label.view.ltr"), onselect:function(s) { if(s){RED.text.bidi.setTextDirection("ltr")}}},
// {id:"menu-item-bidi-rtl",toggle:"text-direction",label:RED._("menu.label.view.rtl"), onselect:function(s) { if(s){RED.text.bidi.setTextDirection("rtl")}}},
// {id:"menu-item-bidi-auto",toggle:"text-direction",label:RED._("menu.label.view.auto"), onselect:function(s) { if(s){RED.text.bidi.setTextDirection("auto")}}}
// ]},
// null,
{id:"menu-item-sidebar",label:RED._("menu.label.sidebar.show"),toggle:true,onselect:"core:toggle-sidebar", selected: true},
null
]});
menuOptions.push(null);
menuOptions.push({id:"menu-item-import",label:RED._("menu.label.import"),options:[
{id:"menu-item-import-clipboard",label:RED._("menu.label.clipboard"),onselect:"core:show-import-dialog"},
{id:"menu-item-import-library",label:RED._("menu.label.library"),options:[]}
]});
menuOptions.push({id:"menu-item-export",label:RED._("menu.label.export"),options:[
{id:"menu-item-export-clipboard",label:RED._("menu.label.clipboard"),onselect:"core:show-export-dialog"},
{id:"menu-item-export-library",label:RED._("menu.label.library"),disabled:true,onselect:"core:library-export"}
]});
menuOptions.push(null);
menuOptions.push({id:"menu-item-search",label:RED._("menu.label.search"),onselect:"core:search"});
menuOptions.push(null);
menuOptions.push({id:"menu-item-config-nodes",label:RED._("menu.label.displayConfig"),onselect:"core:show-config-tab"});
menuOptions.push({id:"menu-item-workspace",label:RED._("menu.label.flows"),options:[
{id:"menu-item-workspace-add",label:RED._("menu.label.add"),onselect:"core:add-flow"},
{id:"menu-item-workspace-edit",label:RED._("menu.label.rename"),onselect:"core:edit-flow"},
{id:"menu-item-workspace-delete",label:RED._("menu.label.delete"),onselect:"core:remove-flow"}
]});
menuOptions.push({id:"menu-item-subflow",label:RED._("menu.label.subflows"), options: [
{id:"menu-item-subflow-create",label:RED._("menu.label.createSubflow"),onselect:"core:create-subflow"},
{id:"menu-item-subflow-convert",label:RED._("menu.label.selectionToSubflow"),disabled:true,onselect:"core:convert-to-subflow"},
]});
menuOptions.push(null);
if (RED.settings.theme('palette.editable') !== false) {
menuOptions.push({id:"menu-item-edit-palette",label:RED._("menu.label.editPalette"),onselect:"core:manage-palette"});
menuOptions.push(null);
}
menuOptions.push({id:"menu-item-user-settings",label:RED._("menu.label.settings"),onselect:"core:show-user-settings"});
menuOptions.push(null);
menuOptions.push({id:"menu-item-keyboard-shortcuts",label:RED._("menu.label.keyboardShortcuts"),onselect:"core:show-help"});
menuOptions.push({id:"menu-item-help",
label: RED.settings.theme("menu.menu-item-help.label",RED._("menu.label.help")),
href: RED.settings.theme("menu.menu-item-help.url","http://nodered.org/docs")
});
menuOptions.push({id:"menu-item-node-red-version", label:"v"+RED.settings.version, onselect: "core:show-about" });
RED.view.init();
RED.userSettings.init();
RED.user.init();
RED.library.init();
RED.keyboard.init();
RED.palette.init();
if (RED.settings.theme('palette.editable') !== false) {
RED.palette.editor.init();
} else {
console.log("Palette editor disabled");
}
RED.sidebar.init();
if (RED.settings.theme("projects.enabled",false)) {
RED.projects.init();
} else {
console.log("Projects disabled");
}
RED.subflow.init();
RED.workspaces.init();
RED.clipboard.init();
RED.search.init();
RED.editor.init();
RED.diff.init();
RED.menu.init({id:"btn-sidemenu",options: menuOptions});
RED.deploy.init(RED.settings.theme("deployButton",null));
RED.notifications.init();
RED.actions.add("core:show-about", showAbout);
RED.nodes.init();
RED.comms.connect();
$("#main-container").show();
$(".header-toolbar").show();
loadNodeList();
}
var initialised = false;
function init(options) {
if (initialised) {
throw new Error("RED already initialised");
}
initialised = true;
ace.require("ace/ext/language_tools");
options = options || {};
options.apiRootUrl = options.apiRootUrl || "";
if (options.apiRootUrl && !/\/$/.test(options.apiRootUrl)) {
options.apiRootUrl = options.apiRootUrl+"/";
}
RED.i18n.init(options, function() {
RED.settings.init(options, loadEditor);
})
}
return {
init: init
}
})();

View File

@@ -89,18 +89,22 @@ RED.settings = (function () {
userSettings = data;
}
var init = function (done) {
var init = function (options, done) {
var accessTokenMatch = /[?&]access_token=(.*?)(?:$|&)/.exec(window.location.search);
if (accessTokenMatch) {
var accessToken = accessTokenMatch[1];
RED.settings.set("auth-tokens",{access_token: accessToken});
window.location.search = "";
}
RED.settings.apiRootUrl = options.apiRootUrl;
$.ajaxSetup({
beforeSend: function(jqXHR,settings) {
// Only attach auth header for requests to relative paths
if (!/^\s*(https?:|\/|\.)/.test(settings.url)) {
if (options.apiRootUrl) {
settings.url = options.apiRootUrl+settings.url;
}
var auth_tokens = RED.settings.get("auth-tokens");
if (auth_tokens) {
jqXHR.setRequestHeader("Authorization","Bearer "+auth_tokens.access_token);

View File

@@ -18,6 +18,12 @@ RED.library = (function() {
var exportToLibraryDialog;
var elementPrefix = "node-input-";
var _librarySaveConfirm = '<div id="node-dialog-library-save-confirm" class="hide"><form class="form-horizontal"><div style="text-align: center; padding-top: 30px;" id="node-dialog-library-save-content"></div></form></div>';
var _librarySave = '<div id="node-dialog-library-save" class="hide"><form class="form-horizontal"><div class="form-row"><label for="node-dialog-library-save-folder" data-i18n="[append]library.folder"><i class="fa fa-folder-open"></i> </label><input type="text" id="node-dialog-library-save-folder" data-i18n="[placeholder]library.folderPlaceholder"></div><div class="form-row"><label for="node-dialog-library-save-filename" data-i18n="[append]library.filename"><i class="fa fa-file"></i> </label><input type="text" id="node-dialog-library-save-filename" data-i18n="[placeholder]library.filenamePlaceholder"></div></form></div>';
var _libraryLookup = '<div id="node-dialog-library-lookup" class="hide"><form class="form-horizontal"><div class="form-row"><ul id="node-dialog-library-breadcrumbs" class="breadcrumb"><li class="active"><a href="#" data-i18n="[append]library.breadcrumb"></a></li></ul></div><div class="form-row"><div style="vertical-align: top; display: inline-block; height: 100%; width: 30%; padding-right: 20px;"><div id="node-select-library" style="border: 1px solid #999; width: 100%; height: 100%; overflow:scroll;"><ul></ul></div></div><div style="vertical-align: top; display: inline-block;width: 65%; height: 100%;"><div style="height: 100%; width: 95%;" class="node-text-editor" id="node-select-library-text" ></div></div></div></form></div>';
function loadFlowLibrary() {
$.getJSON("library/flows",function(data) {
//console.log(data);
@@ -410,6 +416,11 @@ RED.library = (function() {
return {
init: function() {
$(_librarySave).appendTo(document.body);
$(_librarySaveConfirm).appendTo(document.body);
$(_libraryLookup).appendTo(document.body);
RED.actions.add("core:library-export",exportFlow);
RED.events.on("view:selection-changed",function(selection) {

View File

@@ -459,7 +459,6 @@ RED.palette = (function() {
}
});
RED.events.on('registry:node-set-disabled', function(nodeSet) {
console.log(nodeSet);
for (var j=0;j<nodeSet.types.length;j++) {
hideNodeType(nodeSet.types[j]);
var def = RED.nodes.getType(nodeSet.types[j]);

View File

@@ -17,6 +17,10 @@
RED.subflow = (function() {
var _subflowEditTemplate = '<script type="text/x-red" data-template-name="subflow"><div class="form-row"><label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label><input type="text" id="node-input-name"></div></script>';
var _subflowTemplateEditTemplate = '<script type="text/x-red" data-template-name="subflow-template"><div class="form-row"><i class="fa fa-tag"></i><label for="subflow-input-name" data-i18n="common.label.name"></label><input type="text" id="subflow-input-name"></div><div class="form-row" style="margin-bottom: 0px;"><label for="subflow-input-info" data-i18n="editor:subflow.info"></label><a href="https://help.github.com/articles/markdown-basics/" style="font-size: 0.8em; float: right;" data-i18n="[html]subflow.format"></a></div><div class="form-row node-text-editor-row"><div style="height: 250px;" class="node-text-editor" id="subflow-input-info-editor"></div></div><div class="form-row form-tips" id="subflow-dialog-user-count"></div></script>';
function getSubflow() {
return RED.nodes.subflow(RED.workspaces.active());
}
@@ -386,6 +390,10 @@ RED.subflow = (function() {
RED.actions.add("core:create-subflow",createSubflow);
RED.actions.add("core:convert-to-subflow",convertToSubflow);
$(_subflowEditTemplate).appendTo(document.body);
$(_subflowTemplateEditTemplate).appendTo(document.body);
}
function createSubflow() {

View File

@@ -756,25 +756,25 @@ RED.utils = (function() {
function getNodeIcon(def,node) {
if (def.category === 'config') {
return "icons/node-red/cog.png"
return RED.settings.apiRootUrl+"icons/node-red/cog.png"
} else if (node && node.type === 'tab') {
return "icons/node-red/subflow.png"
return RED.settings.apiRootUrl+"icons/node-red/subflow.png"
} else if (node && node.type === 'unknown') {
return "icons/node-red/alert.png"
return RED.settings.apiRootUrl+"icons/node-red/alert.png"
} else if (node && node.icon) {
var iconPath = separateIconPath(node.icon);
if (isIconExists(iconPath)) {
return "icons/" + node.icon;
return RED.settings.apiRootUrl+"icons/" + node.icon;
}
}
var iconPath = getDefaultNodeIcon(def, node);
if (def.category === 'subflows') {
if (!isIconExists(iconPath)) {
return "icons/node-red/subflow.png";
return RED.settings.apiRootUrl+"icons/node-red/subflow.png";
}
}
return "icons/"+iconPath.module+"/"+iconPath.file;
return RED.settings.apiRootUrl+"icons/"+iconPath.module+"/"+iconPath.file;
}
function getNodeLabel(node,defaultLabel) {

View File

@@ -36,7 +36,7 @@
</head>
<body spellcheck="false">
<div id="header">
<span class="logo">{{#header.url}}<a href="{{.}}">{{/header.url}}{{#header.image}}<img src="{{.}}" title="{{version}}">{{/header.image}} <span>{{ header.title }}</span>{{#header.url}}</a>{{/header.url}}</span>
<span class="logo">{{#header.url}}<a href="{{.}}">{{/header.url}}{{#header.image}}<img src="{{.}}">{{/header.image}} <span>{{ header.title }}</span>{{#header.url}}</a>{{/header.url}}</span>
<ul class="header-toolbar hide">
<li><a id="btn-sidemenu" class="button" data-toggle="dropdown" href="#"><i class="fa fa-bars"></i></a></li>
</ul>
@@ -83,68 +83,6 @@
<div id="notifications"></div>
<div id="dropTarget"><div data-i18n="[append]workspace.dropFlowHere"><br/><i class="fa fa-download"></i></div></div>
<div id="node-dialog-library-save-confirm" class="hide">
<form class="form-horizontal">
<div style="text-align: center; padding-top: 30px;" id="node-dialog-library-save-content">
</div>
</form>
</div>
<div id="node-dialog-library-save" class="hide">
<form class="form-horizontal">
<div class="form-row">
<label for="node-dialog-library-save-folder" data-i18n="[append]library.folder"><i class="fa fa-folder-open"></i> </label>
<input type="text" id="node-dialog-library-save-folder" data-i18n="[placeholder]library.folderPlaceholder">
</div>
<div class="form-row">
<label for="node-dialog-library-save-filename" data-i18n="[append]library.filename"><i class="fa fa-file"></i> </label>
<input type="text" id="node-dialog-library-save-filename" data-i18n="[placeholder]library.filenamePlaceholder">
</div>
</form>
</div>
<div id="node-dialog-library-lookup" class="hide">
<form class="form-horizontal">
<div class="form-row">
<ul id="node-dialog-library-breadcrumbs" class="breadcrumb">
<li class="active"><a href="#" data-i18n="[append]library.breadcrumb"></a></li>
</ul>
</div>
<div class="form-row">
<div style="vertical-align: top; display: inline-block; height: 100%; width: 30%; padding-right: 20px;">
<div id="node-select-library" style="border: 1px solid #999; width: 100%; height: 100%; overflow:scroll;"><ul></ul></div>
</div>
<div style="vertical-align: top; display: inline-block;width: 65%; height: 100%;">
<div style="height: 100%; width: 95%;" class="node-text-editor" id="node-select-library-text" ></div>
</div>
</div>
</form>
</div>
<script type="text/x-red" data-template-name="subflow">
<div class="form-row">
<label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label>
<input type="text" id="node-input-name">
</div>
</script>
<script type="text/x-red" data-template-name="subflow-template">
<div class="form-row">
<i class="fa fa-tag"></i>
<label for="subflow-input-name" data-i18n="common.label.name"></label><input type="text" id="subflow-input-name">
</div>
<div class="form-row">
<i class="fa fa-folder-o"></i>
<label for="subflow-input-category" data-i18n="editor:subflow.category"></label><select style="width: 250px;" id="subflow-input-category"></select><input style="display:none; margin-left: 10px; width:calc(100% - 250px)" type="text" id="subflow-input-custom-category">
</div>
<div class="form-row" style="margin-bottom: 0px;">
<label for="subflow-input-info" data-i18n="editor:subflow.info"></label>
<a href="https://help.github.com/articles/markdown-basics/" style="font-size: 0.8em; float: right;" data-i18n="[html]subflow.format"></a>
</div>
<div class="form-row node-text-editor-row">
<div style="height: 250px;" class="node-text-editor" id="subflow-input-info-editor"></div>
</div>
<div class="form-row form-tips" id="subflow-dialog-user-count"></div>
</script>
<script src="vendor/vendor.js"></script>
<script src="vendor/jsonata/jsonata.min.js"></script>
<script src="vendor/ace/ace.js"></script>

22
jsdoc.json Normal file
View File

@@ -0,0 +1,22 @@
{
"opts": {
"template": "./node_modules/ink-docstrap/template",
"destination": "./docs",
"recurse": true
},
"tags": {
"allowUnknownTags": false,
"dictionaries": ["jsdoc"]
},
"source": {
"include": [
"./red/runtime-api"
]
},
"templates": {
"systemName": "Node-RED Runtime API",
"theme":"yeti",
"footer": "",
"copyright": "Released under the Apache License v2.0"
}
}

View File

@@ -1,5 +1,5 @@
$(function() {
RED.i18n.init(function() {
RED.i18n.init({},function() {
var options = {
messageMouseEnter: function(sourceId) {
window.opener.postMessage({event:"mouseEnter",id:sourceId},'*');

View File

@@ -1,119 +1,124 @@
{
"name": "node-red",
"version": "0.18.7",
"description": "A visual tool for wiring the Internet of Things",
"homepage": "http://nodered.org",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/node-red/node-red.git"
},
"main": "red/red.js",
"scripts": {
"start": "node red.js",
"test": "grunt",
"build": "grunt build"
},
"bin": {
"node-red": "./red.js",
"node-red-pi": "bin/node-red-pi"
},
"contributors": [
{
"name": "Nick O'Leary"
"name": "node-red",
"version": "0.18.7",
"description": "A visual tool for wiring the Internet of Things",
"homepage": "http://nodered.org",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/node-red/node-red.git"
},
{
"name": "Dave Conway-Jones"
"main": "red/red.js",
"scripts": {
"start": "node red.js",
"test": "grunt",
"build": "grunt build",
"docs": "grunt docs"
},
"bin": {
"node-red": "./red.js",
"node-red-pi": "bin/node-red-pi"
},
"contributors": [
{
"name": "Nick O'Leary"
},
{
"name": "Dave Conway-Jones"
}
],
"keywords": [
"editor",
"messaging",
"iot",
"flow"
],
"dependencies": {
"ajv": "6.5.2",
"basic-auth": "2.0.0",
"bcryptjs": "2.4.3",
"body-parser": "1.18.3",
"cheerio": "0.22.0",
"clone": "2.1.1",
"cookie": "0.3.1",
"cookie-parser": "1.4.3",
"cors": "2.8.4",
"cron": "1.3.0",
"denque": "1.3.0",
"express": "4.16.3",
"express-session": "1.15.6",
"fs-extra": "5.0.0",
"fs.notify": "0.0.4",
"hash-sum": "1.0.2",
"i18next": "^11.4.0",
"is-utf8": "0.2.1",
"js-yaml": "3.12.0",
"json-stringify-safe": "5.0.1",
"jsonata": "1.5.4",
"media-typer": "0.3.0",
"memorystore": "1.6.0",
"mime": "1.4.1",
"mqtt": "2.18.3",
"multer": "1.3.1",
"mustache": "2.3.0",
"node-red-node-email": "0.1.*",
"node-red-node-feedparser": "^0.1.12",
"node-red-node-rbe": "0.2.*",
"node-red-node-twitter": "*",
"nopt": "4.0.1",
"oauth2orize": "1.11.0",
"on-headers": "1.0.1",
"passport": "0.4.0",
"passport-http-bearer": "1.0.1",
"passport-oauth2-client-password": "0.1.2",
"raw-body": "2.3.3",
"request": "2.87.0",
"semver": "5.5.0",
"sentiment": "2.1.0",
"uglify-js": "3.4.5",
"when": "3.7.8",
"ws": "1.1.5",
"xml2js": "0.4.19"
},
"optionalDependencies": {
"bcrypt": "~2.0.0"
},
"devDependencies": {
"chromedriver": "^2.40.0",
"grunt": "~1.0.3",
"grunt-chmod": "~1.1.1",
"grunt-cli": "~1.2.0",
"grunt-concurrent": "~2.3.1",
"grunt-contrib-clean": "~1.1.0",
"grunt-contrib-compress": "~1.4.0",
"grunt-contrib-concat": "~1.0.1",
"grunt-contrib-copy": "~1.0.0",
"grunt-contrib-jshint": "~1.1.0",
"grunt-contrib-uglify": "~3.3.0",
"grunt-contrib-watch": "~1.1.0",
"grunt-jsdoc": "^2.2.1",
"grunt-jsdoc-to-markdown": "^4.0.0",
"grunt-jsonlint": "~1.1.0",
"grunt-mocha-istanbul": "5.0.2",
"grunt-nodemon": "~0.4.2",
"grunt-sass": "~2.0.0",
"grunt-simple-mocha": "~0.4.1",
"grunt-webdriver": "^2.0.3",
"http-proxy": "^1.16.2",
"ink-docstrap": "^1.3.2",
"istanbul": "0.4.5",
"mocha": "^5.2.0",
"should": "^8.4.0",
"sinon": "1.17.7",
"stoppable": "^1.0.6",
"supertest": "3.1.0",
"wdio-chromedriver-service": "^0.1.3",
"wdio-mocha-framework": "^0.6.2",
"wdio-spec-reporter": "^0.1.5",
"webdriverio": "^4.13.1",
"node-red-node-test-helper": "^0.1.7"
},
"engines": {
"node": ">=4"
}
],
"keywords": [
"editor",
"messaging",
"iot",
"flow"
],
"dependencies": {
"ajv": "6.5.2",
"basic-auth": "2.0.0",
"bcryptjs": "2.4.3",
"body-parser": "1.18.3",
"cheerio": "0.22.0",
"clone": "2.1.1",
"cookie": "0.3.1",
"cookie-parser": "1.4.3",
"cors": "2.8.4",
"cron": "1.3.0",
"denque": "1.3.0",
"express": "4.16.3",
"express-session": "1.15.6",
"fs-extra": "5.0.0",
"fs.notify": "0.0.4",
"hash-sum": "1.0.2",
"i18next": "^11.4.0",
"is-utf8": "0.2.1",
"js-yaml": "3.12.0",
"json-stringify-safe": "5.0.1",
"jsonata": "1.5.4",
"media-typer": "0.3.0",
"memorystore": "1.6.0",
"mqtt": "2.18.3",
"multer": "1.3.1",
"mustache": "2.3.0",
"node-red-node-email": "0.1.*",
"node-red-node-feedparser": "^0.1.12",
"node-red-node-rbe": "0.2.*",
"node-red-node-twitter": "*",
"nopt": "4.0.1",
"oauth2orize": "1.11.0",
"on-headers": "1.0.1",
"passport": "0.4.0",
"passport-http-bearer": "1.0.1",
"passport-oauth2-client-password": "0.1.2",
"raw-body": "2.3.3",
"request": "2.87.0",
"semver": "5.5.0",
"sentiment": "2.1.0",
"uglify-js": "3.4.5",
"when": "3.7.8",
"ws": "1.1.5",
"xml2js": "0.4.19"
},
"optionalDependencies": {
"bcrypt": "~2.0.0"
},
"devDependencies": {
"chromedriver": "^2.40.0",
"grunt": "~1.0.3",
"grunt-chmod": "~1.1.1",
"grunt-cli": "~1.2.0",
"grunt-concurrent": "~2.3.1",
"grunt-contrib-clean": "~1.1.0",
"grunt-contrib-compress": "~1.4.0",
"grunt-contrib-concat": "~1.0.1",
"grunt-contrib-copy": "~1.0.0",
"grunt-contrib-jshint": "~1.1.0",
"grunt-contrib-uglify": "~3.3.0",
"grunt-contrib-watch": "~1.1.0",
"grunt-jsonlint": "~1.1.0",
"grunt-mocha-istanbul": "5.0.2",
"grunt-nodemon": "~0.4.2",
"grunt-sass": "~2.0.0",
"grunt-simple-mocha": "~0.4.1",
"grunt-webdriver": "^2.0.3",
"http-proxy": "^1.16.2",
"istanbul": "0.4.5",
"mocha": "^5.2.0",
"should": "^8.4.0",
"sinon": "1.17.7",
"stoppable": "^1.0.6",
"supertest": "3.1.0",
"wdio-chromedriver-service": "^0.1.3",
"wdio-mocha-framework": "^0.6.2",
"wdio-spec-reporter": "^0.1.5",
"webdriverio": "^4.13.1",
"node-red-node-test-helper": "^0.1.7"
},
"engines": {
"node": ">=4"
}
}

View File

@@ -14,120 +14,28 @@
* limitations under the License.
**/
var log;
var redNodes;
var util;
var settings;
var apiUtils = require("../util");
var runtimeAPI;
function exportContextStore(scope,ctx, store, result, callback) {
ctx.keys(store,function(err, keys) {
if (err) {
return callback(err);
}
result[store] = {};
var c = keys.length;
if (c === 0) {
callback(null);
} else {
keys.forEach(function(key) {
ctx.get(key,store,function(err, v) {
if (err) {
return callback(err);
}
if (scope !== 'global' ||
store === redNodes.listContextStores().default ||
!settings.hasOwnProperty("functionGlobalContext") ||
!settings.functionGlobalContext.hasOwnProperty(key) ||
settings.functionGlobalContext[key] !== v) {
result[store][key] = util.encodeObject({msg:v});
}
c--;
if (c === 0) {
callback(null);
}
});
});
}
});
}
module.exports = {
init: function(runtime) {
redNodes = runtime.nodes;
log = runtime.log;
util = runtime.util;
settings = runtime.settings;
init: function(_runtimeAPI) {
runtimeAPI = _runtimeAPI;
},
get: function(req,res) {
var scope = req.params.scope;
var id = req.params.id;
var key = req.params[0];
var availableStores = redNodes.listContextStores();
//{ default: 'default', stores: [ 'default', 'file' ] }
var store = req.query['store'];
if (store && availableStores.stores.indexOf(store) === -1) {
return res.status(404).end();
}
var ctx;
if (scope === 'global') {
ctx = redNodes.getContext('global');
} else if (scope === 'flow') {
ctx = redNodes.getContext(id);
} else if (scope === 'node') {
var node = redNodes.getNode(id);
if (node) {
ctx = node.context();
}
}
if (ctx) {
if (key) {
store = store || availableStores.default;
ctx.get(key,store,function(err, v) {
var encoded = util.encodeObject({msg:v});
if (store !== availableStores.default) {
encoded.store = store;
}
res.json(encoded);
});
return;
} else {
var stores;
if (!store) {
stores = availableStores.stores;
} else {
stores = [store];
}
var result = {};
var c = stores.length;
var errorReported = false;
stores.forEach(function(store) {
exportContextStore(scope,ctx,store,result,function(err) {
if (err) {
// TODO: proper error reporting
if (!errorReported) {
errorReported = true;
res.end(400);
}
return;
}
c--;
if (c === 0) {
if (!errorReported) {
if (stores.length > 1 && scope === 'global') {
}
res.json(result);
}
}
});
})
}
} else {
res.json({});
var opts = {
user: req.user,
scope: req.params.scope,
id: req.params.id,
key: req.params[0],
store: req.query['store']
}
runtimeAPI.context.getValue(opts).then(function(result) {
res.json(result);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
}
}

View File

@@ -14,72 +14,56 @@
* limitations under the License.
**/
var log;
var redNodes;
var runtimeAPI;
var apiUtils = require("../util");
module.exports = {
init: function(runtime) {
redNodes = runtime.nodes;
log = runtime.log;
init: function(_runtimeAPI) {
runtimeAPI = _runtimeAPI;
},
get: function(req,res) {
var id = req.params.id;
var flow = redNodes.getFlow(id);
if (flow) {
log.audit({event: "flow.get",id:id},req);
res.json(flow);
} else {
log.audit({event: "flow.get",id:id,error:"not_found"},req);
res.status(404).end();
var opts = {
user: req.user,
id: req.params.id
}
runtimeAPI.flows.getFlow(opts).then(function(result) {
return res.json(result);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
},
post: function(req,res) {
var flow = req.body;
redNodes.addFlow(flow).then(function(id) {
log.audit({event: "flow.add",id:id},req);
res.json({id:id});
var opts = {
user: req.user,
flow: req.body
}
runtimeAPI.flows.addFlow(opts).then(function(id) {
return res.json({id:id});
}).catch(function(err) {
log.audit({event: "flow.add",error:err.code||"unexpected_error",message:err.toString()},req);
res.status(400).json({error:err.code||"unexpected_error", message:err.toString()});
apiUtils.rejectHandler(req,res,err);
})
},
put: function(req,res) {
var id = req.params.id;
var flow = req.body;
try {
redNodes.updateFlow(id,flow).then(function() {
log.audit({event: "flow.update",id:id},req);
res.json({id:id});
}).catch(function(err) {
log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()},req);
res.status(400).json({error:err.code||"unexpected_error", message:err.toString()});
})
} catch(err) {
if (err.code === 404) {
log.audit({event: "flow.update",id:id,error:"not_found"},req);
res.status(404).end();
} else {
log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()},req);
res.status(400).json({error:err.code||"unexpected_error", message:err.toString()});
}
var opts = {
user: req.user,
id: req.params.id,
flow: req.body
}
runtimeAPI.flows.updateFlow(opts).then(function(id) {
return res.json({id:id});
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
},
delete: function(req,res) {
var id = req.params.id;
try {
redNodes.removeFlow(id).then(function() {
log.audit({event: "flow.remove",id:id},req);
res.status(204).end();
})
} catch(err) {
if (err.code === 404) {
log.audit({event: "flow.remove",id:id,error:"not_found"},req);
res.status(404).end();
} else {
log.audit({event: "flow.remove",id:id,error:err.code||"unexpected_error",message:err.toString()},req);
res.status(400).json({error:err.code||"unexpected_error", message:err.toString()});
}
var opts = {
user: req.user,
id: req.params.id
}
runtimeAPI.flows.deleteFlow(opts).then(function() {
res.status(204).end();
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
}
}

View File

@@ -14,72 +14,57 @@
* limitations under the License.
**/
var log;
var redNodes;
var runtimeAPI;
var apiUtils = require("../util");
module.exports = {
init: function(runtime) {
redNodes = runtime.nodes;
log = runtime.log;
init: function(_runtimeAPI) {
runtimeAPI = _runtimeAPI;
},
get: function(req,res) {
var version = req.get("Node-RED-API-Version")||"v1";
if (version === "v1") {
log.audit({event: "flows.get",version:"v1"},req);
res.json(redNodes.getFlows().flows);
} else if (version === "v2") {
log.audit({event: "flows.get",version:"v2"},req);
res.json(redNodes.getFlows());
} else {
log.audit({event: "flows.get",version:version,error:"invalid_api_version"},req);
res.status(400).json({code:"invalid_api_version", message:"Invalid API Version requested"});
if (!/^v[12]$/.test(version)) {
return res.status(400).json({code:"invalid_api_version", message:"Invalid API Version requested"});
}
var opts = {
user: req.user
}
runtimeAPI.flows.getFlows(opts).then(function(result) {
if (version === "v1") {
res.json(result.flows);
} else if (version === "v2") {
res.json(result);
}
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
},
post: function(req,res) {
var version = req.get("Node-RED-API-Version")||"v1";
if (!/^v[12]$/.test(version)) {
log.audit({event: "flows.set",version:version,error:"invalid_api_version"},req);
res.status(400).json({code:"invalid_api_version", message:"Invalid API Version requested"});
return;
return res.status(400).json({code:"invalid_api_version", message:"Invalid API Version requested"});
}
var flows = req.body;
var deploymentType = req.get("Node-RED-Deployment-Type")||"full";
log.audit({event: "flows.set",type:deploymentType,version:version},req);
if (deploymentType === 'reload') {
redNodes.loadFlows().then(function(flowId) {
if (version === "v1") {
res.status(204).end();
} else {
res.json({rev:flowId});
}
}).catch(function(err) {
log.warn(log._("api.flows.error-reload",{message:err.message}));
log.warn(err.stack);
res.status(500).json({error:"unexpected_error", message:err.message});
});
} else {
var flowConfig = flows;
if (version === "v2") {
flowConfig = flows.flows;
if (flows.hasOwnProperty('rev')) {
var currentVersion = redNodes.getFlows().rev;
if (currentVersion !== flows.rev) {
//TODO: log warning
return res.status(409).json({code:"version_mismatch"});
}
}
var opts = {
user: req.user,
deploymentType: req.get("Node-RED-Deployment-Type")||"full"
}
if (opts.deploymentType !== 'reload') {
if (version === "v1") {
opts.flows = {flows: req.body}
} else {
opts.flows = req.body;
}
redNodes.setFlows(flowConfig,deploymentType).then(function(flowId) {
if (version === "v1") {
res.status(204).end();
} else if (version === "v2") {
res.json({rev:flowId});
}
}).catch(function(err) {
log.warn(log._("api.flows.error-save",{message:err.message}));
log.warn(err.stack);
res.status(500).json({error:err.code || "unexpected_error", message:err.message});
});
}
runtimeAPI.flows.setFlows(opts).then(function(result) {
if (version === "v1") {
res.status(204).end();
} else {
res.json(result);
}
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
}
}

View File

@@ -25,11 +25,11 @@ var auth = require("../auth");
var apiUtil = require("../util");
module.exports = {
init: function(runtime) {
flows.init(runtime);
flow.init(runtime);
nodes.init(runtime);
context.init(runtime);
init: function(runtimeAPI) {
flows.init(runtimeAPI);
flow.init(runtimeAPI);
nodes.init(runtimeAPI);
context.init(runtimeAPI);
var needsPermission = auth.needsPermission;
@@ -48,6 +48,8 @@ module.exports = {
// Nodes
adminApp.get("/nodes",needsPermission("nodes.read"),nodes.getAll,apiUtil.errorHandler);
adminApp.post("/nodes",needsPermission("nodes.write"),nodes.post,apiUtil.errorHandler);
adminApp.get(/\/nodes\/messages/,needsPermission("nodes.read"),nodes.getModuleCatalogs,apiUtil.errorHandler);
adminApp.get(/\/nodes\/((@[^\/]+\/)?[^\/]+\/[^\/]+)\/messages/,needsPermission("nodes.read"),nodes.getModuleCatalog,apiUtil.errorHandler);
adminApp.get(/\/nodes\/((@[^\/]+\/)?[^\/]+)$/,needsPermission("nodes.read"),nodes.getModule,apiUtil.errorHandler);
adminApp.put(/\/nodes\/((@[^\/]+\/)?[^\/]+)$/,needsPermission("nodes.write"),nodes.putModule,apiUtil.errorHandler);
adminApp.delete(/\/nodes\/((@[^\/]+\/)?[^\/]+)$/,needsPermission("nodes.write"),nodes.delete,apiUtil.errorHandler);

View File

@@ -14,230 +14,160 @@
* limitations under the License.
**/
var when = require("when");
var apiUtils = require("../util");
var redNodes;
var log;
var settings;
var runtimeAPI;
module.exports = {
init: function(runtime) {
redNodes = runtime.nodes;
log = runtime.log;
settings = runtime.settings;
init: function(_runtimeAPI) {
runtimeAPI = _runtimeAPI;
},
getAll: function(req,res) {
var opts = {
user: req.user
}
if (req.get("accept") == "application/json") {
log.audit({event: "nodes.list.get"},req);
res.json(redNodes.getNodeList());
runtimeAPI.nodes.getNodeList(opts).then(function(list) {
res.json(list);
})
} else {
var lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages());
log.audit({event: "nodes.configs.get"},req);
res.send(redNodes.getNodeConfigs(lang));
opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages());
runtimeAPI.nodes.getNodeConfigs(opts).then(function(configs) {
res.send(configs);
})
}
},
post: function(req,res) {
if (!settings.available()) {
log.audit({event: "nodes.install",error:"settings_unavailable"},req);
res.status(400).json({error:"settings_unavailable", message:"Settings unavailable"});
return;
var opts = {
user: req.user,
module: req.body.module,
version: req.body.version
}
var node = req.body;
var promise;
var isUpgrade = false;
if (node.module) {
var module = redNodes.getModuleInfo(node.module);
if (module) {
if (!node.version || module.version === node.version) {
log.audit({event: "nodes.install",module:node.module, version:node.version, error:"module_already_loaded"},req);
res.status(400).json({error:"module_already_loaded", message:"Module already loaded"});
return;
}
if (!module.local) {
log.audit({event: "nodes.install",module:node.module, version:node.version, error:"module_not_local"},req);
res.status(400).json({error:"module_not_local", message:"Module not locally installed"});
return;
}
isUpgrade = true;
}
promise = redNodes.installModule(node.module,node.version);
} else {
log.audit({event: "nodes.install",module:node.module,error:"invalid_request"},req);
res.status(400).json({error:"invalid_request", message:"Invalid request"});
return;
}
promise.then(function(info) {
if (node.module) {
log.audit({event: "nodes.install",module:node.module,version:node.version},req);
res.json(info);
}
runtimeAPI.nodes.addModule(opts).then(function(info) {
res.json(info);
}).catch(function(err) {
if (err.code === 404) {
log.audit({event: "nodes.install",module:node.module,version:node.version,error:"not_found"},req);
res.status(404).end();
} else if (err.code) {
log.audit({event: "nodes.install",module:node.module,version:node.version,error:err.code},req);
res.status(400).json({error:err.code, message:err.message});
} else {
log.audit({event: "nodes.install",module:node.module,version:node.version,error:err.code||"unexpected_error",message:err.toString()},req);
res.status(400).json({error:err.code||"unexpected_error", message:err.toString()});
}
});
apiUtils.rejectHandler(req,res,err);
})
},
delete: function(req,res) {
if (!settings.available()) {
log.audit({event: "nodes.remove",error:"settings_unavailable"},req);
res.status(400).json({error:"settings_unavailable", message:"Settings unavailable"});
return;
}
var mod = req.params[0];
try {
var promise = null;
var module = redNodes.getModuleInfo(mod);
if (!module) {
log.audit({event: "nodes.remove",module:mod,error:"not_found"},req);
res.status(404).end();
return;
} else {
promise = redNodes.uninstallModule(mod);
}
promise.then(function(list) {
log.audit({event: "nodes.remove",module:mod},req);
res.status(204).end();
}).catch(function(err) {
log.audit({event: "nodes.remove",module:mod,error:err.code||"unexpected_error",message:err.toString()},req);
res.status(400).json({error:err.code||"unexpected_error", message:err.toString()});
});
} catch(err) {
log.audit({event: "nodes.remove",module:mod,error:err.code||"unexpected_error",message:err.toString()},req);
res.status(400).json({error:err.code||"unexpected_error", message:err.toString()});
var opts = {
user: req.user,
module: req.params[0]
}
runtimeAPI.nodes.removeModule(opts).then(function() {
res.status(204).end();
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
},
getSet: function(req,res) {
var id = req.params[0] + "/" + req.params[2];
var result = null;
var opts = {
user: req.user,
id: req.params[0] + "/" + req.params[2]
}
if (req.get("accept") === "application/json") {
result = redNodes.getNodeInfo(id);
if (result) {
log.audit({event: "nodes.info.get",id:id},req);
delete result.loaded;
runtimeAPI.nodes.getNodeInfo(opts).then(function(result) {
res.send(result);
} else {
log.audit({event: "nodes.info.get",id:id,error:"not_found"},req);
res.status(404).end();
}
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
} else {
var lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages());
result = redNodes.getNodeConfig(id,lang);
if (result) {
log.audit({event: "nodes.config.get",id:id},req);
res.send(result);
} else {
log.audit({event: "nodes.config.get",id:id,error:"not_found"},req);
res.status(404).end();
}
opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages());
runtimeAPI.nodes.getNodeConfig(opts).then(function(result) {
return res.send(result);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
}
},
getModule: function(req,res) {
var module = req.params[0];
var result = redNodes.getModuleInfo(module);
if (result) {
log.audit({event: "nodes.module.get",module:module},req);
res.json(result);
} else {
log.audit({event: "nodes.module.get",module:module,error:"not_found"},req);
res.status(404).end();
var opts = {
user: req.user,
module: req.params[0]
}
runtimeAPI.nodes.getModuleInfo(opts).then(function(result) {
res.send(result);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
},
putSet: function(req,res) {
if (!settings.available()) {
log.audit({event: "nodes.info.set",error:"settings_unavailable"},req);
res.status(400).json({error:"settings_unavailable", message:"Settings unavailable"});
return;
}
var body = req.body;
if (!body.hasOwnProperty("enabled")) {
log.audit({event: "nodes.info.set",error:"invalid_request"},req);
res.status(400).json({error:"invalid_request", message:"Invalid request"});
// log.audit({event: "nodes.module.set",error:"invalid_request"},req);
res.status(400).json({code:"invalid_request", message:"Invalid request"});
return;
}
var id = req.params[0] + "/" + req.params[2];
try {
var node = redNodes.getNodeInfo(id);
var info;
if (!node) {
log.audit({event: "nodes.info.set",id:id,error:"not_found"},req);
res.status(404).end();
} else {
delete node.loaded;
putNode(node, body.enabled).then(function(result) {
log.audit({event: "nodes.info.set",id:id,enabled:body.enabled},req);
res.json(result);
});
}
} catch(err) {
log.audit({event: "nodes.info.set",id:id,enabled:body.enabled,error:err.code||"unexpected_error",message:err.toString()},req);
res.status(400).json({error:err.code||"unexpected_error", message:err.toString()});
var opts = {
user: req.user,
id: req.params[0] + "/" + req.params[2],
enabled: body.enabled
}
runtimeAPI.nodes.setNodeSetState(opts).then(function(result) {
res.send(result);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
},
putModule: function(req,res) {
if (!settings.available()) {
log.audit({event: "nodes.module.set",error:"settings_unavailable"},req);
res.status(400).json({error:"settings_unavailable", message:"Settings unavailable"});
return;
}
var body = req.body;
if (!body.hasOwnProperty("enabled")) {
log.audit({event: "nodes.module.set",error:"invalid_request"},req);
res.status(400).json({error:"invalid_request", message:"Invalid request"});
// log.audit({event: "nodes.module.set",error:"invalid_request"},req);
res.status(400).json({code:"invalid_request", message:"Invalid request"});
return;
}
var mod = req.params[0];
try {
var module = redNodes.getModuleInfo(mod);
if (!module) {
log.audit({event: "nodes.module.set",module:mod,error:"not_found"},req);
return res.status(404).end();
}
var nodes = module.nodes;
var promises = [];
for (var i = 0; i < nodes.length; ++i) {
promises.push(putNode(nodes[i],body.enabled));
}
when.settle(promises).then(function() {
res.json(redNodes.getModuleInfo(mod));
});
} catch(err) {
log.audit({event: "nodes.module.set",module:mod,enabled:body.enabled,error:err.code||"unexpected_error",message:err.toString()},req);
res.status(400).json({error:err.code||"unexpected_error", message:err.toString()});
var opts = {
user: req.user,
module: req.params[0],
enabled: body.enabled
}
runtimeAPI.nodes.setModuleState(opts).then(function(result) {
res.send(result);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
},
getModuleCatalog: function(req,res) {
var opts = {
user: req.user,
module: req.params[0],
lang: req.query.lng
}
runtimeAPI.nodes.getModuleCatalog(opts).then(function(result) {
res.json(result);
}).catch(function(err) {
console.log(err.stack);
apiUtils.rejectHandler(req,res,err);
})
},
getModuleCatalogs: function(req,res) {
var opts = {
user: req.user,
lang: req.query.lng
}
runtimeAPI.nodes.getModuleCatalogs(opts).then(function(result) {
res.json(result);
}).catch(function(err) {
console.log(err.stack);
apiUtils.rejectHandler(req,res,err);
})
},
getIcons: function(req,res) {
log.audit({event: "nodes.icons.get"},req);
res.json(redNodes.getNodeIcons());
var opts = {
user: req.user
}
runtimeAPI.nodes.getIconList(opts).then(function(list) {
res.json(list);
});
}
};
function putNode(node, enabled) {
var info;
var promise;
if (!node.err && node.enabled === enabled) {
promise = when.resolve(node);
} else {
if (enabled) {
promise = redNodes.enableNode(node.id);
} else {
promise = redNodes.disableNode(node.id);
}
}
return promise;
}

View File

@@ -25,7 +25,7 @@ var permissions = require("./permissions");
var theme = require("../editor/theme");
var settings = null;
var log = null
var log = require("../../util").log; // TODO: separate module
passport.use(strategies.bearerStrategy.BearerStrategy);
@@ -36,13 +36,11 @@ var server = oauth2orize.createServer();
server.exchange(oauth2orize.exchange.password(strategies.passwordTokenExchange));
function init(runtime) {
settings = runtime.settings;
log = runtime.log;
function init(_settings,storage) {
settings = _settings;
if (settings.adminAuth) {
Users.init(settings.adminAuth);
Tokens.init(settings.adminAuth,runtime.storage);
strategies.init(runtime);
Tokens.init(settings.adminAuth,storage);
}
}

View File

@@ -26,7 +26,7 @@ var Users = require("./users");
var Clients = require("./clients");
var permissions = require("./permissions");
var log;
var log = require("../../util").log; // TODO: separate module
var bearerStrategy = function (accessToken, done) {
// is this a valid token?
@@ -124,9 +124,6 @@ AnonymousStrategy.prototype.authenticate = function(req) {
}
module.exports = {
init: function(runtime) {
log = runtime.log;
},
bearerStrategy: bearerStrategy,
clientPasswordStrategy: clientPasswordStrategy,
passwordTokenExchange: passwordTokenExchange,

View File

@@ -15,45 +15,168 @@
**/
var ws = require("ws");
var log;
var log = require("../../util").log; // TODO: separate module
var Tokens;
var Users;
var Permissions;
var server;
var settings;
var runtimeAPI;
var wsServer;
var pendingConnections = [];
var activeConnections = [];
var anonymousUser;
var retained = {};
var heartbeatTimer;
var lastSentTime;
function handleStatus(event) {
publish("status/"+event.id,event.status,true);
}
function handleRuntimeEvent(event) {
log.trace("runtime event: "+JSON.stringify(event));
publish("notification/"+event.id,event.payload||{},event.retain);
}
function init(_server,runtime) {
function init(_server,_settings,_runtimeAPI) {
server = _server;
settings = runtime.settings;
log = runtime.log;
settings = _settings;
runtimeAPI = _runtimeAPI;
Tokens = require("../auth/tokens");
Users = require("../auth/users");
Permissions = require("../auth/permissions");
runtime.events.removeListener("node-status",handleStatus);
runtime.events.on("node-status",handleStatus);
}
runtime.events.removeListener("runtime-event",handleRuntimeEvent);
runtime.events.on("runtime-event",handleRuntimeEvent);
function generateSession(length) {
var c = "ABCDEFGHIJKLMNOPQRSTUZWXYZabcdefghijklmnopqrstuvwxyz1234567890";
var token = [];
for (var i=0;i<length;i++) {
token.push(c[Math.floor(Math.random()*c.length)]);
}
return token.join("");
}
function CommsConnection(ws) {
this.session = generateSession(32);
this.ws = ws;
this.stack = [];
this.user = null;
this.lastSentTime = 0;
var self = this;
log.audit({event: "comms.open"});
log.trace("comms.open "+self.session);
var pendingAuth = (settings.adminAuth != null);
if (!pendingAuth) {
addActiveConnection(self);
}
ws.on('close',function() {
log.audit({event: "comms.close",user:self.user, session: self.session});
log.trace("comms.close "+self.session);
removeActiveConnection(self);
});
ws.on('message', function(data,flags) {
var msg = null;
try {
msg = JSON.parse(data);
} catch(err) {
log.trace("comms received malformed message : "+err.toString());
return;
}
if (!pendingAuth) {
if (msg.subscribe) {
self.subscribe(msg.subscribe);
// handleRemoteSubscription(ws,msg.subscribe);
}
} else {
var completeConnection = function(userScope,sendAck) {
try {
if (!userScope || !Permissions.hasPermission(userScope,"status.read")) {
ws.send(JSON.stringify({auth:"fail"}));
ws.close();
} else {
pendingAuth = false;
addActiveConnection(self);
if (sendAck) {
ws.send(JSON.stringify({auth:"ok"}));
}
}
} catch(err) {
console.log(err.stack);
// Just in case the socket closes before we attempt
// to send anything.
}
}
if (msg.auth) {
Tokens.get(msg.auth).then(function(client) {
if (client) {
Users.get(client.user).then(function(user) {
if (user) {
self.user = user;
log.audit({event: "comms.auth",user:self.user});
completeConnection(client.scope,true);
} else {
log.audit({event: "comms.auth.fail"});
completeConnection(null,false);
}
});
} else {
log.audit({event: "comms.auth.fail"});
completeConnection(null,false);
}
});
} else {
if (anonymousUser) {
log.audit({event: "comms.auth",user:anonymousUser});
self.user = anonymousUser;
completeConnection(anonymousUser.permissions,false);
//TODO: duplicated code - pull non-auth message handling out
if (msg.subscribe) {
self.subscribe(msg.subscribe);
}
} else {
log.audit({event: "comms.auth.fail"});
completeConnection(null,false);
}
}
}
});
ws.on('error', function(err) {
log.warn(log._("comms.error",{message:err.toString()}));
});
}
CommsConnection.prototype.send = function(topic,data) {
var self = this;
if (topic && data) {
this.stack.push({topic:topic,data:data});
}
if (!this._xmitTimer) {
this._xmitTimer = setTimeout(function() {
try {
self.ws.send(JSON.stringify(self.stack));
self.lastSentTime = Date.now();
} catch(err) {
removeActiveConnection(self);
log.warn(log._("comms.error-send",{message:err.toString()}));
}
delete self._xmitTimer;
self.stack = [];
},50);
}
}
CommsConnection.prototype.subscribe = function(topic) {
runtimeAPI.comms.subscribe({
user: this.user,
client: this,
topic: topic
})
}
function start() {
var Tokens = require("../auth/tokens");
var Users = require("../auth/users");
var Permissions = require("../auth/permissions");
if (!settings.disableEditor) {
Users.default().then(function(anonymousUser) {
Users.default().then(function(_anonymousUser) {
anonymousUser = _anonymousUser;
var webSocketKeepAliveTime = settings.webSocketKeepAliveTime || 15000;
var path = settings.httpAdminRoot || "/";
path = (path.slice(0,1) != "/" ? "/":"") + path + (path.slice(-1) == "/" ? "":"/") + "comms";
@@ -68,90 +191,10 @@ function start() {
});
wsServer.on('connection',function(ws) {
log.audit({event: "comms.open"});
var pendingAuth = (settings.adminAuth != null);
ws._nr_stack = [];
ws._nr_ok2tx = true;
if (!pendingAuth) {
activeConnections.push(ws);
} else {
pendingConnections.push(ws);
}
ws.on('close',function() {
log.audit({event: "comms.close",user:ws.user});
removeActiveConnection(ws);
removePendingConnection(ws);
});
ws.on('message', function(data,flags) {
var msg = null;
try {
msg = JSON.parse(data);
} catch(err) {
log.trace("comms received malformed message : "+err.toString());
return;
}
if (!pendingAuth) {
if (msg.subscribe) {
handleRemoteSubscription(ws,msg.subscribe);
}
} else {
var completeConnection = function(userScope,sendAck) {
try {
if (!userScope || !Permissions.hasPermission(userScope,"status.read")) {
ws.send(JSON.stringify({auth:"fail"}));
ws.close();
} else {
pendingAuth = false;
removePendingConnection(ws);
activeConnections.push(ws);
if (sendAck) {
ws.send(JSON.stringify({auth:"ok"}));
}
}
} catch(err) {
// Just in case the socket closes before we attempt
// to send anything.
}
}
if (msg.auth) {
Tokens.get(msg.auth).then(function(client) {
if (client) {
Users.get(client.user).then(function(user) {
if (user) {
ws.user = user;
log.audit({event: "comms.auth",user:ws.user});
completeConnection(client.scope,true);
} else {
log.audit({event: "comms.auth.fail"});
completeConnection(null,false);
}
});
} else {
log.audit({event: "comms.auth.fail"});
completeConnection(null,false);
}
});
} else {
if (anonymousUser) {
log.audit({event: "comms.auth",user:anonymousUser});
completeConnection(anonymousUser.permissions,false);
//TODO: duplicated code - pull non-auth message handling out
if (msg.subscribe) {
handleRemoteSubscription(ws,msg.subscribe);
}
} else {
log.audit({event: "comms.auth.fail"});
completeConnection(null,false);
}
}
}
});
ws.on('error', function(err) {
log.warn(log._("comms.error",{message:err.toString()}));
});
var commsConnection = new CommsConnection(ws);
});
wsServer.on('error', function(err) {
log.warn(log._("comms.error-server",{message:err.toString()}));
});
@@ -161,7 +204,7 @@ function start() {
heartbeatTimer = setInterval(function() {
var now = Date.now();
if (now-lastSentTime > webSocketKeepAliveTime) {
publish("hb",lastSentTime);
activeConnections.forEach(connection => connection.send("hb",lastSentTime));
}
}, webSocketKeepAliveTime);
});
@@ -179,63 +222,15 @@ function stop() {
}
}
function publish(topic,data,retain) {
if (server) {
if (retain) {
retained[topic] = data;
} else {
delete retained[topic];
}
lastSentTime = Date.now();
activeConnections.forEach(function(conn) {
publishTo(conn,topic,data);
});
}
function addActiveConnection(connection) {
activeConnections.push(connection);
runtimeAPI.comms.addConnection({client: connection});
}
function publishTo(ws,topic,data) {
if (topic && data) {
ws._nr_stack.push({topic:topic,data:data});
}
if (ws._nr_ok2tx && (ws._nr_stack.length > 0)) {
ws._nr_ok2tx = false;
try {
ws.send(JSON.stringify(ws._nr_stack));
} catch(err) {
removeActiveConnection(ws);
removePendingConnection(ws);
log.warn(log._("comms.error-send",{message:err.toString()}));
}
ws._nr_stack = [];
setTimeout(function() {
ws._nr_ok2tx = true;
publishTo(ws);
}, 50); // TODO: OK so a 50mS update rate should prob not be hard-coded
}
}
function handleRemoteSubscription(ws,topic) {
var re = new RegExp("^"+topic.replace(/([\[\]\?\(\)\\\\$\^\*\.|])/g,"\\$1").replace(/\+/g,"[^/]+").replace(/\/#$/,"(\/.*)?")+"$");
for (var t in retained) {
if (re.test(t)) {
publishTo(ws,t,retained[t]);
}
}
}
function removeActiveConnection(ws) {
function removeActiveConnection(connection) {
for (var i=0;i<activeConnections.length;i++) {
if (activeConnections[i] === ws) {
if (activeConnections[i] === connection) {
activeConnections.splice(i,1);
break;
}
}
}
function removePendingConnection(ws) {
for (var i=0;i<pendingConnections.length;i++) {
if (pendingConnections[i] === ws) {
pendingConnections.splice(i,1);
runtimeAPI.comms.removeConnection({client:connection})
break;
}
}
@@ -244,6 +239,5 @@ function removePendingConnection(ws) {
module.exports = {
init:init,
start:start,
stop:stop,
publish:publish
stop:stop
}

View File

@@ -14,39 +14,23 @@
* limitations under the License.
**/
var log;
var api;
var runtimeAPI;
var apiUtils = require("../util");
module.exports = {
init: function(runtime) {
log = runtime.log;
api = runtime.nodes;
init: function(_runtimeAPI) {
runtimeAPI = _runtimeAPI
},
get: function (req, res) {
// TODO: It should verify the given node id is of the type specified -
// but that would add a dependency from this module to the
// registry module that knows about node types.
var nodeType = req.params.type;
var nodeID = req.params.id;
log.audit({event: "credentials.get",type:nodeType,id:nodeID},req);
var credentials = api.getCredentials(nodeID);
if (!credentials) {
res.json({});
return;
var opts = {
user: req.user,
type: req.params.type,
id: req.params.id
}
var definition = api.getCredentialDefinition(nodeType);
var sendCredentials = {};
for (var cred in definition) {
if (definition.hasOwnProperty(cred)) {
if (definition[cred].type == "password") {
var key = 'has_' + cred;
sendCredentials[key] = credentials[cred] != null && credentials[cred] !== '';
continue;
}
sendCredentials[cred] = credentials[cred] || '';
}
}
res.json(sendCredentials);
runtimeAPI.flows.getNodeCredentials(opts).then(function(result) {
res.json(result);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
}
}

View File

@@ -24,31 +24,35 @@ var info = require("./settings");
var auth = require("../auth");
var nodes = require("../admin/nodes"); // TODO: move /icons into here
var needsPermission;
var runtime;
var log;
var runtimeAPI;
var log = require("../../util").log; // TODO: separate module
var i18n = require("../../util").i18n; // TODO: separate module
var apiUtil = require("../util");
var ensureRuntimeStarted = function(req,res,next) {
if (!runtime.isStarted()) {
log.error("Node-RED runtime not started");
res.status(503).send("Not started");
} else {
next();
}
runtimeAPI.isStarted().then( started => {
if (!started) {
log.error("Node-RED runtime not started");
res.status(503).send("Not started");
} else {
next()
}
})
}
module.exports = {
init: function(server, _runtime) {
runtime = _runtime;
log = runtime.log;
init: function(server, settings, _runtimeAPI) {
runtimeAPI = _runtimeAPI;
needsPermission = auth.needsPermission;
var settings = runtime.settings;
if (!settings.disableEditor) {
info.init(runtime);
comms.init(server,runtime);
info.init(runtimeAPI);
comms.init(server,settings,runtimeAPI);
var ui = require("./ui");
ui.init(runtime);
ui.init(runtimeAPI);
var editorApp = express();
if (settings.requireHttps === true) {
editorApp.enable('trust proxy');
@@ -67,31 +71,31 @@ module.exports = {
editorApp.get("/icons/:scope/:module/:icon",ui.icon);
var theme = require("./theme");
theme.init(runtime);
theme.init(settings);
editorApp.use("/theme",theme.app());
editorApp.use("/",ui.editorResources);
//Projects
var projects = require("./projects");
projects.init(runtime);
projects.init(runtimeAPI);
editorApp.use("/projects",projects.app());
// Locales
var locales = require("./locales");
locales.init(runtime);
editorApp.get('/locales/nodes',locales.getAllNodes,apiUtil.errorHandler);
locales.init(runtimeAPI);
editorApp.get(/locales\/(.+)\/?$/,locales.get,apiUtil.errorHandler);
// Library
var library = require("./library");
library.init(editorApp,runtime);
editorApp.post(new RegExp("/library/flows\/(.*)"),needsPermission("library.write"),library.post,apiUtil.errorHandler);
library.init(runtimeAPI);
editorApp.get("/library/flows",needsPermission("library.read"),library.getAll,apiUtil.errorHandler);
editorApp.get(new RegExp("/library/flows\/(.*)"),needsPermission("library.read"),library.get,apiUtil.errorHandler);
editorApp.get(/library\/([^\/]+)(?:$|\/(.*))/,needsPermission("library.read"),library.getEntry);
editorApp.post(/library\/([^\/]+)\/(.*)/,needsPermission("library.write"),library.saveEntry);
// Credentials
var credentials = require("./credentials");
credentials.init(runtime);
credentials.init(runtimeAPI);
editorApp.get('/credentials/:type/:id', needsPermission("credentials.read"),credentials.get,apiUtil.errorHandler);
// Settings
@@ -100,18 +104,15 @@ module.exports = {
editorApp.get("/settings/user",needsPermission("settings.read"),info.userSettings,apiUtil.errorHandler);
// User Settings
editorApp.post("/settings/user",needsPermission("settings.write"),info.updateUserSettings,apiUtil.errorHandler);
// SSH keys
var sshkeys = require("./sshkeys");
sshkeys.init(runtime);
editorApp.use("/settings/user/keys",sshkeys.app());
editorApp.use("/settings/user/keys",needsPermission("settings.write"),info.sshkeys());
return editorApp;
}
},
start: function() {
var catalogPath = path.resolve(path.join(__dirname,"locales"));
return runtime.i18n.registerMessageCatalogs([
return i18n.registerMessageCatalogs([
{namespace: "editor", dir: catalogPath, file:"editor.json"},
{namespace: "jsonata", dir: catalogPath, file:"jsonata.json"},
{namespace: "infotips", dir: catalogPath, file:"infotips.json"}
@@ -119,7 +120,5 @@ module.exports = {
comms.start();
});
},
stop: comms.stop,
publish: comms.publish,
registerLibrary: library.register
stop: comms.stop
}

View File

@@ -13,149 +13,71 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var apiUtils = require("../util");
var fs = require('fs');
var fspath = require('path');
var when = require('when');
var redApp = null;
var storage;
var log;
var redNodes;
var needsPermission = require("../auth").needsPermission;
function createLibrary(type) {
if (redApp) {
redApp.get(new RegExp("/library/"+type+"($|\/(.*))"),needsPermission("library.read"),function(req,res) {
var path = req.params[1]||"";
storage.getLibraryEntry(type,path).then(function(result) {
log.audit({event: "library.get",type:type},req);
if (typeof result === "string") {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write(result);
res.end();
} else {
res.json(result);
}
}).catch(function(err) {
if (err) {
log.warn(log._("api.library.error-load-entry",{path:path,message:err.toString()}));
if (err.code === 'forbidden') {
log.audit({event: "library.get",type:type,error:"forbidden"},req);
res.status(403).end();
return;
}
}
log.audit({event: "library.get",type:type,error:"not_found"},req);
res.status(404).end();
});
});
redApp.post(new RegExp("/library/"+type+"\/(.*)"),needsPermission("library.write"),function(req,res) {
var path = req.params[0];
var meta = req.body;
var text = meta.text;
delete meta.text;
storage.saveLibraryEntry(type,path,meta,text).then(function() {
log.audit({event: "library.set",type:type},req);
res.status(204).end();
}).catch(function(err) {
log.warn(log._("api.library.error-save-entry",{path:path,message:err.toString()}));
if (err.code === 'forbidden') {
log.audit({event: "library.set",type:type,error:"forbidden"},req);
res.status(403).end();
return;
}
log.audit({event: "library.set",type:type,error:"unexpected_error",message:err.toString()},req);
res.status(500).json({error:"unexpected_error", message:err.toString()});
});
});
}
}
var runtimeAPI;
module.exports = {
init: function(app,runtime) {
redApp = app;
log = runtime.log;
storage = runtime.storage;
redNodes = runtime.nodes;
init: function(_runtimeAPI) {
runtimeAPI = _runtimeAPI;
},
register: createLibrary,
getAll: function(req,res) {
storage.getAllFlows().then(function(flows) {
log.audit({event: "library.get.all",type:"flow"},req);
var examples = redNodes.getNodeExampleFlows();
if (examples) {
flows.d = flows.d||{};
flows.d._examples_ = redNodes.getNodeExampleFlows();
}
res.json(flows);
var opts = {
user: req.user,
type: 'flows'
}
runtimeAPI.library.getEntries(opts).then(function(result) {
res.json(result);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
});
},
get: function(req,res) {
if (req.params[0].indexOf("_examples_/") === 0) {
var m = /^_examples_\/(@.*?\/[^\/]+|[^\/]+)\/(.*)$/.exec(req.params[0]);
if (m) {
var module = m[1];
var path = m[2];
var fullPath = redNodes.getNodeExampleFlowPath(module,path);
if (fullPath) {
try {
fs.statSync(fullPath);
log.audit({event: "library.get",type:"flow",path:req.params[0]},req);
return res.sendFile(fullPath,{
headers:{
'Content-Type': 'application/json'
}
})
} catch(err) {
console.log(err);
}
}
}
// IF we get here, we didn't find the file
log.audit({event: "library.get",type:"flow",path:req.params[0],error:"not_found"},req);
return res.status(404).end();
} else {
storage.getFlow(req.params[0]).then(function(data) {
// data is already a JSON string
log.audit({event: "library.get",type:"flow",path:req.params[0]},req);
res.set('Content-Type', 'application/json');
res.send(data);
}).catch(function(err) {
if (err) {
log.warn(log._("api.library.error-load-flow",{path:req.params[0],message:err.toString()}));
if (err.code === 'forbidden') {
log.audit({event: "library.get",type:"flow",path:req.params[0],error:"forbidden"},req);
res.status(403).end();
return;
}
}
log.audit({event: "library.get",type:"flow",path:req.params[0],error:"not_found"},req);
res.status(404).end();
});
getEntry: function(req,res) {
var opts = {
user: req.user,
type: req.params[0],
path: req.params[1]||""
}
runtimeAPI.library.getEntry(opts).then(function(result) {
if (typeof result === "string") {
if (opts.type === 'flows') {
res.writeHead(200, {'Content-Type': 'application/json'});
} else {
res.writeHead(200, {'Content-Type': 'text/plain'});
}
res.write(result);
res.end();
} else {
res.json(result);
}
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
});
},
post: function(req,res) {
// if (req.params[0].indexOf("_examples_/") === 0) {
// log.warn(log._("api.library.error-save-flow",{path:req.params[0],message:"forbidden"}));
// log.audit({event: "library.set",type:"flow",path:req.params[0],error:"forbidden"},req);
// return res.status(403).send({error:"unexpected_error", message:"forbidden"});
// }
var flow = JSON.stringify(req.body);
storage.saveFlow(req.params[0],flow).then(function() {
log.audit({event: "library.set",type:"flow",path:req.params[0]},req);
saveEntry: function(req,res) {
var opts = {
user: req.user,
type: req.params[0],
path: req.params[1]||""
}
// TODO: horrible inconsistencies between flows and all other types
if (opts.type === "flows") {
opts.meta = {};
opts.body = JSON.stringify(req.body);
} else {
opts.meta = req.body;
opts.body = opts.meta.text;
delete opts.meta.text;
}
runtimeAPI.library.saveEntry(opts).then(function(result) {
res.status(204).end();
}).catch(function(err) {
log.warn(log._("api.library.error-save-flow",{path:req.params[0],message:err.toString()}));
if (err.code === 'forbidden') {
log.audit({event: "library.set",type:"flow",path:req.params[0],error:"forbidden"},req);
res.status(403).end();
return;
}
log.audit({event: "library.set",type:"flow",path:req.params[0],error:"unexpected_error",message:err.toString()},req);
res.status(500).send({error:"unexpected_error", message:err.toString()});
apiUtils.rejectHandler(req,res,err);
});
}
}

View File

@@ -16,13 +16,14 @@
var fs = require('fs');
var path = require('path');
//var apiUtil = require('../util');
var i18n;
var redNodes;
var i18n = require("../../util").i18n; // TODO: separate module
var runtimeAPI;
module.exports = {
init: function(runtime) {
i18n = runtime.i18n;
redNodes = runtime.nodes;
init: function(_runtimeAPI) {
runtimeAPI = _runtimeAPI;
},
get: function(req,res) {
var namespace = req.params[0];
@@ -36,17 +37,5 @@ module.exports = {
res.json(catalog||{});
});
i18n.i.changeLanguage(prevLang);
},
getAllNodes: function(req,res) {
var lngs = req.query.lng;
var nodeList = redNodes.getNodeList();
var result = {};
nodeList.forEach(function(n) {
if (n.module !== "node-red") {
result[n.id] = i18n.i.getResourceBundle(lngs, n.id)||{};
}
});
res.json(result);
}
}

507
red/api/editor/projects.js Normal file
View File

@@ -0,0 +1,507 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
var express = require("express");
var apiUtils = require("../util");
var runtimeAPI;
var needsPermission = require("../auth").needsPermission;
module.exports = {
init: function(_runtimeAPI) {
runtimeAPI = _runtimeAPI;
},
app: function() {
var app = express();
app.use(function(req,res,next) {
runtimeAPI.projects.available().then(function(available) {
if (!available) {
res.status(404).end();
} else {
next();
}
})
});
// Projects
// List all projects
app.get("/", needsPermission("projects.read"), function(req,res) {
var opts = {
user: req.user
}
runtimeAPI.projects.listProjects(opts).then(function(result) {
res.json(result);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
});
});
// Create project
app.post("/", needsPermission("projects.write"), function(req,res) {
var opts = {
user: req.user,
project: req.body
}
runtimeAPI.projects.createProject(opts).then(function(result) {
res.json(result);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
});
});
// Update a project
app.put("/:id", needsPermission("projects.write"), function(req,res) {
var opts = {
user: req.user,
id: req.params.id,
project: req.body
}
if (req.body.active) {
runtimeAPI.projects.setActiveProject(opts).then(function() {
res.redirect(303,req.baseUrl + '/');
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
} else if (req.body.initialise) {
runtimeAPI.projects.initialiseProject(opts).then(function() {
res.redirect(303,req.baseUrl + '/'+ req.params.id);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
} else if (req.body.hasOwnProperty('credentialSecret') ||
req.body.hasOwnProperty('description') ||
req.body.hasOwnProperty('dependencies')||
req.body.hasOwnProperty('summary') ||
req.body.hasOwnProperty('files') ||
req.body.hasOwnProperty('git')) {
runtimeAPI.projects.updateProject(opts).then(function() {
res.redirect(303,req.baseUrl + '/'+ req.params.id);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
} else {
res.status(400).json({error:"unexpected_error", message:"invalid_request"});
}
});
// Get project metadata
app.get("/:id", needsPermission("projects.read"), function(req,res) {
var opts = {
user: req.user,
id: req.params.id
}
runtimeAPI.projects.getProject(opts).then(function(data) {
if (data) {
res.json(data);
} else {
res.status(404).end();
}
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Delete project
app.delete("/:id", needsPermission("projects.write"), function(req,res) {
var opts = {
user: req.user,
id: req.params.id
}
runtimeAPI.projects.deleteProject(opts).then(function() {
res.status(204).end();
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Get project status - files, commit counts, branch info
app.get("/:id/status", needsPermission("projects.read"), function(req,res) {
var opts = {
user: req.user,
id: req.params.id,
remote: req.query.remote
}
runtimeAPI.projects.getStatus(opts).then(function(data){
if (data) {
res.json(data);
} else {
res.status(404).end();
}
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Project file listing
app.get("/:id/files", needsPermission("projects.read"), function(req,res) {
var opts = {
user: req.user,
id: req.params.id
}
runtimeAPI.projects.getFiles(opts).then(function(data) {
res.json(data);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Get file content in a given tree (index/stage)
app.get("/:id/files/:treeish/*", needsPermission("projects.read"), function(req,res) {
var opts = {
user: req.user,
id: req.params.id,
path: req.params[0],
tree: req.params.treeish
}
runtimeAPI.projects.getFile(opts).then(function(data) {
res.json({content:data});
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Revert a file
app.delete("/:id/files/_/*", needsPermission("projects.write"), function(req,res) {
var opts = {
user: req.user,
id: req.params.id,
path: req.params[0]
}
runtimeAPI.projects.revertFile(opts).then(function() {
res.status(204).end();
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Stage a file
app.post("/:id/stage/*", needsPermission("projects.write"), function(req,res) {
var opts = {
user: req.user,
id: req.params.id,
path: req.params[0]
}
runtimeAPI.projects.stageFile(opts).then(function() {
res.redirect(303,req.baseUrl+"/"+opts.id+"/status");
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Stage multiple files
app.post("/:id/stage", needsPermission("projects.write"), function(req,res) {
var opts = {
user: req.user,
id: req.params.id,
path: req.body.files
}
runtimeAPI.projects.stageFile(opts).then(function() {
res.redirect(303,req.baseUrl+"/"+opts.id+"/status");
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Commit changes
app.post("/:id/commit", needsPermission("projects.write"), function(req,res) {
var opts = {
user: req.user,
id: req.params.id,
message: req.body.message
}
runtimeAPI.projects.commit(opts).then(function() {
res.redirect(303,req.baseUrl+"/"+opts.id+"/status");
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Unstage a file
app.delete("/:id/stage/*", needsPermission("projects.write"), function(req,res) {
var opts = {
user: req.user,
id: req.params.id,
path: req.params[0]
}
runtimeAPI.projects.unstageFile(opts).then(function() {
res.redirect(303,req.baseUrl+"/"+opts.id+"/status");
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Unstage multiple files
app.delete("/:id/stage", needsPermission("projects.write"), function(req, res) {
var opts = {
user: req.user,
id: req.params.id
}
runtimeAPI.projects.unstageFile(opts).then(function() {
res.redirect(303,req.baseUrl+"/"+opts.id+"/status");
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Get a file diff
app.get("/:id/diff/:type/*", needsPermission("projects.read"), function(req,res) {
var opts = {
user: req.user,
id: req.params.id,
path: req.params[0],
type: req.params.type
}
runtimeAPI.projects.getFileDiff(opts).then(function(data) {
res.json({
diff: data
})
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Get a list of commits
app.get("/:id/commits", needsPermission("projects.read"), function(req, res) {
var opts = {
user: req.user,
id: req.params.id,
limit: req.query.limit || 20,
before: req.query.before
}
runtimeAPI.projects.getCommits(opts).then(function(data) {
res.json(data);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Get an individual commit details
app.get("/:id/commits/:sha", needsPermission("projects.read"), function(req, res) {
var opts = {
user: req.user,
id: req.params.id,
sha: req.params.sha
}
runtimeAPI.projects.getCommit(opts).then(function(data) {
res.json({commit:data});
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Push local commits to remote
app.post("/:id/push/?*", needsPermission("projects.write"), function(req,res) {
var opts = {
user: req.user,
id: req.params.id,
remote: req.params[0],
track: req.query.u
}
runtimeAPI.projects.push(opts).then(function(data) {
res.status(204).end();
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Pull remote commits
app.post("/:id/pull/?*", needsPermission("projects.write"), function(req,res) {
var opts = {
user: req.user,
id: req.params.id,
remote: req.params[0],
track: req.query.setUpstream,
allowUnrelatedHistories: req.query.allowUnrelatedHistories
}
runtimeAPI.projects.pull(opts).then(function(data) {
res.status(204).end();
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Abort an ongoing merge
app.delete("/:id/merge", needsPermission("projects.write"), function(req, res) {
var opts = {
user: req.user,
id: req.params.id
}
runtimeAPI.projects.abortMerge(opts).then(function() {
res.status(204).end();
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Resolve a merge
app.post("/:id/resolve/*", needsPermission("projects.write"), function(req, res) {
var opts = {
user: req.user,
id: req.params.id,
path: req.params[0],
resolution: req.body.resolutions
}
runtimeAPI.projects.resolveMerge(opts).then(function() {
res.status(204).end();
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Get a list of local branches
app.get("/:id/branches", needsPermission("projects.read"), function(req, res) {
var opts = {
user: req.user,
id: req.params.id,
remote: false
}
runtimeAPI.projects.getBranches(opts).then(function(data) {
res.json(data);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Delete a local branch - ?force=true
app.delete("/:id/branches/:branchName", needsPermission("projects.write"), function(req, res) {
var opts = {
user: req.user,
id: req.params.id,
branch: req.params.branchName,
force: !!req.query.force
}
runtimeAPI.projects.deleteBranch(opts).then(function(data) {
res.status(204).end();
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Get a list of remote branches
app.get("/:id/branches/remote", needsPermission("projects.read"), function(req, res) {
var opts = {
user: req.user,
id: req.params.id,
remote: true
}
runtimeAPI.projects.getBranches(opts).then(function(data) {
res.json(data);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Get branch status - commit counts/ahead/behind
app.get("/:id/branches/remote/*/status", needsPermission("projects.read"), function(req, res) {
var opts = {
user: req.user,
id: req.params.id,
branch: req.params[0]
}
runtimeAPI.projects.getBranchStatus(opts).then(function(data) {
res.json(data);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Set the active local branch
app.post("/:id/branches", needsPermission("projects.write"), function(req, res) {
var opts = {
user: req.user,
id: req.params.id,
branch: req.body.name,
create: req.body.create
}
runtimeAPI.projects.setBranch(opts).then(function(data) {
res.json(data);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Get a list of remotes
app.get("/:id/remotes", needsPermission("projects.read"), function(req, res) {
var opts = {
user: req.user,
id: req.params.id
}
runtimeAPI.projects.getRemotes(opts).then(function(data) {
res.json(data);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Add a remote
app.post("/:id/remotes", needsPermission("projects.write"), function(req,res) {
var opts = {
user: req.user,
id: req.params.id,
remote: req.body
}
if (/^https?:\/\/[^/]+@/i.test(req.body.url)) {
res.status(400).json({error:"unexpected_error", message:"Git http url must not include username/password"});
return;
}
runtimeAPI.projects.addRemote(opts).then(function(data) {
res.redirect(303,req.baseUrl+"/"+opts.id+"/remotes");
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Delete a remote
app.delete("/:id/remotes/:remoteName", needsPermission("projects.write"), function(req, res) {
var opts = {
user: req.user,
id: req.params.id,
remote: req.params.remoteName
}
runtimeAPI.projects.removeRemote(opts).then(function(data) {
res.redirect(303,req.baseUrl+"/"+opts.id+"/remotes");
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
// Update a remote
app.put("/:id/remotes/:remoteName", needsPermission("projects.write"), function(req,res) {
var remote = req.body || {};
remote.name = req.params.remoteName;
var opts = {
user: req.user,
id: req.params.id,
remote: remote
}
runtimeAPI.projects.updateRemote(opts).then(function() {
res.status(204).end();
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
});
return app;
}
}

View File

@@ -1,559 +0,0 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
var express = require("express");
var runtime;
var needsPermission = require("../../auth").needsPermission;
module.exports = {
init: function(_runtime) {
runtime = _runtime;
},
app: function() {
var app = express();
app.use(function(req,res,next) {
if (!runtime.storage.projects) {
res.status(404).end();
} else {
next();
}
});
// Projects
// List all projects
app.get("/", needsPermission("projects.read"), function(req,res) {
runtime.storage.projects.listProjects(req.user, req.user).then(function(list) {
var active = runtime.storage.projects.getActiveProject(req.user);
var response = {
projects: list
};
if (active) {
response.active = active.name;
}
res.json(response);
}).catch(function(err) {
console.log(err.stack);
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
});
// Create project
app.post("/", needsPermission("projects.write"), function(req,res) {
runtime.storage.projects.createProject(req.user, req.body).then(function(data) {
res.json(data);
}).catch(function(err) {
console.log(err.stack);
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
});
// Update a project
app.put("/:id", needsPermission("projects.write"), function(req,res) {
//TODO: validate the payload properly
if (req.body.active) {
var currentProject = runtime.storage.projects.getActiveProject(req.user);
if (!currentProject || req.params.id !== currentProject.name) {
runtime.storage.projects.setActiveProject(req.user, req.params.id).then(function() {
res.redirect(303,req.baseUrl + '/');
}).catch(function(err) {
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
} else {
res.redirect(303,req.baseUrl + '/'+ req.params.id);
}
} else if (req.body.initialise) {
// Initialised set when creating default files for an empty repo
runtime.storage.projects.initialiseProject(req.user, req.params.id, req.body).then(function() {
res.redirect(303,req.baseUrl + '/'+ req.params.id);
}).catch(function(err) {
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
} else if (req.body.hasOwnProperty('credentialSecret') ||
req.body.hasOwnProperty('description') ||
req.body.hasOwnProperty('dependencies')||
req.body.hasOwnProperty('summary') ||
req.body.hasOwnProperty('files') ||
req.body.hasOwnProperty('git')) {
runtime.storage.projects.updateProject(req.user, req.params.id, req.body).then(function() {
res.redirect(303,req.baseUrl + '/'+ req.params.id);
}).catch(function(err) {
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
} else {
res.status(400).json({error:"unexpected_error", message:"invalid_request"});
}
});
// Get project metadata
app.get("/:id", needsPermission("projects.read"), function(req,res) {
runtime.storage.projects.getProject(req.user, req.params.id).then(function(data) {
if (data) {
res.json(data);
} else {
res.status(404).end();
}
}).catch(function(err) {
console.log(err.stack);
res.status(400).json({error:"unexpected_error", message:err.toString()});
})
});
// Delete project
app.delete("/:id", needsPermission("projects.write"), function(req,res) {
runtime.storage.projects.deleteProject(req.user, req.params.id).then(function() {
res.status(204).end();
})
.catch(function(err) {
console.log(err.stack);
res.status(400).json({error:"unexpected_error", message:err.toString()})
});
});
// Get project status - files, commit counts, branch info
app.get("/:id/status", needsPermission("projects.read"), function(req,res) {
var includeRemote = req.query.remote;
runtime.storage.projects.getStatus(req.user, req.params.id, includeRemote).then(function(data) {
if (data) {
res.json(data);
} else {
res.status(404).end();
}
}).catch(function(err) {
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
});
// Project file listing
app.get("/:id/files", needsPermission("projects.read"), function(req,res) {
runtime.storage.projects.getFiles(req.user, req.params.id).then(function(data) {
// console.log("TODO: REMOVE /:id/files as /:id/status is better!")
res.json(data);
})
.catch(function(err) {
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
});
// Get file content in a given tree (index/stage)
app.get("/:id/files/:treeish/*", needsPermission("projects.read"), function(req,res) {
var projectId = req.params.id;
var treeish = req.params.treeish;
var filePath = req.params[0];
runtime.storage.projects.getFile(req.user, projectId,filePath,treeish).then(function(data) {
res.json({content:data});
})
.catch(function(err) {
console.log(err.stack);
res.status(400).json({error:"unexpected_error", message:err.toString()});
})
});
// Revert a file
app.delete("/:id/files/_/*", needsPermission("projects.write"), function(req,res) {
var projectId = req.params.id;
var filePath = req.params[0];
runtime.storage.projects.revertFile(req.user, projectId,filePath).then(function() {
res.status(204).end();
})
.catch(function(err) {
console.log(err.stack);
res.status(400).json({error:"unexpected_error", message:err.toString()});
})
});
// Stage a file
app.post("/:id/stage/*", needsPermission("projects.write"), function(req,res) {
var projectName = req.params.id;
var file = req.params[0];
runtime.storage.projects.stageFile(req.user, projectName,file).then(function(data) {
res.redirect(303,req.baseUrl+"/"+projectName+"/status");
})
.catch(function(err) {
console.log(err.stack);
res.status(400).json({error:"unexpected_error", message:err.toString()});
})
});
// Stage multiple files
app.post("/:id/stage", needsPermission("projects.write"), function(req,res) {
var projectName = req.params.id;
var files = req.body.files;
runtime.storage.projects.stageFile(req.user, projectName,files).then(function(data) {
res.redirect(303,req.baseUrl+"/"+projectName+"/status");
})
.catch(function(err) {
console.log(err.stack);
res.status(400).json({error:"unexpected_error", message:err.toString()});
})
});
// Commit changes
app.post("/:id/commit", needsPermission("projects.write"), function(req,res) {
var projectName = req.params.id;
runtime.storage.projects.commit(req.user, projectName,req.body).then(function(data) {
res.redirect(303,req.baseUrl+"/"+projectName+"/status");
})
.catch(function(err) {
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
});
// Unstage a file
app.delete("/:id/stage/*", needsPermission("projects.write"), function(req,res) {
var projectName = req.params.id;
var file = req.params[0];
runtime.storage.projects.unstageFile(req.user, projectName,file).then(function(data) {
res.redirect(303,req.baseUrl+"/"+projectName+"/status");
})
.catch(function(err) {
console.log(err.stack);
res.status(400).json({error:"unexpected_error", message:err.toString()});
})
});
// Unstage multiple files
app.delete("/:id/stage", needsPermission("projects.write"), function(req, res) {
var projectName = req.params.id;
runtime.storage.projects.unstageFile(req.user, projectName).then(function(data) {
res.redirect(303,req.baseUrl+"/"+projectName+"/status");
})
.catch(function(err) {
console.log(err.stack);
res.status(400).json({error:"unexpected_error", message:err.toString()});
})
});
// Get a file diff
app.get("/:id/diff/:type/*", needsPermission("projects.read"), function(req,res) {
var projectName = req.params.id;
var type = req.params.type;
var file = req.params[0];
runtime.storage.projects.getFileDiff(req.user, projectName,file,type).then(function(data) {
res.json({
diff: data
})
})
.catch(function(err) {
console.log(err.stack);
res.status(400).json({error:"unexpected_error", message:err.toString()});
})
});
// Get a list of commits
app.get("/:id/commits", needsPermission("projects.read"), function(req, res) {
var projectName = req.params.id;
var options = {
limit: req.query.limit||20,
before: req.query.before
};
runtime.storage.projects.getCommits(req.user, projectName,options).then(function(data) {
res.json(data);
})
.catch(function(err) {
console.log(err.stack);
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
});
// Get an individual commit details
app.get("/:id/commits/:sha", needsPermission("projects.read"), function(req, res) {
var projectName = req.params.id;
var sha = req.params.sha;
runtime.storage.projects.getCommit(req.user, projectName,sha).then(function(data) {
res.json({commit:data});
})
.catch(function(err) {
console.log(err.stack);
res.status(400).json({error:"unexpected_error", message:err.toString()});
})
});
// Push local commits to remote
app.post("/:id/push/?*", needsPermission("projects.write"), function(req,res) {
var projectName = req.params.id;
var remoteBranchName = req.params[0]
var setRemote = req.query.u;
runtime.storage.projects.push(req.user, projectName,remoteBranchName,setRemote).then(function(data) {
res.status(204).end();
})
.catch(function(err) {
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
});
// Pull remote commits
app.post("/:id/pull/?*", needsPermission("projects.write"), function(req,res) {
var projectName = req.params.id;
var remoteBranchName = req.params[0];
var setUpstream = req.query.setUpstream;
var allowUnrelatedHistories = req.query.allowUnrelatedHistories;
runtime.storage.projects.pull(req.user, projectName,remoteBranchName,setUpstream,allowUnrelatedHistories).then(function(data) {
res.status(204).end();
})
.catch(function(err) {
console.log(err.stack);
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
});
// Abort an ongoing merge
app.delete("/:id/merge", needsPermission("projects.write"), function(req, res) {
var projectName = req.params.id;
runtime.storage.projects.abortMerge(req.user, projectName).then(function(data) {
res.status(204).end();
})
.catch(function(err) {
console.log(err.stack);
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
});
// Resolve a merge
app.post("/:id/resolve/*", needsPermission("projects.write"), function(req, res) {
var projectName = req.params.id;
var file = req.params[0];
var resolution = req.body.resolutions;
runtime.storage.projects.resolveMerge(req.user, projectName,file,resolution).then(function(data) {
res.status(204).end();
})
.catch(function(err) {
console.log(err.stack);
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
});
// Get a list of local branches
app.get("/:id/branches", needsPermission("projects.read"), function(req, res) {
var projectName = req.params.id;
runtime.storage.projects.getBranches(req.user, projectName,false).then(function(data) {
res.json(data);
})
.catch(function(err) {
console.log(err.stack);
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
});
// Delete a local branch - ?force=true
app.delete("/:id/branches/:branchName", needsPermission("projects.write"), function(req, res) {
var projectName = req.params.id;
var branchName = req.params.branchName;
var force = !!req.query.force;
runtime.storage.projects.deleteBranch(req.user, projectName, branchName, false, force).then(function(data) {
res.status(204).end();
})
.catch(function(err) {
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
});
});
// Get a list of remote branches
app.get("/:id/branches/remote", needsPermission("projects.read"), function(req, res) {
var projectName = req.params.id;
runtime.storage.projects.getBranches(req.user, projectName,true).then(function(data) {
res.json(data);
})
.catch(function(err) {
console.log(err);
if (err.code) {
res.status(400).json({error:err.code, message: err.message, remote: err.remote});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
});
// Get branch status - commit counts/ahead/behind
app.get("/:id/branches/remote/*/status", needsPermission("projects.read"), function(req, res) {
var projectName = req.params.id;
var branch = req.params[0];
runtime.storage.projects.getBranchStatus(req.user, projectName,branch).then(function(data) {
res.json(data);
})
.catch(function(err) {
console.log(err.stack);
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
});
// Set the active local branch
app.post("/:id/branches", needsPermission("projects.write"), function(req, res) {
var projectName = req.params.id;
var branchName = req.body.name;
var isCreate = req.body.create;
runtime.storage.projects.setBranch(req.user, projectName,branchName,isCreate).then(function(data) {
res.json(data);
})
.catch(function(err) {
console.log(err.stack);
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
});
// Get a list of remotes
app.get("/:id/remotes", needsPermission("projects.read"), function(req, res) {
var projectName = req.params.id;
runtime.storage.projects.getRemotes(req.user, projectName).then(function(data) {
res.json(data);
})
.catch(function(err) {
console.log(err.stack);
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
});
// Add a remote
app.post("/:id/remotes", needsPermission("projects.write"), function(req,res) {
var projectName = req.params.id;
if (/^https?:\/\/[^/]+@/i.test(req.body.url)) {
res.status(400).json({error:"unexpected_error", message:"Git http url must not include username/password"});
return;
}
runtime.storage.projects.addRemote(req.user, projectName, req.body).then(function() {
res.redirect(303,req.baseUrl+"/"+projectName+"/remotes");
}).catch(function(err) {
console.log(err.stack);
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
})
});
// Delete a remote
app.delete("/:id/remotes/:remoteName", needsPermission("projects.write"), function(req, res) {
var projectName = req.params.id;
var remoteName = req.params.remoteName;
runtime.storage.projects.removeRemote(req.user, projectName, remoteName).then(function(data) {
res.redirect(303,req.baseUrl+"/"+projectName+"/remotes");
})
.catch(function(err) {
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
});
});
// Update a remote
app.put("/:id/remotes/:remoteName", needsPermission("projects.write"), function(req,res) {
var projectName = req.params.id;
var remoteName = req.params.remoteName;
runtime.storage.projects.updateRemote(req.user, projectName, remoteName, req.body).then(function(data) {
res.status(204).end();
})
.catch(function(err) {
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
});
});
return app;
}
}

View File

@@ -13,127 +13,48 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var theme = require("../editor/theme");
var util = require('util');
var runtime;
var settings;
var log;
var apiUtils = require("../util");
var runtimeAPI;
var sshkeys = require("./sshkeys");
var theme = require("./theme");
module.exports = {
init: function(_runtime) {
runtime = _runtime;
settings = runtime.settings;
log = runtime.log;
init: function(_runtimeAPI) {
runtimeAPI = _runtimeAPI;
sshkeys.init(runtimeAPI);
},
runtimeSettings: function(req,res) {
var safeSettings = {
httpNodeRoot: settings.httpNodeRoot||"/",
version: settings.version
var opts = {
user: req.user
}
if (req.user) {
safeSettings.user = {}
var props = ["anonymous","username","image","permissions"];
props.forEach(prop => {
if (req.user.hasOwnProperty(prop)) {
safeSettings.user[prop] = req.user[prop];
}
})
}
safeSettings.context = runtime.nodes.listContextStores();
var themeSettings = theme.settings();
if (themeSettings) {
safeSettings.editorTheme = themeSettings;
}
if (util.isArray(settings.paletteCategories)) {
safeSettings.paletteCategories = settings.paletteCategories;
}
if (settings.flowFilePretty) {
safeSettings.flowFilePretty = settings.flowFilePretty;
}
if (!runtime.nodes.paletteEditorEnabled()) {
safeSettings.editorTheme = safeSettings.editorTheme || {};
safeSettings.editorTheme.palette = safeSettings.editorTheme.palette || {};
safeSettings.editorTheme.palette.editable = false;
}
if (runtime.storage.projects) {
var activeProject = runtime.storage.projects.getActiveProject();
if (activeProject) {
safeSettings.project = activeProject;
} else if (runtime.storage.projects.flowFileExists()) {
safeSettings.files = {
flow: runtime.storage.projects.getFlowFilename(),
credentials: runtime.storage.projects.getCredentialsFilename()
}
runtimeAPI.settings.getRuntimeSettings(opts).then(function(result) {
var themeSettings = theme.settings();
if (themeSettings) {
result.editorTheme = themeSettings;
}
safeSettings.git = {
globalUser: runtime.storage.projects.getGlobalGitUser()
}
}
safeSettings.flowEncryptionType = runtime.nodes.getCredentialKeyType();
settings.exportNodeSettings(safeSettings);
res.json(safeSettings);
res.json(result);
});
},
userSettings: function(req, res) {
var username;
if (!req.user || req.user.anonymous) {
username = '_';
} else {
username = req.user.username;
var opts = {
user: req.user
}
res.json(settings.getUserSettings(username)||{});
runtimeAPI.settings.getUserSettings(opts).then(function(result) {
res.json(result);
});
},
updateUserSettings: function(req,res) {
var username;
if (!req.user || req.user.anonymous) {
username = '_';
} else {
username = req.user.username;
}
var currentSettings = settings.getUserSettings(username)||{};
currentSettings = extend(currentSettings, req.body);
try {
settings.setUserSettings(username, currentSettings).then(function() {
log.audit({event: "settings.update",username:username},req);
res.status(204).end();
}).catch(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()});
});
} catch(err) {
log.warn(log._("settings.user-not-available",{message:log._("settings.not-available")}));
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()});
var opts = {
user: req.user,
settings: req.body
}
runtimeAPI.settings.updateUserSettings(opts).then(function(result) {
res.status(204).end();
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
});
},
sshkeys: function() {
return sshkeys.app()
}
}
function extend(target, source) {
var keys = Object.keys(source);
var i = keys.length;
while(i--) {
var value = source[keys[i]]
var type = typeof value;
if (type === 'string' || type === 'number' || type === 'boolean' || Array.isArray(value)) {
target[keys[i]] = value;
} else if (value === null) {
if (target.hasOwnProperty(keys[i])) {
delete target[keys[i]];
}
} else {
// Object
if (target.hasOwnProperty(keys[i])) {
target[keys[i]] = extend(target[keys[i]],value);
} else {
target[keys[i]] = value;
}
}
}
return target;
}

View File

@@ -14,10 +14,9 @@
* limitations under the License.
**/
var apiUtils = require("../util");
var express = require("express");
var os = require("os");
var runtime;
var needsPermission = require("../auth").needsPermission;
var runtimeAPI;
function getUsername(userObj) {
var username = '__default';
@@ -28,94 +27,72 @@ function getUsername(userObj) {
}
module.exports = {
init: function(_runtime) {
runtime = _runtime;
init: function(_runtimeAPI) {
runtimeAPI = _runtimeAPI;
},
app: function() {
var app = express();
// SSH keys
// List all SSH keys
app.get("/", needsPermission("settings.read"), function(req,res) {
var username = getUsername(req.user);
runtime.storage.projects.ssh.listSSHKeys(username)
.then(function(list) {
app.get("/", function(req,res) {
var opts = {
user: req.user
}
runtimeAPI.settings.getUserKeys(opts).then(function(list) {
res.json({
keys: list
});
})
.catch(function(err) {
// console.log(err.stack);
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
});
});
// Get SSH key detail
app.get("/:id", needsPermission("settings.read"), function(req,res) {
var username = getUsername(req.user);
// console.log('username:', username);
runtime.storage.projects.ssh.getSSHKey(username, req.params.id)
.then(function(data) {
if (data) {
res.json({
publickey: data
});
} else {
res.status(404).end();
}
})
.catch(function(err) {
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
app.get("/:id", function(req,res) {
var opts = {
user: req.user,
id: req.params.id
}
runtimeAPI.settings.getUserKey(opts).then(function(data) {
res.json({
publickey: data
});
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
});
});
// Generate a SSH key
app.post("/", needsPermission("settings.write"), function(req,res) {
var username = getUsername(req.user);
// console.log('req.body:', req.body);
if ( req.body && req.body.name && /^[a-zA-Z0-9\-_]+$/.test(req.body.name)) {
runtime.storage.projects.ssh.generateSSHKey(username, req.body)
.then(function(name) {
// console.log('generate key --- success name:', name);
res.json({
name: name
});
})
.catch(function(err) {
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
app.post("/", function(req,res) {
var opts = {
user: req.user,
id: req.params.id
}
// TODO: validate params
opts.name = req.body.name;
opts.password = req.body.password;
opts.comment = req.body.comment;
opts.size = req.body.size;
runtimeAPI.settings.generateUserKey(opts).then(function(name) {
res.json({
name: name
});
}
else {
res.status(400).json({error:"unexpected_error", message:"You need to have body or body.name"});
}
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
});
});
// Delete a SSH key
app.delete("/:id", needsPermission("settings.write"), function(req,res) {
var username = getUsername(req.user);
runtime.storage.projects.ssh.deleteSSHKey(username, req.params.id)
.then(function() {
app.delete("/:id", function(req,res) {
var opts = {
user: req.user,
id: req.params.id
}
runtimeAPI.settings.removeUserKey(opts).then(function(name) {
res.status(204).end();
})
.catch(function(err) {
if (err.code) {
res.status(400).json({error:err.code, message: err.message});
} else {
res.status(400).json({error:"unexpected_error", message:err.toString()});
}
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
});
});

View File

@@ -40,7 +40,6 @@ var defaultContext = {
var theme = null;
var themeContext = clone(defaultContext);
var themeSettings = null;
var runtime = null;
var themeApp;
@@ -78,12 +77,8 @@ function serveFilesFromTheme(themeValue, themeApp, directory) {
}
module.exports = {
init: function(runtime) {
var settings = runtime.settings;
init: function(settings) {
themeContext = clone(defaultContext);
if (runtime.version) {
themeContext.version = runtime.version();
}
themeSettings = null;
theme = settings.editorTheme || {};
},

View File

@@ -17,17 +17,20 @@ var express = require('express');
var fs = require("fs");
var path = require("path");
var Mustache = require("mustache");
var mime = require("mime");
var apiUtils = require("../util");
var theme = require("./theme");
var redNodes;
var runtimeAPI;
var templateDir = path.resolve(__dirname+"/../../../editor/templates");
var editorTemplate;
module.exports = {
init: function(runtime) {
redNodes = runtime.nodes;
init: function(_runtimeAPI) {
runtimeAPI = _runtimeAPI;
editorTemplate = fs.readFileSync(path.join(templateDir,"index.mst"),"utf8");
Mustache.parse(editorTemplate);
},
@@ -46,8 +49,18 @@ module.exports = {
var icon = req.params.icon;
var scope = req.params.scope;
var module = scope ? scope + '/' + req.params.module : req.params.module;
var iconPath = redNodes.getNodeIconPath(module,icon);
res.sendFile(iconPath);
var opts = {
user: req.user,
module: module,
icon: icon
}
runtimeAPI.nodes.getIcon(opts).then(function(data) {
var contentType = mime.lookup(icon);
res.set("Content-Type", contentType);
res.send(data);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
},
editor: function(req,res) {
res.send(Mustache.render(editorTemplate,theme.context()));

View File

@@ -26,17 +26,21 @@ var apiUtil = require("./util");
var adminApp;
var server;
var runtime;
var editor;
function init(_server,_runtime) {
function init(_server,settings,storage,runtimeAPI) {
server = _server;
runtime = _runtime;
var settings = runtime.settings;
if (settings.httpAdminRoot !== false) {
apiUtil.init(runtime);
adminApp = express();
auth.init(runtime);
var cors = require('cors');
var corsHandler = cors({
origin: "*",
methods: "GET,PUT,POST,DELETE"
});
adminApp.use(corsHandler);
auth.init(settings,storage);
var maxApiRequestSize = settings.apiMaxLength || '5mb';
adminApp.use(bodyParser.json({limit:maxApiRequestSize}));
@@ -61,7 +65,7 @@ function init(_server,_runtime) {
// Editor
if (!settings.disableEditor) {
editor = require("./editor");
var editorApp = editor.init(server, runtime);
var editorApp = editor.init(server, settings, runtimeAPI);
adminApp.use(editorApp);
}
@@ -70,7 +74,7 @@ function init(_server,_runtime) {
adminApp.use(corsHandler);
}
var adminApiApp = require("./admin").init(runtime);
var adminApiApp = require("./admin").init(runtimeAPI);
adminApp.use(adminApiApp);
} else {
adminApp = null;
@@ -93,23 +97,9 @@ module.exports = {
init: init,
start: start,
stop: stop,
library: {
register: function(type) {
if (editor) {
editor.registerLibrary(type);
}
}
},
auth: {
needsPermission: auth.needsPermission
},
comms: {
publish: function(topic,data,retain) {
if (editor) {
editor.publish(topic,data,retain);
}
}
},
get adminApp() { return adminApp; },
get server() { return server; }
};

View File

@@ -15,16 +15,12 @@
**/
var i18n;
var log;
var log = require("../util").log; // TODO: separate module
var i18n = require("../util").i18n; // TODO: separate module
module.exports = {
init: function(_runtime) {
log = _runtime.log;
i18n = _runtime.i18n;
},
errorHandler: function(err,req,res,next) {
console.error(err.stack);
if (err.message === "request entity too large") {
log.error(err);
} else {
@@ -41,5 +37,11 @@ module.exports = {
lang = acceptedLanguages[0];
}
return lang;
},
rejectHandler: function(req,res,err) {
res.status(err.status||500).json({
code: err.code||"unexpected_error",
message: err.message||err.toString()
});
}
}

View File

@@ -18,6 +18,9 @@ var fs = require("fs");
var path = require('path');
var runtime = require("./runtime");
var runtimeAPI = require("./runtime-api");
var redUtil = require("./util");
var api = require("./api");
process.env.NODE_RED_HOME = process.env.NODE_RED_HOME || path.resolve(__dirname+"/..");
@@ -64,15 +67,17 @@ module.exports = {
if (!userSettings.coreNodesDir) {
userSettings.coreNodesDir = path.resolve(path.join(__dirname,"..","nodes"));
}
redUtil.init(userSettings);
if (userSettings.httpAdminRoot !== false) {
runtime.init(userSettings,api);
api.init(httpServer,runtime);
runtime.init(userSettings,redUtil,api);
runtimeAPI.init(runtime,redUtil);
api.init(httpServer,userSettings,runtime.storage,runtimeAPI);
apiEnabled = true;
server = runtime.adminApi.server;
runtime.server = runtime.adminApi.server;
} else {
runtime.init(userSettings);
runtime.init(userSettings,redUtil);
apiEnabled = false;
if (httpServer){
server = httpServer;
@@ -101,14 +106,25 @@ module.exports = {
})
},
nodes: runtime.nodes,
log: runtime.log,
log: redUtil.log,
settings:runtime.settings,
util: runtime.util,
version: runtime.version,
events: runtime.events,
comms: api.comms,
library: api.library,
comms: {
publish: function(topic,data,retain) {
runtime.events.emit("comms",{
topic: topic,
data: data,
retain: retain
})
}
},
library: {
register: function(type) {
return runtime.library.register(null,type);
}
},
auth: api.auth,
get app() { console.log("Deprecated use of RED.app - use RED.httpAdmin instead"); return runtime.app },

131
red/runtime-api/comms.js Normal file
View File

@@ -0,0 +1,131 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
/**
* @namespace RED.comms
*/
/**
* @typedef CommsConnection
* @type {object}
* @property {string} session - a unique session identifier
* @property {Object} user - the user associated with the connection
* @property {Function} send - publish a message to the connection
*/
var runtime;
var retained = {};
var connections = [];
function handleCommsEvent(event) {
publish(event.topic,event.data,event.retain);
}
function handleStatusEvent(event) {
publish("status/"+event.id,event.status,true);
}
function handleRuntimeEvent(event) {
runtime.log.trace("runtime event: "+JSON.stringify(event));
publish("notification/"+event.id,event.payload||{},event.retain);
}
function publish(topic,data,retain) {
if (retain) {
retained[topic] = data;
} else {
delete retained[topic];
}
connections.forEach(connection => connection.send(topic,data))
}
var api = module.exports = {
init: function(_runtime) {
runtime = _runtime;
connections = [];
retained = {};
runtime.events.removeListener("node-status",handleStatusEvent);
runtime.events.on("node-status",handleStatusEvent);
runtime.events.removeListener("runtime-event",handleRuntimeEvent);
runtime.events.on("runtime-event",handleRuntimeEvent);
runtime.events.removeListener("comms",handleCommsEvent);
runtime.events.on("comms",handleCommsEvent);
},
/**
* Registers a new comms connection
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {CommsConnection} opts.client - the client connection
* @return {Promise<Object>} - resolves when complete
* @memberof RED.comms
*/
addConnection: function(opts) {
connections.push(opts.client);
return Promise.resolve();
},
/**
* Unregisters a comms connection
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {CommsConnection} opts.client - the client connection
* @return {Promise<Object>} - resolves when complete
* @memberof RED.comms
*/
removeConnection: function(opts) {
for (var i=0;i<connections.length;i++) {
if (connections[i] === opts.client) {
connections.splice(i,1);
break;
}
}
return Promise.resolve();
},
/**
* Subscribes a comms connection to a given topic. Currently, all clients get
* automatically subscribed to everything and cannot unsubscribe. Sending a subscribe
* request will trigger retained messages to be sent.
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {CommsConnection} opts.client - the client connection
* @param {String} opts.topic - the topic to subscribe to
* @return {Promise<Object>} - resolves when complete
* @memberof RED.comms
*/
subscribe: function(opts) {
var re = new RegExp("^"+opts.topic.replace(/([\[\]\?\(\)\\\\$\^\*\.|])/g,"\\$1").replace(/\+/g,"[^/]+").replace(/\/#$/,"(\/.*)?")+"$");
for (var t in retained) {
if (re.test(t)) {
opts.client.send(t,retained[t]);
}
}
return Promise.resolve();
},
/**
* TODO: Unsubscribes a comms connection from a given topic
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {CommsConnection} opts.client - the client connection
* @param {String} opts.topic - the topic to unsubscribe from
* @return {Promise<Object>} - resolves when complete
* @memberof RED.comms
*/
unsubscribe: function(opts) {}
};

156
red/runtime-api/context.js Normal file
View File

@@ -0,0 +1,156 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
/**
* @namespace RED.context
*/
var runtime;
// TODO: move runtime/util to util/index
var util = require("../runtime/util");
function exportContextStore(scope,ctx, store, result, callback) {
ctx.keys(store,function(err, keys) {
if (err) {
return callback(err);
}
result[store] = {};
var c = keys.length;
if (c === 0) {
callback(null);
} else {
keys.forEach(function(key) {
ctx.get(key,store,function(err, v) {
if (err) {
return callback(err);
}
if (scope !== 'global' ||
store === runtime.nodes.listContextStores().default ||
!runtime.settings.hasOwnProperty("functionGlobalContext") ||
!runtime.settings.functionGlobalContext.hasOwnProperty(key) ||
runtime.settings.functionGlobalContext[key] !== v) {
result[store][key] = util.encodeObject({msg:v});
}
c--;
if (c === 0) {
callback(null);
}
});
});
}
});
}
var api = module.exports = {
init: function(_runtime) {
runtime = _runtime;
},
/**
* Gets the info of an individual node set
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.scope - the scope of the context
* @param {String} opts.id - the id of the context
* @param {String} opts.store - the context store
* @param {String} opts.key - the context key
* @return {Promise} - the node information
* @memberof RED.nodes
*/
getValue: function(opts) {
return new Promise(function(resolve,reject) {
var scope = opts.scope;
var id = opts.id;
var store = opts.store;
var key = opts.key;
var availableStores = runtime.nodes.listContextStores();
//{ default: 'default', stores: [ 'default', 'file' ] }
if (store && availableStores.stores.indexOf(store) === -1) {
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"not_found"});
var err = new Error();
err.code = "not_found";
err.status = 404;
return reject(err);
}
var ctx;
if (scope === 'global') {
ctx = runtime.nodes.getContext('global');
} else if (scope === 'flow') {
ctx = runtime.nodes.getContext(id);
} else if (scope === 'node') {
var node = runtime.nodes.getNode(id);
if (node) {
ctx = node.context();
}
}
if (ctx) {
if (key) {
store = store || availableStores.default;
ctx.get(key,store,function(err, v) {
var encoded = util.encodeObject({msg:v});
if (store !== availableStores.default) {
encoded.store = store;
}
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key});
resolve(encoded);
});
return;
} else {
var stores;
if (!store) {
stores = availableStores.stores;
} else {
stores = [store];
}
var result = {};
var c = stores.length;
var errorReported = false;
stores.forEach(function(store) {
exportContextStore(scope,ctx,store,result,function(err) {
if (err) {
// TODO: proper error reporting
if (!errorReported) {
errorReported = true;
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"unexpected_error"});
var err = new Error();
err.code = "unexpected_error";
err.status = 400;
return reject(err);
}
return;
}
c--;
if (c === 0) {
if (!errorReported) {
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key});
resolve(result);
}
}
});
})
}
} else {
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key});
resolve({});
}
})
}
}

251
red/runtime-api/flows.js Normal file
View File

@@ -0,0 +1,251 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
/**
* @namespace RED.flows
*/
/**
* @typedef Flows
* @type {object}
* @property {string} rev - the flow revision identifier
* @property {Array} flows - the flow configuration, an array of node configuration objects
*/
/**
* @typedef Flow
* @type {object}
* @property {string} id - the flow identifier
* @property {string} label - a label for the flow
* @property {Array} nodes - an array of node configuration objects
*/
var runtime;
var api = module.exports = {
init: function(_runtime) {
runtime = _runtime;
},
/**
* Gets the current flow configuration
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @return {Promise<Flows>} - the active flow configuration
* @memberof RED.flows
*/
getFlows: function(opts) {
return new Promise(function(resolve,reject) {
runtime.log.audit({event: "flows.get"}/*,req*/);
return resolve(runtime.nodes.getFlows());
});
},
/**
* Sets the current flow configuration
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @return {Promise<Flows>} - the active flow configuration
* @memberof RED.flows
*/
setFlows: function(opts) {
return new Promise(function(resolve,reject) {
var flows = opts.flows;
var deploymentType = opts.deploymentType||"full";
runtime.log.audit({event: "flows.set",type:deploymentType}/*,req*/);
var apiPromise;
if (deploymentType === 'reload') {
apiPromise = runtime.nodes.loadFlows();
} else {
if (flows.hasOwnProperty('rev')) {
var currentVersion = runtime.nodes.getFlows().rev;
if (currentVersion !== flows.rev) {
var err;
err = new Error();
err.code = "version_mismatch";
err.status = 409;
//TODO: log warning
return reject(err);
}
}
apiPromise = runtime.nodes.setFlows(flows.flows,deploymentType);
}
apiPromise.then(function(flowId) {
return resolve({rev:flowId});
}).catch(function(err) {
runtime.log.warn(runtime.log._("api.flows.error-"+(deploymentType === 'reload'?'reload':'save'),{message:err.message}));
runtime.log.warn(err.stack);
return reject(err);
});
});
},
/**
* Adds a flow configuration
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {Object} opts.flow - the flow to add
* @return {Promise<String>} - the id of the added flow
* @memberof RED.flows
*/
addFlow: function(opts) {
return new Promise(function(resolve,reject) {
var flow = opts.flow;
runtime.nodes.addFlow(flow).then(function(id) {
runtime.log.audit({event: "flow.add",id:id});
return resolve(id);
}).catch(function(err) {
runtime.log.audit({event: "flow.add",error:err.code||"unexpected_error",message:err.toString()});
err.status = 400;
return reject(err);
})
})
},
/**
* Gets an individual flow configuration
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {Object} opts.id - the id of the flow to retrieve
* @return {Promise<Flow>} - the active flow configuration
* @memberof RED.flows
*/
getFlow: function(opts) {
return new Promise(function (resolve,reject) {
var flow = runtime.nodes.getFlow(opts.id);
if (flow) {
runtime.log.audit({event: "flow.get",id:opts.id});
return resolve(flow);
} else {
runtime.log.audit({event: "flow.get",id:opts.id,error:"not_found"});
var err = new Error();
err.code = "not_found";
err.status = 404;
return reject(err);
}
})
},
/**
* Updates an existing flow configuration
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {Object} opts.id - the id of the flow to update
* @param {Object} opts.flow - the flow configuration
* @return {Promise<String>} - the id of the updated flow
* @memberof RED.flows
*/
updateFlow: function(opts) {
return new Promise(function (resolve,reject) {
var flow = opts.flow;
var id = opts.id;
try {
runtime.nodes.updateFlow(id,flow).then(function() {
runtime.log.audit({event: "flow.update",id:id});
return resolve(id);
}).catch(function(err) {
runtime.log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()});
err.status = 400;
return reject(err);
})
} catch(err) {
if (err.code === 404) {
runtime.log.audit({event: "flow.update",id:id,error:"not_found"});
// TODO: this swap around of .code and .status isn't ideal
err.status = 404;
err.code = "not_found";
return reject(err);
} else {
runtime.log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()});
err.status = 400;
return reject(err);
}
}
});
},
/**
* Deletes a flow
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {Object} opts.id - the id of the flow to delete
* @return {Promise} - resolves if successful
* @memberof RED.flows
*/
deleteFlow: function(opts) {
return new Promise(function (resolve,reject) {
var id = opts.id;
try {
runtime.nodes.removeFlow(id).then(function() {
runtime.log.audit({event: "flow.remove",id:id});
return resolve();
}).catch(function(err) {
runtime.log.audit({event: "flow.remove",id:id,error:err.code||"unexpected_error",message:err.toString()});
err.status = 400;
return reject(err);
});
} catch(err) {
if (err.code === 404) {
runtime.log.audit({event: "flow.remove",id:id,error:"not_found"});
// TODO: this swap around of .code and .status isn't ideal
err.status = 404;
err.code = "not_found";
return reject(err);
} else {
runtime.log.audit({event: "flow.remove",id:id,error:err.code||"unexpected_error",message:err.toString()});
err.status = 400;
return reject(err);
}
}
});
},
/**
* Gets the safe credentials for a node
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.type - the node type to return the credential information for
* @param {String} opts.id - the node id
* @return {Promise<Object>} - the safe credentials
* @memberof RED.flows
*/
getNodeCredentials: function(opts) {
return new Promise(function(resolve,reject) {
runtime.log.audit({event: "credentials.get",type:opts.type,id:opts.id});
var credentials = runtime.nodes.getCredentials(opts.id);
if (!credentials) {
return resolve({});
}
var definition = runtime.nodes.getCredentialDefinition(opts.type) || {};
var sendCredentials = {};
for (var cred in definition) {
if (definition.hasOwnProperty(cred)) {
if (definition[cred].type == "password") {
var key = 'has_' + cred;
sendCredentials[key] = credentials[cred] != null && credentials[cred] !== '';
continue;
}
sendCredentials[cred] = credentials[cred] || '';
}
}
resolve(sendCredentials);
})
}
}

68
red/runtime-api/index.js Normal file
View File

@@ -0,0 +1,68 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
/**
* A user accessing the API
* @typedef User
* @type {object}
*/
var runtime;
/**
* @namespace RED
*/
var api = module.exports = {
init: function(_runtime, redUtil) {
runtime = _runtime;
api.comms.init(runtime);
api.flows.init(runtime);
api.nodes.init(runtime);
api.settings.init(runtime);
api.library.init(runtime);
api.projects.init(runtime);
api.context.init(runtime);
},
comms: require("./comms"),
flows: require("./flows"),
library: require("./library"),
nodes: require("./nodes"),
settings: require("./settings"),
projects: require("./projects"),
context: require("./context"),
/**
* Returns whether the runtime is started
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @return {Promise<Boolean>} - whether the runtime is started
* @memberof RED
*/
isStarted: function(opts) {
return Promise.resolve(runtime.isStarted());
},
/**
* Returns version number of the runtime
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @return {Promise<String>} - the runtime version number
* @memberof RED
*/
version: function(opts) {
return Promise.resolve(runtime.version());
}
}

120
red/runtime-api/library.js Normal file
View File

@@ -0,0 +1,120 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
/**
* @namespace RED.library
*/
var runtime;
var api = module.exports = {
init: function(_runtime) {
runtime = _runtime;
},
/**
* Gets an entry from the library.
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.type - the type of entry
* @param {String} opts.path - the path of the entry
* @return {Promise<String|Object>} - resolves when complete
* @memberof RED.library
*/
getEntry: function(opts) {
return new Promise(function(resolve,reject) {
runtime.library.getEntry(opts.type,opts.path).then(function(result) {
runtime.log.audit({event: "library.get",type:opts.type,path:opts.path});
return resolve(result);
}).catch(function(err) {
if (err) {
runtime.log.warn(runtime.log._("api.library.error-load-entry",{path:opts.path,message:err.toString()}));
if (err.code === 'forbidden') {
err.status = 403;
return reject(err);
} else if (err.code === "not_found") {
err.status = 404;
} else {
err.status = 400;
}
runtime.log.audit({event: "library.get",type:opts.type,path:opts.path,error:err.code});
return reject(err);
}
runtime.log.audit({event: "library.get",type:opts.type,error:"not_found"});
var error = new Error();
error.code = "not_found";
error.status = 404;
return reject(error);
});
})
},
/**
* Saves an entry to the library
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.type - the type of entry
* @param {String} opts.path - the path of the entry
* @param {Object} opts.meta - any meta data associated with the entry
* @param {String} opts.body - the body of the entry
* @return {Promise} - resolves when complete
* @memberof RED.library
*/
saveEntry: function(opts) {
return new Promise(function(resolve,reject) {
runtime.library.saveEntry(opts.type,opts.path,opts.meta,opts.body).then(function() {
runtime.log.audit({event: "library.set",type:opts.type,path:opts.path});
return resolve();
}).catch(function(err) {
runtime.log.warn(runtime.log._("api.library.error-save-entry",{path:opts.path,message:err.toString()}));
if (err.code === 'forbidden') {
runtime.log.audit({event: "library.set",type:opts.type,path:opts.path,error:"forbidden"});
err.status = 403;
return reject(err);
}
runtime.log.audit({event: "library.set",type:opts.type,path:opts.path,error:"unexpected_error",message:err.toString()});
var error = new Error();
error.status = 400;
return reject(error);
});
})
},
/**
* Returns a complete listing of all entries of a given type in the library.
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.type - the type of entry
* @return {Promise<Object>} - the entry listing
* @memberof RED.library
*/
getEntries: function(opts) {
return new Promise(function(resolve,reject) {
if (opts.type !== 'flows') {
return reject(new Error("API only supports flows"));
}
runtime.storage.getAllFlows().then(function(flows) {
runtime.log.audit({event: "library.get.all",type:"flow"});
var examples = runtime.nodes.getNodeExampleFlows();
if (examples) {
flows.d = flows.d||{};
flows.d._examples_ = examples;
}
return resolve(flows);
});
})
}
}

438
red/runtime-api/nodes.js Normal file
View File

@@ -0,0 +1,438 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
/**
* @namespace RED.nodes
*/
var fs = require("fs");
var runtime;
function putNode(node, enabled) {
var info;
var promise;
if (!node.err && node.enabled === enabled) {
promise = Promise.resolve(node);
} else {
if (enabled) {
promise = runtime.nodes.enableNode(node.id);
} else {
promise = runtime.nodes.disableNode(node.id);
}
}
return promise;
}
var api = module.exports = {
init: function(_runtime) {
runtime = _runtime;
},
/**
* Gets the info of an individual node set
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the node set to return
* @return {Promise<NodeInfo>} - the node information
* @memberof RED.nodes
*/
getNodeInfo: function(opts) {
return new Promise(function(resolve,reject) {
var id = opts.id;
var result = runtime.nodes.getNodeInfo(id);
if (result) {
runtime.log.audit({event: "nodes.info.get",id:id});
delete result.loaded;
return resolve(result);
} else {
runtime.log.audit({event: "nodes.info.get",id:id,error:"not_found"});
var err = new Error();
err.code = "not_found";
err.status = 404;
return reject(err);
}
})
},
/**
* Gets the list of node modules installed in the runtime
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @return {Promise<NodeList>} - the list of node modules
* @memberof RED.nodes
*/
getNodeList: function(opts) {
return new Promise(function(resolve,reject) {
runtime.log.audit({event: "nodes.list.get"});
return resolve(runtime.nodes.getNodeList());
})
},
/**
* Gets an individual node's html content
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the node set to return
* @param {String} opts.lang - the locale language to return
* @return {Promise<String>} - the node html content
* @memberof RED.nodes
*/
getNodeConfig: function(opts) {
return new Promise(function(resolve,reject) {
var id = opts.id;
var lang = opts.lang;
var result = runtime.nodes.getNodeConfig(id,lang);
if (result) {
runtime.log.audit({event: "nodes.config.get",id:id});
return resolve(result);
} else {
runtime.log.audit({event: "nodes.config.get",id:id,error:"not_found"});
var err = new Error();
err.code = "not_found";
err.status = 404;
return reject(err);
}
});
},
/**
* Gets all node html content
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.lang - the locale language to return
* @return {Promise<String>} - the node html content
* @memberof RED.nodes
*/
getNodeConfigs: function(opts) {
return new Promise(function(resolve,reject) {
runtime.log.audit({event: "nodes.configs.get"});
return resolve(runtime.nodes.getNodeConfigs(opts.lang));
});
},
/**
* Gets the info of a node module
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.module - the id of the module to return
* @return {Promise<ModuleInfo>} - the node module info
* @memberof RED.nodes
*/
getModuleInfo: function(opts) {
return new Promise(function(resolve,reject) {
var result = runtime.nodes.getModuleInfo(opts.module);
if (result) {
runtime.log.audit({event: "nodes.module.get",id:opts.module});
return resolve(result);
} else {
runtime.log.audit({event: "nodes.module.get",id:opts.module,error:"not_found"});
var err = new Error();
err.code = "not_found";
err.status = 404;
return reject(err);
}
})
},
/**
* Install a new module into the runtime
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.module - the id of the module to install
* @param {String} opts.version - (optional) the version of the module to install
* @return {Promise<ModuleInfo>} - the node module info
* @memberof RED.nodes
*/
addModule: function(opts) {
return new Promise(function(resolve,reject) {
if (!runtime.settings.available()) {
runtime.log.audit({event: "nodes.install",error:"settings_unavailable"});
var err = new Error("Settings unavailable");
err.code = "settings_unavailable";
err.status = 400;
return reject(err);
}
if (opts.module) {
var existingModule = runtime.nodes.getModuleInfo(opts.module);
if (existingModule) {
if (!opts.version || existingModule.version === opts.version) {
runtime.log.audit({event: "nodes.install",module:opts.module, version:opts.version, error:"module_already_loaded"});
var err = new Error("Module already loaded");
err.code = "module_already_loaded";
err.status = 400;
return reject(err);
}
if (!existingModule.local) {
runtime.log.audit({event: "nodes.install",module:opts.module, version:opts.version, error:"module_not_local"});
var err = new Error("Module not locally installed");
err.code = "module_not_local";
err.status = 400;
return reject(err);
}
}
runtime.nodes.installModule(opts.module,opts.version).then(function(info) {
runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version});
return resolve(info);
}).catch(function(err) {
if (err.code === 404) {
runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,error:"not_found"});
// TODO: code/status
err.status = 404;
} else if (err.code) {
err.status = 400;
runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,error:err.code});
} else {
err.status = 400;
runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,error:err.code||"unexpected_error",message:err.toString()});
}
return reject(err);
})
} else {
runtime.log.audit({event: "nodes.install",module:opts.module,error:"invalid_request"});
var err = new Error("Invalid request");
err.code = "invalid_request";
err.status = 400;
return reject(err);
}
});
},
/**
* Removes a module from the runtime
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.module - the id of the module to remove
* @return {Promise} - resolves when complete
* @memberof RED.nodes
*/
removeModule: function(opts) {
return new Promise(function(resolve,reject) {
if (!runtime.settings.available()) {
runtime.log.audit({event: "nodes.install",error:"settings_unavailable"});
var err = new Error("Settings unavailable");
err.code = "settings_unavailable";
err.status = 400;
return reject(err);
}
var module = runtime.nodes.getModuleInfo(opts.module);
if (!module) {
runtime.log.audit({event: "nodes.remove",module:opts.module,error:"not_found"});
var err = new Error();
err.code = "not_found";
err.status = 404;
return reject(err);
}
try {
runtime.nodes.uninstallModule(opts.module).then(function() {
runtime.log.audit({event: "nodes.remove",module:opts.module});
resolve();
}).catch(function(err) {
err.status = 400;
runtime.log.audit({event: "nodes.remove",module:opts.module,error:err.code||"unexpected_error",message:err.toString()});
return reject(err);
})
} catch(error) {
runtime.log.audit({event: "nodes.remove",module:opts.module,error:error.code||"unexpected_error",message:error.toString()});
error.status = 400;
return reject(error);
}
});
},
/**
* Enables or disables a module in the runtime
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.module - the id of the module to enable or disable
* @param {String} opts.enabled - whether the module should be enabled or disabled
* @return {Promise<ModuleInfo>} - the module info object
* @memberof RED.nodes
*/
setModuleState: function(opts) {
var mod = opts.module;
return new Promise(function(resolve,reject) {
if (!runtime.settings.available()) {
runtime.log.audit({event: "nodes.module.set",error:"settings_unavailable"});
var err = new Error("Settings unavailable");
err.code = "settings_unavailable";
err.status = 400;
return reject(err);
}
try {
var module = runtime.nodes.getModuleInfo(mod);
if (!module) {
runtime.log.audit({event: "nodes.module.set",module:mod,error:"not_found"});
var err = new Error();
err.code = "not_found";
err.status = 404;
return reject(err);
}
var nodes = module.nodes;
var promises = [];
for (var i = 0; i < nodes.length; ++i) {
promises.push(putNode(nodes[i],opts.enabled));
}
Promise.all(promises).then(function() {
return resolve(runtime.nodes.getModuleInfo(mod));
}).catch(function(err) {
err.status = 400;
return reject(err);
});
} catch(error) {
runtime.log.audit({event: "nodes.module.set",module:mod,enabled:opts.enabled,error:error.code||"unexpected_error",message:error.toString()});
error.status = 400;
return reject(error);
}
});
},
/**
* Enables or disables a n individual node-set in the runtime
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the node-set to enable or disable
* @param {String} opts.enabled - whether the module should be enabled or disabled
* @return {Promise<ModuleInfo>} - the module info object
* @memberof RED.nodes
*/
setNodeSetState: function(opts) {
return new Promise(function(resolve,reject) {
if (!runtime.settings.available()) {
runtime.log.audit({event: "nodes.info.set",error:"settings_unavailable"});
var err = new Error("Settings unavailable");
err.code = "settings_unavailable";
err.status = 400;
return reject(err);
}
var id = opts.id;
var enabled = opts.enabled;
try {
var node = runtime.nodes.getNodeInfo(id);
if (!node) {
runtime.log.audit({event: "nodes.info.set",id:id,error:"not_found"});
var err = new Error();
err.code = "not_found";
err.status = 404;
return reject(err);
} else {
delete node.loaded;
putNode(node,enabled).then(function(result) {
runtime.log.audit({event: "nodes.info.set",id:id,enabled:enabled});
return resolve(result);
}).catch(function(err) {
runtime.log.audit({event: "nodes.info.set",id:id,enabled:enabled,error:err.code||"unexpected_error",message:err.toString()});
err.status = 400;
return reject(err);
});
}
} catch(error) {
runtime.log.audit({event: "nodes.info.set",id:id,enabled:enabled,error:error.code||"unexpected_error",message:error.toString()});
error.status = 400;
return reject(error);
}
});
},
/**
* Gets all registered module message catalogs
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {User} opts.lang - the i18n language to return. If not set, uses runtime default (en-US)
* @return {Promise<Object>} - the message catalogs
* @memberof RED.nodes
*/
getModuleCatalogs: function(opts) {
return new Promise(function(resolve,reject) {
var namespace = opts.module;
var lang = opts.lang;
var prevLang = runtime.i18n.i.language;
// Trigger a load from disk of the language if it is not the default
runtime.i18n.i.changeLanguage(lang, function(){
var nodeList = runtime.nodes.getNodeList();
var result = {};
nodeList.forEach(function(n) {
if (n.module !== "node-red") {
result[n.id] = runtime.i18n.i.getResourceBundle(lang, n.id)||{};
}
});
resolve(result);
});
runtime.i18n.i.changeLanguage(prevLang);
});
},
/**
* Gets a modules message catalog
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {User} opts.module - the module
* @param {User} opts.lang - the i18n language to return. If not set, uses runtime default (en-US)
* @return {Promise<Object>} - the message catalog
* @memberof RED.nodes
*/
getModuleCatalog: function(opts) {
return new Promise(function(resolve,reject) {
var namespace = opts.module;
var lang = opts.lang;
var prevLang = runtime.i18n.i.lng();
// Trigger a load from disk of the language if it is not the default
runtime.i18n.i.changeLanguage(lang, function(){
var catalog = runtime.i18n.getResourceBundle(lang, namespace);
resolve(catalog||{});
});
runtime.i18n.i.changeLanguage(prevLang);
});
},
/**
* Gets the list of all icons available in the modules installed within the runtime
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @return {Promise<IconList>} - the list of all icons
* @memberof RED.nodes
*/
getIconList: function(opts) {
return new Promise(function(resolve,reject) {
runtime.log.audit({event: "nodes.icons.get"});
return resolve(runtime.nodes.getNodeIcons());
});
},
/**
* Gets a node icon
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.module - the id of the module requesting the icon
* @param {String} opts.icon - the name of the icon
* @return {Promise<Buffer>} - the icon file as a Buffer
* @memberof RED.nodes
*/
getIcon: function(opts) {
return new Promise(function(resolve,reject) {
var iconPath = runtime.nodes.getNodeIconPath(opts.module,opts.icon);
fs.readFile(iconPath,function(err,data) {
if (err) {
err.status = 400;
return reject(err);
}
return resolve(data)
});
});
}
}

440
red/runtime-api/projects.js Normal file
View File

@@ -0,0 +1,440 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
/**
* @namespace RED.projects
*/
var runtime;
var api = module.exports = {
init: function(_runtime) {
runtime = _runtime;
},
available: function(opts) {
return Promise.resolve(!!runtime.storage.projects);
},
/**
* List projects known to the runtime
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
listProjects: function(opts) {
return runtime.storage.projects.listProjects(opts.user).then(function(list) {
var active = runtime.storage.projects.getActiveProject(opts.user);
var response = {
projects: list
};
if (active) {
response.active = active.name;
}
return response;
}).catch(function(err) {
err.status = 400;
throw err;
})
},
/**
* Create a new project
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {Object} opts.project - the project information
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
createProject: function(opts) {
return runtime.storage.projects.createProject(opts.user, opts.project)
},
/**
* Initialises an empty project
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project to initialise
* @param {Object} opts.project - the project information
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
initialiseProject: function(opts) {
// Initialised set when creating default files for an empty repo
return runtime.storage.projects.initialiseProject(opts.user, opts.id, opts.project)
},
/**
* Gets the active project
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @return {Promise<Object>} - the active project
* @memberof RED.projects
*/
getActiveProject: function(opts) {
return Promise.resolve(runtime.storage.projects.getActiveProject(opts.user));
},
/**
*
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project to activate
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
setActiveProject: function(opts) {
var currentProject = runtime.storage.projects.getActiveProject(opts.user);
if (!currentProject || opts.id !== currentProject.name) {
return runtime.storage.projects.setActiveProject(opts.user, opts.id);
} else {
return Promise.resolve();
}
},
/**
* Gets a projects metadata
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project to get
* @return {Promise<Object>} - the project metadata
* @memberof RED.projects
*/
getProject: function(opts) {
return runtime.storage.projects.getProject(opts.user, opts.id)
},
/**
* Updates the metadata of an existing project
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project to update
* @param {Object} opts.project - the project information
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
updateProject: function(opts) {
return runtime.storage.projects.updateProject(opts.user, opts.id, opts.project);
},
/**
* Deletes a project
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project to update
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
deleteProject: function(opts) {
return runtime.storage.projects.deleteProject(opts.user, opts.id);
},
/**
* Gets current git status of a project
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {Boolean} opts.remote - whether to include status of remote repos
* @return {Promise<Object>} - the project status
* @memberof RED.projects
*/
getStatus: function(opts) {
return runtime.storage.projects.getStatus(opts.user, opts.id, opts.remote)
},
/**
* Get a list of local branches
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {Boolean} opts.remote - whether to return remote branches (true) or local (false)
* @return {Promise<Object>} - a list of the local branches
* @memberof RED.projects
*/
getBranches: function(opts) {
return runtime.storage.projects.getBranches(opts.user, opts.id, opts.remote);
},
/**
* Gets the status of a branch
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {String} opts.branch - the name of the branch
* @return {Promise<Object>} - the status of the branch
* @memberof RED.projects
*/
getBranchStatus: function(opts) {
return runtime.storage.projects.getBranchStatus(opts.user, opts.id, opts.branch);
},
/**
* Sets the current local branch
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {String} opts.branch - the name of the branch
* @param {Boolean} opts.create - whether to create the branch if it doesn't exist
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
setBranch: function(opts) {
return runtime.storage.projects.setBranch(opts.user, opts.id, opts.branch, opts.create)
},
/**
* Deletes a branch
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {String} opts.branch - the name of the branch
* @param {Boolean} opts.force - whether to force delete
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
deleteBranch: function(opts) {
return runtime.storage.projects.deleteBranch(opts.user, opts.id, opts.branch, false, opts.force);
},
/**
* Commits the current staged files
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {String} opts.message - the message to associate with the commit
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
commit: function(opts) {
return runtime.storage.projects.commit(opts.user, opts.id,{message: opts.message});
},
/**
* Gets the details of a single commit
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {String} opts.sha - the sha of the commit to return
* @return {Promise<Object>} - the commit details
* @memberof RED.projects
*/
getCommit: function(opts) {
return runtime.storage.projects.getCommit(opts.user, opts.id, opts.sha);
},
/**
* Gets the commit history of the project
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {String} opts.limit - limit how many to return
* @param {String} opts.before - id of the commit to work back from
* @return {Promise<Array>} - an array of commits
* @memberof RED.projects
*/
getCommits: function(opts) {
return runtime.storage.projects.getCommits(opts.user, opts.id, {
limit: opts.limit || 20,
before: opts.before
});
},
/**
* Abort an in-progress merge
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
abortMerge: function(opts) {
return runtime.storage.projects.abortMerge(opts.user, opts.id);
},
/**
* Resolves a merge conflict
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {String} opts.path - the path of the file being merged
* @param {String} opts.resolutions - how to resolve the merge conflict
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
resolveMerge: function(opts) {
return runtime.storage.projects.resolveMerge(opts.user, opts.id, opts.path, opts.resolution);
},
/**
* Gets a listing of the files in the project
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @return {Promise<Object>} - the file listing
* @memberof RED.projects
*/
getFiles: function(opts) {
return runtime.storage.projects.getFiles(opts.user, opts.id);
},
/**
* Gets the contents of a file
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {String} opts.path - the path of the file
* @param {String} opts.tree - the version control tree to use
* @return {Promise<String>} - the content of the file
* @memberof RED.projects
*/
getFile: function(opts) {
return runtime.storage.projects.getFile(opts.user, opts.id,opts.path,opts.tree);
},
/**
*
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {String|Array} opts.path - the path of the file, or an array of paths
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
stageFile: function(opts) {
return runtime.storage.projects.stageFile(opts.user, opts.id, opts.path);
},
/**
*
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {String} opts.path - the path of the file. If not set, all staged files are unstaged
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
unstageFile: function(opts) {
return runtime.storage.projects.unstageFile(opts.user, opts.id, opts.path);
},
/**
* Reverts changes to a file back to its commited version
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {String} opts.path - the path of the file
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
revertFile: function(opts) {
return runtime.storage.projects.revertFile(opts.user, opts.id,opts.path)
},
/**
* Get the diff of a file
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {String} opts.path - the path of the file
* @param {String} opts.type - the type of diff
* @return {Promise<Object>} - the requested diff
* @memberof RED.projects
*/
getFileDiff: function(opts) {
return runtime.storage.projects.getFileDiff(opts.user, opts.id, opts.path, opts.type);
},
/**
* Gets a list of the project remotes
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @return {Promise<Object>} - a list of project remotes
* @memberof RED.projects
*/
getRemotes: function(opts) {
return runtime.storage.projects.getRemotes(opts.user, opts.id);
},
/**
*
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {Object} opts.remote - the remote metadata
* @param {String} opts.remote.name - the name of the remote
* @param {String} opts.remote.url - the url of the remote
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
addRemote: function(opts) {
return runtime.storage.projects.addRemote(opts.user, opts.id, opts.remote)
},
/**
* Remove a project remote
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {String} opts.remote - the name of the remote
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
removeRemote: function(opts) {
return runtime.storage.projects.removeRemote(opts.user, opts.id, opts.remote);
},
/**
*
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {Object} opts.remote - the remote metadata
* @param {String} opts.remote.name - the name of the remote
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
updateRemote: function(opts) {
return runtime.storage.projects.updateRemote(opts.user, opts.id, opts.remote.name, opts.remote)
},
/**
* Pull changes from the remote
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
pull: function(opts) {
return runtime.storage.projects.pull(opts.user, opts.id, opts.remote, opts.track, opts.allowUnrelatedHistories);
},
/**
* Push changes to a remote
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project
* @param {String} opts.remote - the name of the remote
* @param {String} opts.track - whether to set the remote as the upstream
* @return {Promise<Object>} - resolves when complete
* @memberof RED.projects
*/
push: function(opts) {
return runtime.storage.projects.push(opts.user, opts.id, opts.remote, opts.track);
}
}

267
red/runtime-api/settings.js Normal file
View File

@@ -0,0 +1,267 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
/**
* @namespace RED.settings
*/
var util = require("util");
var runtime;
function extend(target, source) {
var keys = Object.keys(source);
var i = keys.length;
while(i--) {
var value = source[keys[i]]
var type = typeof value;
if (type === 'string' || type === 'number' || type === 'boolean' || Array.isArray(value)) {
target[keys[i]] = value;
} else if (value === null) {
if (target.hasOwnProperty(keys[i])) {
delete target[keys[i]];
}
} else {
// Object
if (target.hasOwnProperty(keys[i])) {
target[keys[i]] = extend(target[keys[i]],value);
} else {
target[keys[i]] = value;
}
}
}
return target;
}
function getSSHKeyUsername(userObj) {
var username = '__default';
if ( userObj && userObj.name ) {
username = userObj.name;
}
return username;
}
var api = module.exports = {
init: function(_runtime) {
runtime = _runtime;
},
/**
* Gets the runtime settings object
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @return {Promise<Object>} - the runtime settings
* @memberof RED.settings
*/
getRuntimeSettings: function(opts) {
return new Promise(function(resolve,reject) {
try {
var safeSettings = {
httpNodeRoot: runtime.settings.httpNodeRoot||"/",
version: runtime.settings.version
}
if (opts.user) {
safeSettings.user = {}
var props = ["anonymous","username","image","permissions"];
props.forEach(prop => {
if (opts.user.hasOwnProperty(prop)) {
safeSettings.user[prop] = opts.user[prop];
}
})
}
safeSettings.context = runtime.nodes.listContextStores();
if (util.isArray(runtime.settings.paletteCategories)) {
safeSettings.paletteCategories = runtime.settings.paletteCategories;
}
if (runtime.settings.flowFilePretty) {
safeSettings.flowFilePretty = runtime.settings.flowFilePretty;
}
if (!runtime.nodes.paletteEditorEnabled()) {
safeSettings.editorTheme = safeSettings.editorTheme || {};
safeSettings.editorTheme.palette = safeSettings.editorTheme.palette || {};
safeSettings.editorTheme.palette.editable = false;
}
if (runtime.storage.projects) {
var activeProject = runtime.storage.projects.getActiveProject();
if (activeProject) {
safeSettings.project = activeProject;
} else if (runtime.storage.projects.flowFileExists()) {
safeSettings.files = {
flow: runtime.storage.projects.getFlowFilename(),
credentials: runtime.storage.projects.getCredentialsFilename()
}
}
safeSettings.git = {
globalUser: runtime.storage.projects.getGlobalGitUser()
}
}
safeSettings.flowEncryptionType = runtime.nodes.getCredentialKeyType();
runtime.settings.exportNodeSettings(safeSettings);
resolve(safeSettings);
}catch(err) {
console.log(err);
}
});
},
/**
* Gets an individual user's settings object
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @return {Promise<Object>} - the user settings
* @memberof RED.settings
*/
getUserSettings: function(opts) {
var username;
if (!opts.user || opts.user.anonymous) {
username = '_';
} else {
username = opts.user.username;
}
return Promise.resolve(runtime.settings.getUserSettings(username)||{});
},
/**
* Updates an individual user's settings object.
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {Object} opts.settings - the updates to the user settings
* @return {Promise<Object>} - the user settings
* @memberof RED.settings
*/
updateUserSettings: function(opts) {
var username;
if (!opts.user || opts.user.anonymous) {
username = '_';
} else {
username = opts.user.username;
}
return new Promise(function(resolve,reject) {
var currentSettings = runtime.settings.getUserSettings(username)||{};
currentSettings = extend(currentSettings, opts.settings);
try {
runtime.settings.setUserSettings(username, currentSettings).then(function() {
runtime.log.audit({event: "settings.update",username:username});
return resolve();
}).catch(function(err) {
runtime.log.audit({event: "settings.update",username:username,error:err.code||"unexpected_error",message:err.toString()});
err.status = 400;
return reject(err);
});
} catch(err) {
log.warn(log._("settings.user-not-available",{message:log._("settings.not-available")}));
log.audit({event: "settings.update",username:username,error:err.code||"unexpected_error",message:err.toString()});
err.status = 400;
return reject(err);
}
});
},
/**
* Gets a list of a user's ssh keys
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @return {Promise<Object>} - the user's ssh keys
* @memberof RED.settings
*/
getUserKeys: function(opts) {
return new Promise(function(resolve,reject) {
var username = getSSHKeyUsername(opts.user);
runtime.storage.projects.ssh.listSSHKeys(username).then(function(list) {
return resolve(list);
}).catch(function(err) {
err.status = 400;
return reject(err);
});
});
},
/**
* Gets a user's ssh public key
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {User} opts.id - the id of the key to return
* @return {Promise<String>} - the user's ssh public key
* @memberof RED.settings
*/
getUserKey: function(opts) {
return new Promise(function(resolve,reject) {
var username = getSSHKeyUsername(opts.user);
// console.log('username:', username);
runtime.storage.projects.ssh.getSSHKey(username, opts.id).then(function(data) {
if (data) {
return resolve(data);
} else {
var err = new Error("Key not found");
err.code = "not_found";
err.status = 404;
return reject(err);
}
}).catch(function(err) {
err.status = 400;
return reject(err);
});
});
},
/**
* Generates a new ssh key pair
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {User} opts.name - the id of the key to return
* @param {User} opts.password - (optional) the password for the key pair
* @param {User} opts.comment - (option) a comment to associate with the key pair
* @param {User} opts.size - (optional) the size of the key. Default: 2048
* @return {Promise<String>} - the id of the generated key
* @memberof RED.settings
*/
generateUserKey: function(opts) {
return new Promise(function(resolve,reject) {
var username = getSSHKeyUsername(opts.user);
runtime.storage.projects.ssh.generateSSHKey(username, opts).then(function(name) {
return resolve(name);
}).catch(function(err) {
err.status = 400;
return reject(err);
});
});
},
/**
* Deletes a user's ssh key pair
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {User} opts.id - the id of the key to delete
* @return {Promise} - resolves when deleted
* @memberof RED.settings
*/
removeUserKey: function(opts) {
return new Promise(function(resolve,reject) {
var username = getSSHKeyUsername(req.user);
runtime.storage.projects.ssh.deleteSSHKey(username, opts.id).then(function() {
return resolve();
}).catch(function(err) {
err.status = 400;
return reject(err);
});
});
}
}

View File

@@ -14,22 +14,19 @@
* limitations under the License.
**/
var when = require("when");
var fs = require("fs");
var path = require("path");
var events = require("../../events");
var registry = require("./registry");
var loader = require("./loader");
var installer = require("./installer");
var library = require("./library");
var settings;
function init(runtime) {
settings = runtime.settings;
installer.init(runtime.settings);
installer.init(runtime);
loader.init(runtime);
registry.init(settings,loader);
registry.init(settings,loader,runtime.events);
library.init();
}
function load() {
@@ -51,7 +48,7 @@ function enableNodeSet(typeOrId) {
return registry.getNodeInfo(typeOrId);
});
}
return when.resolve(nodeSet);
return Promise.resolve(nodeSet);
});
}
@@ -84,5 +81,11 @@ module.exports = {
cleanModuleList: registry.cleanModuleList,
paletteEditorEnabled: installer.paletteEditorEnabled
paletteEditorEnabled: installer.paletteEditorEnabled,
getNodeExampleFlows: library.getExampleFlows,
getNodeExampleFlowPath: library.getExampleFlowPath,
deprecated: require("./deprecated")
};

View File

@@ -19,9 +19,10 @@ var path = require("path");
var fs = require("fs");
var registry = require("./registry");
var log = require("../../log");
var library = require("./library");
var log;
var events = require("../../events");
var events;
var child_process = require('child_process');
var npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
@@ -31,8 +32,10 @@ var settings;
var moduleRe = /^(@[^/]+?[/])?[^/]+?$/;
var slashRe = process.platform === "win32" ? /\\|[/]/ : /[/]/;
function init(_settings) {
settings = _settings;
function init(runtime) {
events = runtime.events;
settings = runtime.settings;
log = runtime.log;
}
var activePromise = Promise.resolve();
@@ -219,8 +222,7 @@ function uninstallModule(module) {
} else {
log.info(log._("server.install.uninstalled",{name:module}));
reportRemovedModules(list);
// TODO: tidy up internal event names
events.emit("node-module-uninstalled",module)
library.removeExamplesDir(module);
resolve(list);
}
}

View File

@@ -59,11 +59,11 @@ function getFlowsFromPath(path) {
})
}
function addNodeExamplesDir(module) {
exampleRoots[module.name] = module.path;
getFlowsFromPath(module.path).then(function(result) {
function addNodeExamplesDir(module,path) {
exampleRoots[module] = path;
return getFlowsFromPath(path).then(function(result) {
exampleFlows = exampleFlows||{d:{}};
exampleFlows.d[module.name] = result;
exampleFlows.d[module] = result;
});
}
function removeNodeExamplesDir(module) {
@@ -77,17 +77,9 @@ function removeNodeExamplesDir(module) {
}
function init(_runtime) {
runtime = _runtime;
function init() {
exampleRoots = {};
exampleFlows = null;
runtime.events.removeListener("node-examples-dir",addNodeExamplesDir);
runtime.events.on("node-examples-dir",addNodeExamplesDir);
runtime.events.removeListener("node-module-uninstalled",removeNodeExamplesDir);
runtime.events.on("node-module-uninstalled",removeNodeExamplesDir);
}
function getExampleFlows() {
@@ -103,6 +95,8 @@ function getExampleFlowPath(module,path) {
module.exports = {
init: init,
addExamplesDir: addNodeExamplesDir,
removeExamplesDir: removeNodeExamplesDir,
getExampleFlows: getExampleFlows,
getExampleFlowPath: getExampleFlowPath
}

View File

@@ -22,6 +22,8 @@ var semver = require("semver");
var localfilesystem = require("./localfilesystem");
var registry = require("./registry");
var i18n = require("../util").i18n; // TODO: separate module
var settings;
var runtime;
@@ -82,7 +84,21 @@ function createNodeApi(node) {
events: runtime.events,
util: runtime.util,
version: runtime.version,
require: requireModule
require: requireModule,
comms: {
publish: function(topic,data,retain) {
runtime.events.emit("comms",{
topic: topic,
data: data,
retain: retain
})
}
},
library: {
register: function(type) {
return runtime.library.register(node.id,type);
}
}
}
copyObjectProperties(runtime.nodes,red.nodes,["createNode","getNode","eachNode","addCredentials","getCredentials","deleteCredentials" ]);
red.nodes.registerType = function(type,constructor,opts) {
@@ -91,8 +107,6 @@ function createNodeApi(node) {
copyObjectProperties(runtime.log,red.log,null,["init"]);
copyObjectProperties(runtime.settings,red.settings,null,["init","load","reset"]);
if (runtime.adminApi) {
red.comms = runtime.adminApi.comms;
red.library = runtime.adminApi.library;
red.auth = runtime.adminApi.auth;
red.httpAdmin = runtime.adminApi.adminApp;
red.httpNode = runtime.nodeApp;
@@ -100,12 +114,6 @@ function createNodeApi(node) {
} else {
//TODO: runtime.adminApi is always stubbed if not enabled, so this block
// is unused - but may be needed for the unit tests
red.comms = {
publish: function() {}
};
red.library = {
register: function() {}
};
red.auth = {
needsPermission: function() {}
};
@@ -116,7 +124,7 @@ function createNodeApi(node) {
if (args[0].indexOf(":") === -1) {
args[0] = node.namespace+":"+args[0];
}
return runtime.i18n._.apply(null,args);
return i18n._.apply(null,args);
}
return red;
}
@@ -257,7 +265,7 @@ function loadNodeConfig(fileInfo) {
index = regExp.lastIndex;
var help = content.substring(regExp.lastIndex-match[1].length,regExp.lastIndex);
var lang = runtime.i18n.defaultLang;
var lang = i18n.defaultLang;
if ((match = langRegExp.exec(help)) !== null) {
lang = match[1];
}
@@ -288,7 +296,7 @@ function loadNodeConfig(fileInfo) {
fs.stat(path.join(path.dirname(file),"locales"),function(err,stat) {
if (!err) {
node.namespace = node.id;
runtime.i18n.registerMessageCatalog(node.id,
i18n.registerMessageCatalog(node.id,
path.join(path.dirname(file),"locales"),
path.basename(file,".js")+".json")
.then(function() {
@@ -432,10 +440,10 @@ function getNodeHelp(node,lang) {
}
if (help) {
node.help[lang] = help;
} else if (lang === runtime.i18n.defaultLang) {
} else if (lang === i18n.defaultLang) {
return null;
} else {
node.help[lang] = getNodeHelp(node, runtime.i18n.defaultLang);
node.help[lang] = getNodeHelp(node, i18n.defaultLang);
}
}
return node.help[lang];

View File

@@ -19,7 +19,8 @@ var path = require("path");
var events;
var log;
var i18n;
var i18n = require("../util").i18n; // TODO: separate module
var settings;
var disableNodePathScan = false;
@@ -29,7 +30,6 @@ function init(runtime) {
settings = runtime.settings;
events = runtime.events;
log = runtime.log;
i18n = runtime.i18n;
}
function isIncluded(name) {
@@ -228,7 +228,8 @@ function getModuleNodeFiles(module) {
var examplesDir = path.join(moduleDir,"examples");
try {
fs.statSync(examplesDir)
events.emit("node-examples-dir",{name:pkg.name,path:examplesDir});
result.examples = {path:examplesDir};
// events.emit("node-examples-dir",{name:pkg.name,path:examplesDir});
} catch(err) {
}
return result;
@@ -240,7 +241,7 @@ function getNodeFiles(disableNodePathScan) {
var nodeFiles = [];
var results;
var dir = path.resolve(__dirname + '/../../../../public/icons');
var dir = path.resolve(__dirname + '/../../public/icons');
var iconList = [{path:dir,icons:scanIconDir(dir)}];
if (settings.coreNodesDir) {
@@ -298,7 +299,8 @@ function getNodeFiles(disableNodePathScan) {
path: moduleFile.dir,
local: moduleFile.local||false,
nodes: {},
icons: nodeModuleFiles.icons
icons: nodeModuleFiles.icons,
examples: nodeModuleFiles.examples
};
if (moduleFile.package['node-red'].version) {
nodeList[moduleFile.package.name].redVersion = moduleFile.package['node-red'].version;
@@ -331,7 +333,8 @@ function getModuleFiles(module) {
name: moduleFile.package.name,
version: moduleFile.package.version,
nodes: {},
icons: nodeModuleFiles.icons
icons: nodeModuleFiles.icons,
examples: nodeModuleFiles.examples
};
if (moduleFile.package['node-red'].version) {
nodeList[moduleFile.package.name].redVersion = moduleFile.package['node-red'].version;

View File

@@ -15,17 +15,13 @@
**/
//var UglifyJS = require("uglify-js");
var util = require("util");
var when = require("when");
var path = require("path");
var fs = require("fs");
var events = require("../../events");
var library = require("./library");
var events;
var settings;
var Node;
var loader;
var nodeConfigCache = null;
@@ -35,18 +31,15 @@ var nodeConstructors = {};
var nodeTypeToId = {};
var moduleNodes = {};
function init(_settings,_loader) {
function init(_settings,_loader, _events) {
settings = _settings;
loader = _loader;
events = _events;
moduleNodes = {};
nodeTypeToId = {};
nodeConstructors = {};
nodeList = [];
nodeConfigCache = null;
Node = require("../Node");
events.removeListener("node-icon-dir",nodeIconDir);
events.on("node-icon-dir",nodeIconDir);
}
function load() {
@@ -133,7 +126,7 @@ function saveNodeList() {
if (settings.available()) {
return settings.set("nodes",moduleList);
} else {
return when.reject("Settings unavailable");
return Promise.reject("Settings unavailable");
}
}
@@ -214,6 +207,9 @@ function addModule(module) {
icon_paths[module.name] = [];
module.icons.forEach(icon=>icon_paths[module.name].push(path.resolve(icon.path)) )
}
if (module.examples) {
library.addExamplesDir(module.name,module.examples.path);
}
nodeConfigCache = null;
}
@@ -369,27 +365,6 @@ function getCaller(){
return stack[0].getFileName();
}
function inheritNode(constructor) {
if(Object.getPrototypeOf(constructor.prototype) === Object.prototype) {
util.inherits(constructor,Node);
} else {
var proto = constructor.prototype;
while(Object.getPrototypeOf(proto) !== Object.prototype) {
proto = Object.getPrototypeOf(proto);
}
//TODO: This is a partial implementation of util.inherits >= node v5.0.0
// which should be changed when support for node < v5.0.0 is dropped
// see: https://github.com/nodejs/node/pull/3455
proto.constructor.super_ = Node;
if(Object.setPrototypeOf) {
Object.setPrototypeOf(proto, Node.prototype);
} else {
// hack for node v0.10
proto.__proto__ = Node.prototype;
}
}
}
function registerNodeConstructor(nodeSet,type,constructor) {
if (nodeConstructors.hasOwnProperty(type)) {
throw new Error(type+" already registered");
@@ -397,9 +372,6 @@ function registerNodeConstructor(nodeSet,type,constructor) {
//TODO: Ensure type is known - but doing so will break some tests
// that don't have a way to register a node template ahead
// of registering the constructor
if(!(constructor.prototype instanceof Node)) {
inheritNode(constructor);
}
var nodeSetInfo = getFullNodeInfo(nodeSet);
if (nodeSetInfo) {
@@ -584,37 +556,15 @@ function setModulePendingUpdated(module,version) {
}
var icon_paths = {
"node-red":[path.resolve(__dirname + '/../../../../public/icons')]
"node-red":[path.resolve(__dirname + '/../../public/icons')]
};
var iconCache = {};
var defaultIcon = path.resolve(__dirname + '/../../../../public/icons/arrow-in.png');
function nodeIconDir(dir) {
return;
icon_paths[dir.name] = icon_paths[dir.name] || [];
icon_paths[dir.name].push(path.resolve(dir.path));
if (dir.icons) {
if (!moduleConfigs[dir.name]) {
moduleConfigs[dir.name] = {
name: dir.name,
nodes: {},
icons: []
};
}
var module = moduleConfigs[dir.name];
if (module.icons === undefined) {
module.icons = [];
}
dir.icons.forEach(function(icon) {
if (module.icons.indexOf(icon) === -1) {
module.icons.push(icon);
}
});
}
}
var defaultIcon = path.resolve(__dirname + '/../../public/icons/arrow-in.png');
function getNodeIconPath(module,icon) {
if (/\.\./.test(icon)) {
throw new Error();
}
var iconName = module+"/"+icon;
if (iconCache[iconName]) {
return iconCache[iconName];
@@ -647,7 +597,7 @@ function getNodeIcons() {
if (moduleConfigs.hasOwnProperty(module)) {
if (moduleConfigs[module].icons) {
iconList[module] = [];
moduleConfigs[module].icons.forEach(icon=>{ iconList[module] = iconList[module].concat(icon.icons) });
moduleConfigs[module].icons.forEach(icon=>{ iconList[module] = iconList[module].concat(icon.icons)})
}
}
}

View File

@@ -18,8 +18,7 @@ var when = require('when');
var redNodes = require("./nodes");
var storage = require("./storage");
var log = require("./log");
var i18n = require("./i18n");
var library = require("./library");
var events = require("./events");
var settings = require("./settings");
@@ -28,6 +27,10 @@ var path = require('path');
var fs = require("fs");
var os = require("os");
var redUtil;
var log;
var i18n;
var runtimeMetricInterval = null;
var started = false;
@@ -39,9 +42,6 @@ var stubbedExpressApp = {
delete: function() {}
}
var adminApi = {
library: {
register: function() {}
},
auth: {
needsPermission: function() {}
},
@@ -54,9 +54,12 @@ var adminApi = {
var nodeApp;
function init(userSettings,_adminApi) {
function init(userSettings,_redUtil,_adminApi) {
redUtil = _redUtil;
log = redUtil.log;
i18n = redUtil.i18n;
userSettings.version = getVersion();
log.init(userSettings);
settings.init(userSettings);
nodeApp = express();
@@ -65,6 +68,7 @@ function init(userSettings,_adminApi) {
adminApi = _adminApi;
}
redNodes.init(runtime);
library.init(runtime);
}
var version;
@@ -84,10 +88,8 @@ function getVersion() {
}
function start() {
return i18n.init()
.then(function() {
return i18n.registerMessageCatalog("runtime",path.resolve(path.join(__dirname,"locales")),"runtime.json")
})
return i18n.registerMessageCatalog("runtime",path.resolve(path.join(__dirname,"locales")),"runtime.json")
.then(function() { return storage.init(runtime)})
.then(function() { return settings.load(storage)})
.then(function() {
@@ -243,12 +245,13 @@ var runtime = module.exports = {
version: getVersion,
log: log,
i18n: i18n,
get log() { return log },
get i18n() { return i18n },
settings: settings,
storage: storage,
events: events,
nodes: redNodes,
library: library,
util: require("./util"),
get adminApi() { return adminApi },
get nodeApp() { return nodeApp },

View File

@@ -0,0 +1,103 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
var fs = require('fs');
var fspath = require('path');
var runtime;
var knownTypes = {};
var storage;
function init(_runtime) {
runtime = _runtime;
storage = runtime.storage;
knownTypes = {};
}
function registerType(id,type) {
// TODO: would like to enforce this, but currently the tests register the same type multiple
// times and have no way to remove themselves.
// if (knownTypes.hasOwnProperty(type)) {
// throw new Error(`Library type '${type}' already registered by ${id}'`)
// }
knownTypes[type] = id;
}
// function getAllEntries(type) {
// if (!knownTypes.hasOwnProperty(type)) {
// throw new Error(`Unknown library type '${type}'`);
// }
// }
function getEntry(type,path) {
if (type !== 'flows') {
if (!knownTypes.hasOwnProperty(type)) {
throw new Error(`Unknown library type '${type}'`);
}
return storage.getLibraryEntry(type,path);
} else {
return new Promise(function(resolve,reject) {
if (path.indexOf("_examples_/") === 0) {
var m = /^_examples_\/(@.*?\/[^\/]+|[^\/]+)\/(.*)$/.exec(path);
if (m) {
var module = m[1];
var entryPath = m[2];
var fullPath = runtime.nodes.getNodeExampleFlowPath(module,entryPath);
if (fullPath) {
try {
fs.readFile(fullPath,'utf8',function(err, data) {
runtime.log.audit({event: "library.get",type:"flow",path:path});
if (err) {
return reject(err);
}
return resolve(data);
})
} catch(err) {
return reject(err);
}
return;
}
}
// IF we get here, we didn't find the file
var error = new Error("not_found");
error.code = "not_found";
return reject(error);
} else {
resolve(storage.getFlow(path));
}
});
}
}
function saveEntry(type,path,meta,body) {
if (type !== 'flows') {
if (!knownTypes.hasOwnProperty(type)) {
throw new Error(`Unknown library type '${type}'`);
}
return storage.saveLibraryEntry(type,path,meta,body);
} else {
return storage.saveFlow(path,body);
}
}
module.exports = {
init: init,
register: registerType,
// getAllEntries: getAllEntries,
getEntry: getEntry,
saveEntry: saveEntry
}

View File

@@ -19,7 +19,7 @@ var EventEmitter = require("events").EventEmitter;
var when = require("when");
var redUtil = require("../util");
var Log = require("../log");
var Log = require("../../util").log; // TODO: separate module
var context = require("./context");
var flows = require("./flows");

View File

@@ -15,7 +15,7 @@
**/
var clone = require("clone");
var log = require("../../log");
var log = require("../../../util").log; // TODO: separate module
var memory = require("./memory");
var settings;

View File

@@ -20,6 +20,7 @@ var runtime;
var settings;
var log;
var encryptedCredentials = null;
var credentialCache = {};
var credentialsDef = {};

View File

@@ -16,10 +16,11 @@
var when = require("when");
var clone = require("clone");
var typeRegistry = require("../registry");
var Log = require("../../log");
var typeRegistry = require("../../../runtime-registry");
var Log;
var redUtil = require("../../util");
var flowUtil = require("./util");
var Node;
var nodeCloseTimeout = 15000;
@@ -292,6 +293,7 @@ function Flow(global,flow) {
function createNode(type,config) {
var nn = null;
try {
var nt = typeRegistry.get(type);
if (nt) {
var conf = clone(config);
@@ -315,6 +317,9 @@ function createNode(type,config) {
} else {
Log.error(Log._("nodes.flow.unknown-type", {type:type}));
}
} catch(err) {
Log.error(err);
}
return nn;
}
@@ -495,8 +500,10 @@ function createSubflow(sf,sfn,subflows,globalSubflows,activeNodes) {
module.exports = {
init: function(settings) {
nodeCloseTimeout = settings.nodeCloseTimeout || 15000;
init: function(runtime) {
nodeCloseTimeout = runtime.settings.nodeCloseTimeout || 15000;
Log = runtime.log;
Node = require("../Node");
},
create: function(global,conf) {
return new Flow(global,conf);

View File

@@ -19,15 +19,17 @@ var when = require("when");
var Flow = require('./Flow');
var typeRegistry = require("../registry");
var typeRegistry = require("../../../runtime-registry");
var deprecated = typeRegistry.deprecated;
var context = require("../context")
var credentials = require("../credentials");
var flowUtil = require("./util");
var log = require("../../log");
var log;
var events = require("../../events");
var redUtil = require("../../util");
var deprecated = require("../registry/deprecated");
var storage = null;
var settings = null;
@@ -50,6 +52,7 @@ function init(runtime) {
}
settings = runtime.settings;
storage = runtime.storage;
log = runtime.log;
started = false;
if (!typeEventRegistered) {
events.on('type-registered',function(type) {
@@ -67,7 +70,7 @@ function init(runtime) {
});
typeEventRegistered = true;
}
Flow.init(settings);
Flow.init(runtime);
}
function loadFlows() {

View File

@@ -16,7 +16,7 @@
var clone = require("clone");
var redUtil = require("../../util");
var subflowInstanceRE = /^subflow:(.+)$/;
var typeRegistry = require("../registry");
var typeRegistry = require("../../../runtime-registry");
function diffNodes(oldNode,newNode) {
if (oldNode == null) {

View File

@@ -18,15 +18,16 @@ var when = require("when");
var path = require("path");
var fs = require("fs");
var clone = require("clone");
var util = require("util");
var registry = require("../../runtime-registry");
var registry = require("./registry");
var credentials = require("./credentials");
var flows = require("./flows");
var flowUtil = require("./flows/util")
var context = require("./context");
var Node = require("./Node");
var log = null;
var library = require("./library");
var log;
var events = require("../events");
@@ -61,6 +62,26 @@ function registerType(nodeSet,type,constructor,opts) {
}
}
}
if(!(constructor.prototype instanceof Node)) {
if(Object.getPrototypeOf(constructor.prototype) === Object.prototype) {
util.inherits(constructor,Node);
} else {
var proto = constructor.prototype;
while(Object.getPrototypeOf(proto) !== Object.prototype) {
proto = Object.getPrototypeOf(proto);
}
//TODO: This is a partial implementation of util.inherits >= node v5.0.0
// which should be changed when support for node < v5.0.0 is dropped
// see: https://github.com/nodejs/node/pull/3455
proto.constructor.super_ = Node;
if(Object.setPrototypeOf) {
Object.setPrototypeOf(proto, Node.prototype);
} else {
// hack for node v0.10
proto.__proto__ = Node.prototype;
}
}
}
registry.registerType(nodeSet,type,constructor);
}
@@ -99,7 +120,6 @@ function init(runtime) {
flows.init(runtime);
registry.init(runtime);
context.init(runtime.settings);
library.init(runtime);
}
function disableNode(id) {
@@ -190,8 +210,8 @@ module.exports = {
getNodeConfig: registry.getNodeConfig,
getNodeIconPath: registry.getNodeIconPath,
getNodeIcons: registry.getNodeIcons,
getNodeExampleFlows: library.getExampleFlows,
getNodeExampleFlowPath: library.getExampleFlowPath,
getNodeExampleFlows: registry.getNodeExampleFlows,
getNodeExampleFlowPath: registry.getNodeExampleFlowPath,
clearRegistry: registry.clear,
cleanModuleList: registry.cleanModuleList,

View File

@@ -17,7 +17,7 @@
var when = require("when");
var clone = require("clone");
var assert = require("assert");
var log = require("./log");
var log = require("../util").log; // TODO: separate module
var util = require("./util");
// localSettings are those provided in the runtime settings.js file

View File

@@ -18,7 +18,7 @@ var when = require('when');
var Path = require('path');
var crypto = require('crypto');
var log = require("../log");
var log = require("../../util").log; // TODO: separate module
var runtime;
var storageModule;

View File

@@ -18,7 +18,8 @@ var fs = require('fs-extra');
var when = require('when');
var fspath = require("path");
var log = require("../../log");
var log = require("../../../util").log; // TODO: separate module
var util = require("./util");
var library = require("./library");
var sessions = require("./sessions");

View File

@@ -107,6 +107,11 @@ function getSSHKey(username, name) {
function generateSSHKey(username, options) {
options = options || {};
var name = options.name || "";
if (!/^[a-zA-Z0-9\-_]+$/.test(options.name)) {
var err = new Error("Invalid SSH Key name");
e.code = "invalid_key_name";
return Promise.reject(err);
}
return checkExistSSHKeyFiles(username, name)
.then(function(result) {
if ( result ) {

View File

@@ -18,7 +18,8 @@ var when = require('when');
var fs = require('fs-extra');
var fspath = require("path");
var log = require("../../log");
var log = require("../../../util").log; // TODO: separate module
var util = require("./util");
var sessionsFile;

View File

@@ -18,7 +18,7 @@ var when = require('when');
var fs = require('fs-extra');
var fspath = require("path");
var log = require("../../log");
var log = require("../../../util").log; // TODO: separate module
var util = require("./util");
var globalSettingsFile;

View File

@@ -18,7 +18,7 @@ var fs = require('fs-extra');
var when = require('when');
var nodeFn = require('when/node/function');
var log = require("../../log");
var log = require("../../../util").log; // TODO: separate module
function parseJSON(data) {
if (data.charCodeAt(0) === 0xFEFF) {

View File

@@ -24,6 +24,7 @@ var defaultLang = "en-US";
var resourceMap = {};
var resourceCache = {};
var initPromise;
function registerMessageCatalogs(catalogs) {
var promises = catalogs.map(function(catalog) {
@@ -33,10 +34,12 @@ function registerMessageCatalogs(catalogs) {
}
function registerMessageCatalog(namespace,dir,file) {
return when.promise(function(resolve,reject) {
resourceMap[namespace] = { basedir:dir, file:file};
i18n.loadNamespaces(namespace,function() {
resolve();
return initPromise.then(function() {
return new Promise((resolve,reject) => {
resourceMap[namespace] = { basedir:dir, file:file};
i18n.loadNamespaces(namespace,function() {
resolve();
});
});
});
}
@@ -96,28 +99,32 @@ function getCurrentLocale() {
}
function init() {
return when.promise(function(resolve,reject) {
i18n.use(MessageFileLoader);
var opt = {
// debug: true,
defaultNS: "runtime",
ns: [],
fallbackLng: defaultLang,
interpolation: {
unescapeSuffix: 'HTML',
escapeValue: false,
prefix: '__',
suffix: '__'
if (!initPromise) {
// Keep this as a 'when' promise as top-level red.js uses 'otherwise'
// and embedded users of NR may have copied that.
initPromise = when.promise((resolve,reject) => {
i18n.use(MessageFileLoader);
var opt = {
// debug: true,
defaultNS: "runtime",
ns: [],
fallbackLng: defaultLang,
interpolation: {
unescapeSuffix: 'HTML',
escapeValue: false,
prefix: '__',
suffix: '__'
}
};
var lang = getCurrentLocale();
if (lang) {
opt.lng = lang;
}
};
var lang = getCurrentLocale();
if (lang) {
opt.lng = lang;
}
i18n.init(opt ,function() {
resolve();
i18n.init(opt ,function() {
resolve();
});
});
});
}
}
function getCatalog(namespace,lang) {

28
red/util/index.js Normal file
View File

@@ -0,0 +1,28 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
var log = require("./log");
var i18n = require("./i18n");
module.exports = {
init: function(settings) {
log.init(settings);
i18n.init();
},
log: log,
i18n: i18n
}

View File

@@ -26,6 +26,9 @@ var Context = require("../../../../red/runtime/nodes/context");
var Util = require("../../../../red/runtime/util");
describe("api/admin/context", function() {
it.skip("NEEDS TESTS WRITING",function() {});
});
/*
var app = undefined;
before(function (done) {
@@ -58,7 +61,7 @@ describe("api/admin/context", function() {
},
util: Util
});
Context.init({
contextStorage: {
memory0: {
@@ -103,7 +106,7 @@ describe("api/admin/context", function() {
function check_scope(scope, prefix, id) {
describe('# '+scope, function () {
var xid = id ? ("/"+id) : "";
it('should return '+scope+' contexts', function (done) {
request(app)
.get('/context/'+scope+xid)
@@ -229,3 +232,4 @@ describe("api/admin/context", function() {
});
});
*/

View File

@@ -38,18 +38,23 @@ describe("api/admin/flow", function() {
describe("get", function() {
before(function() {
var opts;
flow.init({
settings:{},
nodes: {
getFlow: function(id) {
if (id === '123') {
return {id:'123'}
flows: {
getFlow: function(_opts) {
opts = _opts;
if (opts.id === '123') {
return Promise.resolve({id:'123'});
} else {
return null;
var err = new Error("message");
err.code = "not_found";
err.status = 404;
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
}
},
log:{ audit: sinon.stub() }
}
});
})
it('gets a known flow', function(done) {
@@ -75,19 +80,24 @@ describe("api/admin/flow", function() {
});
describe("add", function() {
var opts;
before(function() {
flow.init({
settings:{},
nodes: {
addFlow: function(f) {
if (f.id === "123") {
return when.resolve('123')
flows: {
addFlow: function(_opts) {
opts = _opts;
if (opts.flow.id === "123") {
return Promise.resolve('123')
} else {
return when.reject(new Error("test error"));
var err = new Error("random error");
err.code = "random_error";
err.status = 400;
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
}
},
log:{ audit: sinon.stub() }
}
});
})
it('adds a new flow', function(done) {
@@ -114,8 +124,8 @@ describe("api/admin/flow", function() {
if (err) {
return done(err);
}
res.body.should.has.a.property('error','unexpected_error');
res.body.should.has.a.property('message','Error: test error');
res.body.should.has.a.property('code','random_error');
res.body.should.has.a.property('message','random error');
done();
});
@@ -123,35 +133,29 @@ describe("api/admin/flow", function() {
})
describe("update", function() {
var nodes;
var opts;
before(function() {
nodes = {
updateFlow: function(id,f) {
var err;
if (id === "123") {
return when.resolve()
} else if (id === "unknown") {
err = new Error();
err.code = 404;
throw err;
} else if (id === "unexpected") {
err = new Error();
err.code = 500;
throw err;
} else {
return when.reject(new Error("test error"));
flow.init({
flows: {
updateFlow: function(_opts) {
opts = _opts;
if (opts.id === "123") {
return Promise.resolve('123')
} else {
var err = new Error("random error");
err.code = "random_error";
err.status = 400;
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
}
}
};
flow.init({
settings:{},
nodes: nodes,
log:{ audit: sinon.stub() }
});
})
it('updates an existing flow', function(done) {
sinon.spy(nodes,"updateFlow");
request(app)
.put('/flow/123')
.set('Accept', 'application/json')
@@ -162,115 +166,79 @@ describe("api/admin/flow", function() {
return done(err);
}
res.body.should.has.a.property('id','123');
nodes.updateFlow.calledOnce.should.be.true();
nodes.updateFlow.lastCall.args[0].should.eql('123');
nodes.updateFlow.lastCall.args[1].should.eql({id:'123'});
nodes.updateFlow.restore();
opts.should.have.property('id','123');
opts.should.have.property('flow',{id:'123'})
done();
});
})
it('404s on an unknown flow', function(done) {
it('400 an invalid flow', function(done) {
request(app)
.put('/flow/unknown')
.put('/flow/456')
.set('Accept', 'application/json')
.send({id:'123'})
.expect(404)
.end(done);
})
it('400 on async update error', function(done) {
request(app)
.put('/flow/async_error')
.set('Accept', 'application/json')
.send({id:'123'})
.send({id:'456'})
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.has.a.property('error','unexpected_error');
res.body.should.has.a.property('message','Error: test error');
done();
});
})
res.body.should.has.a.property('code','random_error');
res.body.should.has.a.property('message','random error');
it('400 on sync update error', function(done) {
request(app)
.put('/flow/unexpected')
.set('Accept', 'application/json')
.send({id:'123'})
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.has.a.property('error',500);
res.body.should.has.a.property('message','Error');
done();
});
})
})
describe("delete", function() {
var nodes;
var opts;
before(function() {
nodes = {
removeFlow: function(id) {
var err;
if (id === "123") {
return when.resolve()
} else if (id === "unknown") {
err = new Error();
err.code = 404;
throw err;
} else if (id === "unexpected") {
err = new Error();
err.code = 500;
throw err;
flow.init({
flows: {
deleteFlow: function(_opts) {
opts = _opts;
if (opts.id === "123") {
return Promise.resolve()
} else {
var err = new Error("random error");
err.code = "random_error";
err.status = 400;
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
}
}
};
flow.init({
settings:{},
nodes: nodes,
log:{ audit: sinon.stub() }
});
})
it('updates an existing flow', function(done) {
sinon.spy(nodes,"removeFlow");
it('deletes an existing flow', function(done) {
request(app)
.delete('/flow/123')
.del('/flow/123')
.set('Accept', 'application/json')
.expect(204)
.end(function(err,res) {
if (err) {
return done(err);
}
nodes.removeFlow.calledOnce.should.be.true();
nodes.removeFlow.lastCall.args[0].should.eql('123');
nodes.removeFlow.restore();
opts.should.have.property('id','123');
done();
});
})
it('404s on an unknown flow', function(done) {
it('400 an invalid flow', function(done) {
request(app)
.delete('/flow/unknown')
.expect(404)
.end(done);
})
it('400 on remove error', function(done) {
request(app)
.delete('/flow/unexpected')
.del('/flow/456')
.set('Accept', 'application/json')
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.has.a.property('error',500);
res.body.should.has.a.property('message','Error');
res.body.should.has.a.property('code','random_error');
res.body.should.has.a.property('message','random error');
done();
});
})

View File

@@ -19,7 +19,6 @@ var request = require('supertest');
var express = require('express');
var bodyParser = require('body-parser');
var sinon = require('sinon');
var when = require('when');
var flows = require("../../../../red/api/admin/flows");
@@ -36,10 +35,8 @@ describe("api/admin/flows", function() {
it('returns flow - v1', function(done) {
flows.init({
settings: {},
log:{warn:function(){},_:function(){},audit:function(){}},
nodes:{
getFlows: function() { return {rev:"123",flows:[1,2,3]}; }
flows:{
getFlows: function() { return Promise.resolve({rev:"123",flows:[1,2,3]}); }
}
});
request(app)
@@ -60,10 +57,8 @@ describe("api/admin/flows", function() {
});
it('returns flow - v2', function(done) {
flows.init({
settings: {},
log:{warn:function(){},_:function(){},audit:function(){}},
nodes:{
getFlows: function() { return {rev:"123",flows:[1,2,3]}; }
flows:{
getFlows: function() { return Promise.resolve({rev:"123",flows:[1,2,3]}); }
}
});
request(app)
@@ -104,10 +99,9 @@ describe("api/admin/flows", function() {
});
});
it('sets flows - default - v1', function(done) {
var setFlows = sinon.spy(function() { return when.resolve();});
var setFlows = sinon.spy(function() { return Promise.resolve();});
flows.init({
log:{warn:function(){},_:function(){},audit:function(){}},
nodes:{
flows:{
setFlows: setFlows
}
});
@@ -120,15 +114,14 @@ describe("api/admin/flows", function() {
return done(err);
}
setFlows.calledOnce.should.be.true();
setFlows.lastCall.args[1].should.eql('full');
setFlows.lastCall.args[0].should.have.property('deploymentType','full');
done();
});
});
it('sets flows - non-default - v1', function(done) {
var setFlows = sinon.spy(function() { return when.resolve();});
var setFlows = sinon.spy(function() { return Promise.resolve();});
flows.init({
log:{warn:function(){},_:function(){},audit:function(){}},
nodes:{
flows:{
setFlows: setFlows
}
});
@@ -142,19 +135,22 @@ describe("api/admin/flows", function() {
return done(err);
}
setFlows.calledOnce.should.be.true();
setFlows.lastCall.args[1].should.eql('nodes');
setFlows.lastCall.args[0].should.have.property('deploymentType','nodes');
done();
});
});
it('set flows - rejects mismatched revision - v2', function(done) {
var setFlows = sinon.spy(function() { return when.resolve();});
var getFlows = sinon.spy(function() { return {rev:123,flows:[1,2,3]}});
flows.init({
log:{warn:function(){},_:function(){},audit:function(){}},
nodes:{
setFlows: setFlows,
getFlows: getFlows
flows:{
setFlows: function() {
var err = new Error("mismatch");
err.code = "version_mismatch";
err.status = 409;
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
}
});
request(app)
@@ -171,54 +167,6 @@ describe("api/admin/flows", function() {
done();
});
});
it('set flows - rev provided - v2', function(done) {
var setFlows = sinon.spy(function() { return when.resolve(456);});
var getFlows = sinon.spy(function() { return {rev:123,flows:[1,2,3]}});
flows.init({
log:{warn:function(){},_:function(){},audit:function(){}},
nodes:{
setFlows: setFlows,
getFlows: getFlows
}
});
request(app)
.post('/flows')
.set('Accept', 'application/json')
.set('Node-RED-API-Version','v2')
.send({rev:123,flows:[4,5,6]})
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property("rev",456);
done();
});
});
it('set flows - no rev provided - v2', function(done) {
var setFlows = sinon.spy(function() { return when.resolve(456);});
var getFlows = sinon.spy(function() { return {rev:123,flows:[1,2,3]}});
flows.init({
log:{warn:function(){},_:function(){},audit:function(){}},
nodes:{
setFlows: setFlows,
getFlows: getFlows
}
});
request(app)
.post('/flows')
.set('Accept', 'application/json')
.set('Node-RED-API-Version','v2')
.send({flows:[4,5,6]})
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property("rev",456);
done();
});
});
it('sets flow - bad version', function(done) {
request(app)
.post('/flows')
@@ -238,11 +186,10 @@ describe("api/admin/flows", function() {
});
});
it('reloads flows', function(done) {
var loadFlows = sinon.spy(function() { return when.resolve(); });
var setFlows = sinon.spy(function() { return Promise.resolve();});
flows.init({
log:{warn:function(){},_:function(){},audit:function(){}},
nodes:{
loadFlows: loadFlows
flows:{
setFlows: setFlows
}
});
request(app)
@@ -254,29 +201,9 @@ describe("api/admin/flows", function() {
if (err) {
return done(err);
}
loadFlows.called.should.be.true();
setFlows.called.should.be.true();
setFlows.lastCall.args[0].should.not.have.property('flows');
done();
});
});
it('returns error when set fails', function(done) {
flows.init({
log:{warn:function(){},_:function(){},audit:function(){}},
nodes:{
setFlows: function() { return when.reject(new Error("expected error")); }
}
});
request(app)
.post('/flows')
.set('Accept', 'application/json')
.expect(500)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property("message","expected error");
done();
});
});
});

View File

@@ -66,7 +66,8 @@ describe("api/admin/index", function() {
sinon.stub(nodes,"delete",stubApp);
sinon.stub(nodes,"getSet",stubApp);
sinon.stub(nodes,"putSet",stubApp);
sinon.stub(nodes,"getModuleCatalog",stubApp);
sinon.stub(nodes,"getModuleCatalogs",stubApp);
});
after(function() {
mockList.forEach(function(m) {
@@ -87,6 +88,8 @@ describe("api/admin/index", function() {
nodes.delete.restore();
nodes.getSet.restore();
nodes.putSet.restore();
nodes.getModuleCatalog.restore();
nodes.getModuleCatalogs.restore();
});
@@ -281,5 +284,36 @@ describe("api/admin/index", function() {
done();
})
});
it('GET /nodes/messages', function(done) {
request(app).get("/nodes/messages").expect(200).end(function(err,res) {
if (err) {
return done(err);
}
permissionChecks.should.have.property('nodes.read',1);
done();
})
});
it('GET /nodes/module/set/messages', function(done) {
request(app).get("/nodes/module/set/messages").expect(200).end(function(err,res) {
if (err) {
return done(err);
}
permissionChecks.should.have.property('nodes.read',1);
lastRequest.params.should.have.property(0,'module/set');
done();
})
});
it('GET /nodes/@scope/module/set/messages', function(done) {
request(app).get("/nodes/@scope/module/set/messages").expect(200).end(function(err,res) {
if (err) {
return done(err);
}
permissionChecks.should.have.property('nodes.read',1);
lastRequest.params.should.have.property(0,'@scope/module/set');
done();
})
});
});
});

View File

@@ -27,31 +27,19 @@ var apiUtil = require("../../../../red/api/util");
describe("api/admin/nodes", function() {
var app;
function initNodes(runtime) {
runtime.log = {
audit:function(e){},//console.log(e)},
_:function(){},
info: function(){},
warn: function(){}
}
runtime.events = {
emit: function(){}
}
nodes.init(runtime);
}
before(function() {
app = express();
app.use(bodyParser.json());
app.get("/nodes",nodes.getAll);
app.post("/nodes",nodes.post);
app.get(/\/nodes\/messages/,nodes.getModuleCatalogs);
app.get(/\/nodes\/((@[^\/]+\/)?[^\/]+\/[^\/]+)\/messages/,nodes.getModuleCatalog);
app.get(/\/nodes\/((@[^\/]+\/)?[^\/]+)$/,nodes.getModule);
app.get(/\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,nodes.getSet);
app.put(/\/nodes\/((@[^\/]+\/)?[^\/]+)$/,nodes.putModule);
app.get(/\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,nodes.getSet);
app.put(/\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,nodes.putSet);
app.get("/getIcons",nodes.getIcons);
app.delete("/nodes/:id",nodes.delete);
app.delete(/\/nodes\/((@[^\/]+\/)?[^\/]+)$/,nodes.delete);
sinon.stub(apiUtil,"determineLangFromHeaders", function() {
return "en-US";
});
@@ -62,10 +50,10 @@ describe("api/admin/nodes", function() {
describe('get nodes', function() {
it('returns node list', function(done) {
initNodes({
nodes.init({
nodes:{
getNodeList: function() {
return [1,2,3];
return Promise.resolve([1,2,3]);
}
}
});
@@ -84,10 +72,10 @@ describe("api/admin/nodes", function() {
});
it('returns node configs', function(done) {
initNodes({
nodes.init({
nodes:{
getNodeConfigs: function() {
return "<script></script>";
return Promise.resolve("<script></script>");
}
},
i18n: {
@@ -108,10 +96,10 @@ describe("api/admin/nodes", function() {
});
it('returns node module info', function(done) {
initNodes({
nodes.init({
nodes:{
getModuleInfo: function(id) {
return {"node-red":{name:"node-red"}}[id];
getModuleInfo: function(opts) {
return Promise.resolve({"node-red":{name:"node-red"}}[opts.module]);
}
}
});
@@ -128,10 +116,15 @@ describe("api/admin/nodes", function() {
});
it('returns 404 for unknown module', function(done) {
initNodes({
nodes.init({
nodes:{
getModuleInfo: function(id) {
return {"node-red":{name:"node-red"}}[id];
getModuleInfo: function(opts) {
var errInstance = new Error("Not Found");
errInstance.code = "not_found";
errInstance.status = 404;
var p = Promise.reject(errInstance);
p.catch(()=>{});
return p;
}
}
});
@@ -147,10 +140,10 @@ describe("api/admin/nodes", function() {
});
it('returns individual node info', function(done) {
initNodes({
nodes.init({
nodes:{
getNodeInfo: function(id) {
return {"node-red/123":{id:"node-red/123"}}[id];
getNodeInfo: function(opts) {
return Promise.resolve({"node-red/123":{id:"node-red/123"}}[opts.id]);
}
}
});
@@ -168,10 +161,10 @@ describe("api/admin/nodes", function() {
});
it('returns individual node configs', function(done) {
initNodes({
nodes.init({
nodes:{
getNodeConfig: function(id) {
return {"node-red/123":"<script></script>"}[id];
getNodeConfig: function(opts) {
return Promise.resolve({"node-red/123":"<script></script>"}[opts.id]);
}
},
i18n: {
@@ -190,12 +183,16 @@ describe("api/admin/nodes", function() {
done();
});
});
it('returns 404 for unknown node', function(done) {
initNodes({
nodes.init({
nodes:{
getNodeInfo: function(id) {
return {"node-red/123":{id:"node-red/123"}}[id];
getNodeInfo: function(opts) {
var errInstance = new Error("Not Found");
errInstance.code = "not_found";
errInstance.status = 404;
var p = Promise.reject(errInstance);
p.catch(()=>{});
return p;
}
}
});
@@ -213,142 +210,96 @@ describe("api/admin/nodes", function() {
});
describe('install', function() {
it('returns 400 if settings are unavailable', function(done) {
initNodes({
settings:{available:function(){return false}}
it('installs the module and returns module info', function(done) {
var opts;
nodes.init({
nodes:{
addModule: function(_opts) {
opts = _opts;
return Promise.resolve({
name:"foo",
nodes:[{id:"123"}]
});
}
}
});
request(app)
.post('/nodes')
.send({module: 'foo',version:"1.2.3"})
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.have.property("name","foo");
res.body.should.have.property("nodes");
res.body.nodes[0].should.have.property("id","123");
opts.should.have.property("module","foo");
opts.should.have.property("version","1.2.3");
done();
});
});
it('returns error', function(done) {
nodes.init({
nodes:{
addModule: function(opts) {
var errInstance = new Error("Message");
errInstance.code = "random_error";
errInstance.status = 400;
var p = Promise.reject(errInstance);
p.catch(()=>{});
return p;
}
}
});
request(app)
.post('/nodes')
.send({module: 'foo',version:"1.2.3"})
.expect(400)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.have.a.property('code','random_error');
done();
});
});
it('returns 400 if request is invalid', function(done) {
initNodes({
settings:{available:function(){return true}}
});
request(app)
.post('/nodes')
.send({})
.expect(400)
.end(function(err,res) {
if (err) {
throw err;
}
done();
});
});
describe('by module', function() {
it('installs the module and returns module info', function(done) {
initNodes({
settings:{available:function(){return true}},
nodes:{
getModuleInfo: function(id) { return null; },
installModule: function() {
return when.resolve({
name:"foo",
nodes:[{id:"123"}]
});
}
}
});
request(app)
.post('/nodes')
.send({module: 'foo'})
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.have.property("name","foo");
res.body.should.have.property("nodes");
res.body.nodes[0].should.have.property("id","123");
done();
});
});
it('fails the install if already installed', function(done) {
initNodes({
settings:{available:function(){return true}},
nodes:{
getModuleInfo: function(id) { return {nodes:{id:"123"}}; },
installModule: function() {
return when.resolve({id:"123"});
}
}
});
request(app)
.post('/nodes')
.send({module: 'foo'})
.expect(400)
.end(function(err,res) {
if (err) {
throw err;
}
done();
});
});
it('fails the install if module error', function(done) {
initNodes({
settings:{available:function(){return true}},
nodes:{
getModuleInfo: function(id) { return null },
installModule: function() {
return when.reject(new Error("test error"));
}
}
});
request(app)
.post('/nodes')
.send({module: 'foo'})
.expect(400)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.have.property("message","Error: test error");
done();
});
});
it('fails the install if module not found', function(done) {
initNodes({
settings:{available:function(){return true}},
nodes:{
getModuleInfo: function(id) { return null },
installModule: function() {
var err = new Error("test error");
err.code = 404;
return when.reject(err);
}
}
});
request(app)
.post('/nodes')
.send({module: 'foo'})
.expect(404)
.end(function(err,res) {
if (err) {
throw err;
}
done();
});
});
});
});
describe('delete', function() {
it('returns 400 if settings are unavailable', function(done) {
initNodes({
settings:{available:function(){return false}}
it('uninstalls the module', function(done) {
var opts;
nodes.init({
nodes:{
removeModule: function(_opts) {
opts = _opts;
return Promise.resolve();
}
}
});
request(app)
.del('/nodes/123')
.expect(204)
.end(function(err,res) {
if (err) {
throw err;
}
opts.should.have.property("module","123");
done();
});
});
it('returns error', function(done) {
nodes.init({
nodes:{
removeModule: function(opts) {
var errInstance = new Error("Message");
errInstance.code = "random_error";
errInstance.status = 400;
var p = Promise.reject(errInstance);
p.catch(()=>{});
return p;
}
}
});
request(app)
.del('/nodes/123')
.expect(400)
@@ -356,93 +307,18 @@ describe("api/admin/nodes", function() {
if (err) {
throw err;
}
res.body.should.have.a.property('code','random_error');
done();
});
});
describe('by module', function() {
it('uninstalls the module', function(done) {
initNodes({
settings:{available:function(){return true}},
nodes:{
getModuleInfo: function(id) { return {nodes:[{id:"123"}]} },
getNodeInfo: function() { return null },
uninstallModule: function() { return when.resolve({id:"123"});}
}
});
request(app)
.del('/nodes/foo')
.expect(204)
.end(function(err,res) {
if (err) {
throw err;
}
done();
});
});
it('fails the uninstall if the module is not installed', function(done) {
initNodes({
settings:{available:function(){return true}},
nodes:{
getModuleInfo: function(id) { return null },
getNodeInfo: function() { return null }
}
});
request(app)
.del('/nodes/foo')
.expect(404)
.end(function(err,res) {
if (err) {
throw err;
}
done();
});
});
it('fails the uninstall if the module is not installed', function(done) {
initNodes({
settings:{available:function(){return true}},
nodes:{
getModuleInfo: function(id) { return {nodes:[{id:"123"}]} },
getNodeInfo: function() { return null },
uninstallModule: function() { return when.reject(new Error("test error"));}
}
});
request(app)
.del('/nodes/foo')
.expect(400)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.have.property("message","Error: test error");
done();
});
});
});
});
describe('enable/disable', function() {
it('returns 400 if settings are unavailable', function(done) {
initNodes({
settings:{available:function(){return false}}
});
request(app)
.put('/nodes/123')
.expect(400)
.end(function(err,res) {
if (err) {
throw err;
}
done();
});
});
it('returns 400 for invalid node payload', function(done) {
initNodes({
settings:{available:function(){return true}}
describe('enable/disable node set', function() {
it('returns 400 for invalid request payload', function(done) {
nodes.init({
nodes:{
setNodeSetState: function(opts) {return Promise.resolve()}
}
});
request(app)
.put('/nodes/node-red/foo')
@@ -452,77 +328,23 @@ describe("api/admin/nodes", function() {
if (err) {
throw err;
}
res.body.should.have.property("code","invalid_request");
res.body.should.have.property("message","Invalid request");
done();
});
});
it('returns 400 for invalid module payload', function(done) {
initNodes({
settings:{available:function(){return true}}
});
request(app)
.put('/nodes/foo')
.send({})
.expect(400)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.have.property("message","Invalid request");
done();
});
});
it('returns 404 for unknown node', function(done) {
initNodes({
settings:{available:function(){return true}},
it('sets node state and returns node info', function(done) {
var opts;
nodes.init({
nodes:{
getNodeInfo: function() { return null }
setNodeSetState: function(_opts) {
opts = _opts;
return Promise.resolve({id:"123",enabled: true });
}
}
});
request(app)
.put('/nodes/node-red/foo')
.send({enabled:false})
.expect(404)
.end(function(err,res) {
if (err) {
throw err;
}
done();
});
});
it('returns 404 for unknown module', function(done) {
initNodes({
settings:{available:function(){return true}},
nodes:{
getModuleInfo: function(id) { return null }
}
});
request(app)
.put('/nodes/node-blue')
.send({enabled:false})
.expect(404)
.end(function(err,res) {
if (err) {
throw err;
}
done();
});
});
it('enables disabled node', function(done) {
initNodes({
settings:{available:function(){return true}},
nodes:{
getNodeInfo: function() { return {id:"123",enabled: false} },
enableNode: function() { return when.resolve({id:"123",enabled: true,types:['a']}); }
}
});
request(app)
.put('/nodes/node-red/foo')
.send({enabled:true})
@@ -533,130 +355,41 @@ describe("api/admin/nodes", function() {
}
res.body.should.have.property("id","123");
res.body.should.have.property("enabled",true);
opts.should.have.property("enabled",true);
opts.should.have.property("id","node-red/foo");
done();
});
});
it('disables enabled node', function(done) {
initNodes({
settings:{available:function(){return true}},
});
describe('enable/disable module' ,function() {
it('returns 400 for invalid request payload', function(done) {
nodes.init({
nodes:{
getNodeInfo: function() { return {id:"123",enabled: true} },
disableNode: function() { return when.resolve({id:"123",enabled: false,types:['a']}); }
setModuleState: function(opts) {return Promise.resolve()}
}
});
request(app)
.put('/nodes/node-red/foo')
.send({enabled:false})
.expect(200)
.put('/nodes/node-red')
.send({})
.expect(400)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.have.property("id","123");
res.body.should.have.property("enabled",false);
res.body.should.have.property("code","invalid_request");
res.body.should.have.property("message","Invalid request");
done();
});
});
describe('no-ops if already in the right state', function() {
function run(state,done) {
var enableNode = sinon.spy(function() { return when.resolve({id:"123",enabled: true,types:['a']}) });
var disableNode = sinon.spy(function() { return when.resolve({id:"123",enabled: false,types:['a']}) });
initNodes({
settings:{available:function(){return true}},
nodes:{
getNodeInfo: function() { return {id:"123",enabled: state} },
enableNode: enableNode,
disableNode: disableNode
}
});
request(app)
.put('/nodes/node-red/foo')
.send({enabled:state})
.expect(200)
.end(function(err,res) {
var enableNodeCalled = enableNode.called;
var disableNodeCalled = disableNode.called;
if (err) {
throw err;
}
enableNodeCalled.should.be.false();
disableNodeCalled.should.be.false();
res.body.should.have.property("id","123");
res.body.should.have.property("enabled",state);
done();
});
}
it('already enabled', function(done) {
run(true,done);
});
it('already disabled', function(done) {
run(false,done);
});
});
describe('does not no-op if err on node', function() {
function run(state,done) {
var enableNode = sinon.spy(function() { return when.resolve({id:"123",enabled: true,types:['a']}) });
var disableNode = sinon.spy(function() { return when.resolve({id:"123",enabled: false,types:['a']}) });
initNodes({
settings:{available:function(){return true}},
nodes:{
getNodeInfo: function() { return {id:"123",enabled: state, err:"foo"} },
enableNode: enableNode,
disableNode: disableNode
}
});
request(app)
.put('/nodes/node-red/foo')
.send({enabled:state})
.expect(200)
.end(function(err,res) {
var enableNodeCalled = enableNode.called;
var disableNodeCalled = disableNode.called;
if (err) {
throw err;
}
enableNodeCalled.should.be.equal(state);
disableNodeCalled.should.be.equal(!state);
res.body.should.have.property("id","123");
res.body.should.have.property("enabled",state);
done();
});
}
it('already enabled', function(done) {
run(true,done);
});
it('already disabled', function(done) {
run(false,done);
});
});
it('enables disabled module', function(done) {
var n1 = {id:"123",enabled:false,types:['a']};
var n2 = {id:"456",enabled:false,types:['b']};
var enableNode = sinon.stub();
enableNode.onFirstCall().returns((function() {
n1.enabled = true;
return when.resolve(n1);
})());
enableNode.onSecondCall().returns((function() {
n2.enabled = true;
return when.resolve(n2);
})());
enableNode.returns(null);
initNodes({
settings:{available:function(){return true}},
it('sets module state and returns module info', function(done) {
var opts;
nodes.init({
nodes:{
getModuleInfo: function() { return {name:"node-red", nodes:[n1, n2]} },
enableNode: enableNode
setModuleState: function(_opts) {
opts = _opts;
return Promise.resolve({name:"node-red"});
}
}
});
@@ -669,154 +402,20 @@ describe("api/admin/nodes", function() {
throw err;
}
res.body.should.have.property("name","node-red");
res.body.should.have.property("nodes");
res.body.nodes[0].should.have.property("enabled",true);
res.body.nodes[1].should.have.property("enabled",true);
opts.should.have.property("enabled",true);
opts.should.have.property("module","node-red");
done();
});
});
it('disables enabled module', function(done) {
var n1 = {id:"123",enabled:true,types:['a']};
var n2 = {id:"456",enabled:true,types:['b']};
var disableNode = sinon.stub();
disableNode.onFirstCall().returns((function() {
n1.enabled = false;
return when.resolve(n1);
})());
disableNode.onSecondCall().returns((function() {
n2.enabled = false;
return when.resolve(n2);
})());
disableNode.returns(null);
initNodes({
settings:{available:function(){return true}},
nodes:{
getModuleInfo: function() { return {name:"node-red", nodes:[n1, n2]} },
disableNode: disableNode
}
});
request(app)
.put('/nodes/node-red')
.send({enabled:false})
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.have.property("name","node-red");
res.body.should.have.property("nodes");
res.body.nodes[0].should.have.property("enabled",false);
res.body.nodes[1].should.have.property("enabled",false);
done();
});
});
describe('no-ops if a node in module already in the right state', function() {
function run(state,done) {
var node = {id:"123",enabled:state,types:['a']};
var enableNode = sinon.spy(function(id) {
node.enabled = true;
return when.resolve(node);
});
var disableNode = sinon.spy(function(id) {
node.enabled = false;
return when.resolve(node);
});
initNodes({
settings:{available:function(){return true}},
nodes:{
getModuleInfo: function() { return {name:"node-red", nodes:[node]}; },
enableNode: enableNode,
disableNode: disableNode
}
});
request(app)
.put('/nodes/node-red')
.send({enabled:state})
.expect(200)
.end(function(err,res) {
var enableNodeCalled = enableNode.called;
var disableNodeCalled = disableNode.called;
if (err) {
throw err;
}
enableNodeCalled.should.be.false();
disableNodeCalled.should.be.false();
res.body.should.have.property("name","node-red");
res.body.should.have.property("nodes");
res.body.nodes[0].should.have.property("enabled",state);
done();
});
}
it('already enabled', function(done) {
run(true,done);
});
it('already disabled', function(done) {
run(false,done);
});
});
describe('does not no-op if err on a node in module', function() {
function run(state,done) {
var node = {id:"123",enabled:state,types:['a'],err:"foo"};
var enableNode = sinon.spy(function(id) {
node.enabled = true;
return when.resolve(node);
});
var disableNode = sinon.spy(function(id) {
node.enabled = false;
return when.resolve(node);
});
initNodes({
settings:{available:function(){return true}},
nodes:{
getModuleInfo: function() { return {name:"node-red", nodes:[node]}; },
enableNode: enableNode,
disableNode: disableNode
}
});
request(app)
.put('/nodes/node-red')
.send({enabled:state})
.expect(200)
.end(function(err,res) {
var enableNodeCalled = enableNode.called;
var disableNodeCalled = disableNode.called;
if (err) {
throw err;
}
enableNodeCalled.should.be.equal(state);
disableNodeCalled.should.be.equal(!state);
res.body.should.have.property("name","node-red");
res.body.should.have.property("nodes");
res.body.nodes[0].should.have.property("enabled",state);
done();
});
}
it('already enabled', function(done) {
run(true,done);
});
it('already disabled', function(done) {
run(false,done);
});
});
});
describe('get icons', function() {
it('returns icon list', function(done) {
initNodes({
nodes.init({
nodes:{
getNodeIcons: function() {
return {"module":["1.png","2.png","3.png"]};
getIconList: function() {
return Promise.resolve({module:[1,2,3]});
}
}
});
@@ -827,7 +426,6 @@ describe("api/admin/nodes", function() {
if (err) {
throw err;
}
console.log(res.body);
res.body.should.have.property("module");
res.body.module.should.be.an.Array();
res.body.module.should.have.lengthOf(3);
@@ -835,4 +433,45 @@ describe("api/admin/nodes", function() {
});
});
});
describe('get module messages', function() {
it('returns message catalog', function(done) {
nodes.init({
nodes:{
getModuleCatalog: function(opts) {
return Promise.resolve(opts);
}
}
});
request(app)
.get('/nodes/module/set/messages')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.eql({ module: 'module/set' });
done();
});
});
it('returns all node catalogs', function(done) {
nodes.init({
nodes:{
getModuleCatalogs: function(opts) {
return Promise.resolve({a:1});
}
}
});
request(app)
.get('/nodes/messages')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.eql({a:1});
done();
});
});
})
});

View File

@@ -23,6 +23,7 @@ var passport = require("passport");
var auth = require("../../../../red/api/auth");
var Users = require("../../../../red/api/auth/users");
var Tokens = require("../../../../red/api/auth/tokens");
var Permissions = require("../../../../red/api/auth/permissions");
describe("api/auth/index",function() {
@@ -30,7 +31,7 @@ describe("api/auth/index",function() {
describe("ensureClientSecret", function() {
before(function() {
auth.init({settings:{},log:{audit:function(){}}})
auth.init({},{})
});
it("leaves client_secret alone if not present",function(done) {
var req = {
@@ -85,7 +86,7 @@ describe("api/auth/index",function() {
Users.init.restore();
});
it("returns login details - credentials", function(done) {
auth.init({settings:{adminAuth:{type:"credentials"}},log:{audit:function(){}}})
auth.init({adminAuth:{type:"credentials"}},{})
auth.login(null,{json: function(resp) {
resp.should.have.a.property("type","credentials");
resp.should.have.a.property("prompts");
@@ -94,14 +95,14 @@ describe("api/auth/index",function() {
}});
});
it("returns login details - none", function(done) {
auth.init({settings:{},log:{audit:function(){}}})
auth.init({},{})
auth.login(null,{json: function(resp) {
resp.should.eql({});
done();
}});
});
it("returns login details - strategy", function(done) {
auth.init({settings:{adminAuth:{type:"strategy",strategy:{label:"test-strategy",icon:"test-icon"}}},log:{audit:function(){}}})
auth.init({adminAuth:{type:"strategy",strategy:{label:"test-strategy",icon:"test-icon"}}},{})
auth.login(null,{json: function(resp) {
resp.should.have.a.property("type","strategy");
resp.should.have.a.property("prompts");
@@ -115,5 +116,100 @@ describe("api/auth/index",function() {
});
});
describe("needsPermission", function() {
beforeEach(function() {
sinon.stub(Tokens,"init",function(){});
sinon.stub(Users,"init",function(){});
});
afterEach(function() {
Tokens.init.restore();
Users.init.restore();
if (passport.authenticate.restore) {
passport.authenticate.restore();
}
if (Permissions.hasPermission.restore) {
Permissions.hasPermission.restore();
}
});
it('no-ops if adminAuth not set', function(done) {
sinon.stub(passport,"authenticate",function(scopes,opts) {
return function(req,res,next) {
}
});
auth.init({});
var func = auth.needsPermission("foo");
func({},{},function() {
passport.authenticate.called.should.be.false();
done();
})
});
it('skips auth if req.user undefined', function(done) {
sinon.stub(passport,"authenticate",function(scopes,opts) {
return function(req,res,next) {
next();
}
});
sinon.stub(Permissions,"hasPermission",function(perm) { return true });
auth.init({adminAuth:{}});
var func = auth.needsPermission("foo");
func({user:null},{},function() {
try {
passport.authenticate.called.should.be.true();
Permissions.hasPermission.called.should.be.false();
done();
} catch(err) {
done(err);
}
})
});
it('passes for valid user permission', function(done) {
sinon.stub(passport,"authenticate",function(scopes,opts) {
return function(req,res,next) {
next();
}
});
sinon.stub(Permissions,"hasPermission",function(perm) { return true });
auth.init({adminAuth:{}});
var func = auth.needsPermission("foo");
func({user:true,authInfo: { scope: "read"}},{},function() {
try {
passport.authenticate.called.should.be.true();
Permissions.hasPermission.called.should.be.true();
Permissions.hasPermission.lastCall.args[0].should.eql("read");
Permissions.hasPermission.lastCall.args[1].should.eql("foo");
done();
} catch(err) {
done(err);
}
})
});
it('rejects for invalid user permission', function(done) {
sinon.stub(passport,"authenticate",function(scopes,opts) {
return function(req,res,next) {
next();
}
});
sinon.stub(Permissions,"hasPermission",function(perm) { return false });
auth.init({adminAuth:{}});
var func = auth.needsPermission("foo");
func({user:true,authInfo: { scope: "read"}},{
status: function(status) {
return { end: function() {
try {
status.should.eql(401);
done();
} catch(err) {
done(err);
}
}}
}
},function() {
done(new Error("hasPermission unexpected passed"))
});
});
});
});

View File

@@ -24,9 +24,6 @@ var Tokens = require("../../../../red/api/auth/tokens");
var Clients = require("../../../../red/api/auth/clients");
describe("api/auth/strategies", function() {
before(function() {
strategies.init({log:{audit:function(){}}})
});
describe("Password Token Exchange", function() {
var userAuthentication;
afterEach(function() {
@@ -108,7 +105,6 @@ describe("api/auth/strategies", function() {
user.should.equal("anon");
strategies.anonymousStrategy.success = strategies.anonymousStrategy._success;
delete strategies.anonymousStrategy._success;
userDefault.restore();
done();
};
strategies.anonymousStrategy.authenticate({});
@@ -122,11 +118,13 @@ describe("api/auth/strategies", function() {
err.should.equal(401);
strategies.anonymousStrategy.fail = strategies.anonymousStrategy._fail;
delete strategies.anonymousStrategy._fail;
userDefault.restore();
done();
};
strategies.anonymousStrategy.authenticate({});
});
afterEach(function() {
Users.default.restore();
})
});
describe("Bearer Strategy", function() {

View File

@@ -21,6 +21,9 @@ var sinon = require('sinon');
var Users = require("../../../../red/api/auth/users");
describe("api/auth/users", function() {
after(function() {
Users.init({});
})
describe('Initalised with a credentials object, no anon',function() {
before(function() {
Users.init({

View File

@@ -33,6 +33,24 @@ var listenPort = 0; // use ephemeral port
describe("api/editor/comms", function() {
var connections = [];
var mockComms = {
addConnection: function(opts) {
connections.push(opts.client);
return Promise.resolve()
},
removeConnection: function(opts) {
for (var i=0;i<connections.length;i++) {
if (connections[i] === opts.client) {
connections.splice(i,1);
break;
}
}
return Promise.resolve()
},
subscribe: function() { return Promise.resolve()},
unsubscribe: function() { return Promise.resolve(); }
}
describe("with default keepalive", function() {
var server;
@@ -41,11 +59,7 @@ describe("api/editor/comms", function() {
before(function(done) {
sinon.stub(Users,"default",function() { return when.resolve(null);});
server = stoppable(http.createServer(function(req,res){app(req,res)}));
comms.init(server, {
settings:{},
log:{warn:function(){},_:function(){},trace:function(){},audit:function(){}},
events:{on:function(){},removeListener:function(){}}
});
comms.init(server, {}, {comms: mockComms});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
@@ -63,9 +77,15 @@ describe("api/editor/comms", function() {
it('accepts connection', function(done) {
var ws = new WebSocket(url);
connections.length.should.eql(0);
ws.on('open', function() {
ws.close();
done();
try {
connections.length.should.eql(1);
ws.close();
done();
} catch(err) {
done(err);
}
});
});
@@ -73,7 +93,8 @@ describe("api/editor/comms", function() {
var ws = new WebSocket(url);
ws.on('open', function() {
ws.send('{"subscribe":"topic1"}');
comms.publish('topic1', 'foo');
connections.length.should.eql(1);
connections[0].send('topic1', 'foo');
});
ws.on('message', function(msg) {
msg.should.equal('[{"topic":"topic1","data":"foo"}]');
@@ -82,43 +103,13 @@ describe("api/editor/comms", function() {
});
});
it('publishes retained message for subscription', function(done) {
comms.publish('topic2', 'bar', true);
var ws = new WebSocket(url);
ws.on('open', function() {
ws.send('{"subscribe":"topic2"}');
});
ws.on('message', function(msg) {
console.log(msg);
msg.should.equal('[{"topic":"topic2","data":"bar"}]');
ws.close();
done();
});
});
it('retained message is deleted by non-retained message', function(done) {
comms.publish('topic3', 'retained', true);
comms.publish('topic3', 'non-retained');
var ws = new WebSocket(url);
ws.on('open', function() {
ws.send('{"subscribe":"topic3"}');
comms.publish('topic3', 'new');
});
ws.on('message', function(msg) {
console.log(msg);
msg.should.equal('[{"topic":"topic3","data":"new"}]');
ws.close();
done();
});
});
it('malformed messages are ignored',function(done) {
var ws = new WebSocket(url);
ws.on('open', function() {
ws.send('not json');
ws.send('[]');
ws.send('{"subscribe":"topic3"}');
comms.publish('topic3', 'correct');
connections[0].send('topic3', 'correct');
});
ws.on('message', function(msg) {
console.log(msg);
@@ -127,40 +118,16 @@ describe("api/editor/comms", function() {
done();
});
});
// The following test currently fails due to minimum viable
// implementation. More test should be written to test topic
// matching once this one is passing
it.skip('receives message on correct topic', function(done) {
var ws = new WebSocket(url);
ws.on('open', function() {
ws.send('{"subscribe":"topic4"}');
comms.publish('topic5', 'foo');
comms.publish('topic4', 'bar');
});
ws.on('message', function(msg) {
console.log(msg);
msg.should.equal('[{"topic":"topic4","data":"bar"}]');
ws.close();
done();
});
});
it('listens for node/status events');
});
describe("disabled editor", function() {
var server;
var url;
var port;
before(function(done) {
sinon.stub(Users,"default",function() { return when.resolve(null);});
sinon.stub(Users,"default",function() { return Promise.resolve(null);});
server = stoppable(http.createServer(function(req,res){app(req,res)}));
comms.init(server, {
settings:{disableEditor:true},
log:{warn:function(){},_:function(){},trace:function(){},audit:function(){}},
events:{on:function(){},removeListener:function(){}}
});
comms.init(server, {disableEditor:true}, {comms: mockComms});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
@@ -177,12 +144,14 @@ describe("api/editor/comms", function() {
});
it('rejects websocket connections',function(done) {
connections.length.should.eql(0);
var ws = new WebSocket(url);
ws.on('open', function() {
done(new Error("Socket connection unexpectedly accepted"));
ws.close();
});
ws.on('error', function() {
connections.length.should.eql(0);
done();
});
@@ -196,11 +165,7 @@ describe("api/editor/comms", function() {
before(function(done) {
sinon.stub(Users,"default",function() { return when.resolve(null);});
server = stoppable(http.createServer(function(req,res){app(req,res)}));
comms.init(server, {
settings:{httpAdminRoot:"/adminPath"},
log:{warn:function(){},_:function(){},trace:function(){},audit:function(){}},
events:{on:function(){},removeListener:function(){}}
});
comms.init(server, {httpAdminRoot:"/adminPath"}, {comms: mockComms});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
@@ -217,8 +182,10 @@ describe("api/editor/comms", function() {
});
it('accepts connections',function(done) {
connections.length.should.eql(0);
var ws = new WebSocket(url);
ws.on('open', function() {
connections.length.should.eql(1);
ws.close();
done();
});
@@ -236,11 +203,7 @@ describe("api/editor/comms", function() {
before(function(done) {
sinon.stub(Users,"default",function() { return when.resolve(null);});
server = stoppable(http.createServer(function(req,res){app(req,res)}));
comms.init(server,{
settings:{httpAdminRoot:"/adminPath"},
log:{warn:function(){},_:function(){},trace:function(){},audit:function(){}},
events:{on:function(){},removeListener:function(){}}
});
comms.init(server, {httpAdminRoot:"/adminPath/"}, {comms: mockComms});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
@@ -257,8 +220,10 @@ describe("api/editor/comms", function() {
});
it('accepts connections',function(done) {
connections.length.should.eql(0);
var ws = new WebSocket(url);
ws.on('open', function() {
connections.length.should.eql(1);
ws.close();
done();
});
@@ -276,11 +241,7 @@ describe("api/editor/comms", function() {
before(function(done) {
sinon.stub(Users,"default",function() { return when.resolve(null);});
server = stoppable(http.createServer(function(req,res){app(req,res)}));
comms.init(server, {
settings:{httpAdminRoot:"adminPath"},
log:{warn:function(){},_:function(){},trace:function(){},audit:function(){}},
events:{on:function(){},removeListener:function(){}}
});
comms.init(server, {httpAdminRoot:"adminPath"}, {comms: mockComms});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
@@ -297,8 +258,10 @@ describe("api/editor/comms", function() {
});
it('accepts connections',function(done) {
connections.length.should.eql(0);
var ws = new WebSocket(url);
ws.on('open', function() {
connections.length.should.eql(1);
ws.close();
done();
});
@@ -316,11 +279,7 @@ describe("api/editor/comms", function() {
before(function(done) {
sinon.stub(Users,"default",function() { return when.resolve(null);});
server = stoppable(http.createServer(function(req,res){app(req,res)}));
comms.init(server, {
settings:{webSocketKeepAliveTime: 100},
log:{warn:function(){},_:function(){},trace:function(){},audit:function(){}},
events:{on:function(){},removeListener:function(){}}
});
comms.init(server, {webSocketKeepAliveTime: 100}, {comms: mockComms});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
@@ -355,7 +314,7 @@ describe("api/editor/comms", function() {
ws.on('open', function() {
ws.send('{"subscribe":"foo"}');
interval = setInterval(function() {
comms.publish('foo', 'bar');
connections[0].send('foo', 'bar');
}, 50);
});
ws.on('message', function(data) {
@@ -403,11 +362,7 @@ describe("api/editor/comms", function() {
server = stoppable(http.createServer(function(req,res){app(req,res)}));
comms.init(server,{
settings:{adminAuth:{}},
log:{warn:function(){},_:function(){},trace:function(){},audit:function(){}},
events:{on:function(){},removeListener:function(){}}
});
comms.init(server, {adminAuth:{}}, {comms: mockComms});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
@@ -447,7 +402,7 @@ describe("api/editor/comms", function() {
if (received == 1) {
msg.should.equal('{"auth":"ok"}');
ws.send('{"subscribe":"foo"}');
comms.publish('foo', 'correct');
connections[0].send('foo', 'correct');
} else {
msg.should.equal('[{"topic":"foo","data":"correct"}]');
ws.close();
@@ -494,11 +449,7 @@ describe("api/editor/comms", function() {
before(function(done) {
getDefaultUser = sinon.stub(Users,"default",function() { return when.resolve({permissions:"read"});});
server = stoppable(http.createServer(function(req,res){app(req,res)}));
comms.init(server, {
settings:{adminAuth:{}},
log:{warn:function(){},_:function(){},trace:function(){},audit:function(){}},
events:{on:function(){},removeListener:function(){}}
});
comms.init(server, {adminAuth:{}}, {comms: mockComms});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
@@ -520,7 +471,7 @@ describe("api/editor/comms", function() {
ws.on('open', function() {
ws.send('{"subscribe":"foo"}');
setTimeout(function() {
comms.publish('foo', 'correct');
connections[0].send('foo', 'correct');
},200);
});
ws.on('message', function(msg) {

View File

@@ -29,64 +29,30 @@ describe('api/editor/credentials', function() {
app = express();
app.get('/credentials/:type/:id',credentials.get);
credentials.init({
log:{audit:function(){}},
nodes:{
getCredentials: function(id) {
if (id === "n1") {
return {user1:"abc",password1:"123"};
flows: {
getNodeCredentials: function(opts) {
if (opts.type === "known-type" && opts.id === "n1") {
return Promise.resolve({
user1:"abc",
has_password1: true
});
} else {
return null;
}
},
getCredentialDefinition:function(type) {
if (type === "known-type") {
return {user1:{type:"text"},password1:{type:"password"}};
} else {
return null;
var err = new Error("message");
err.code = "test_code";
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
}
}
});
});
it('returns empty credentials if unknown type',function(done) {
request(app)
.get("/credentials/unknown-type/n1")
.expect(200)
.expect("Content-Type",/json/)
.end(function(err,res) {
if (err) {
done(err);
} else {
try {
res.body.should.eql({});
done();
} catch(e) {
done(e);
}
}
})
});
it('returns empty credentials if none are stored',function(done) {
request(app)
.get("/credentials/known-type/n2")
.expect("Content-Type",/json/)
.end(function(err,res) {
if (err) {
done(err);
} else {
try {
res.body.should.eql({});
done();
} catch(e) {
done(e);
}
}
})
});
it('returns stored credentials',function(done) {
request(app)
.get("/credentials/known-type/n1")
.expect("Content-Type",/json/)
.expect(200)
.end(function(err,res) {
if (err) {
done(err);
@@ -102,5 +68,26 @@ describe('api/editor/credentials', function() {
}
})
});
it('returns any error',function(done) {
request(app)
.get("/credentials/unknown-type/n2")
.expect("Content-Type",/json/)
.expect(500)
.end(function(err,res) {
if (err) {
done(err);
} else {
try {
res.body.should.have.property('code');
res.body.code.should.be.equal("test_code");
res.body.should.have.property('message');
res.body.message.should.be.equal('message');
done();
} catch(e) {
done(e);
}
}
})
});
});

View File

@@ -22,6 +22,10 @@ var editorApi = require("../../../../red/api/editor");
var comms = require("../../../../red/api/editor/comms");
var info = require("../../../../red/api/editor/settings");
var auth = require("../../../../red/api/auth");
var log = require("../../../../red/util/log");
var when = require("when");
@@ -37,9 +41,7 @@ describe("api/editor/index", function() {
info.init.restore();
});
it("disables the editor", function() {
var editorApp = editorApi.init({},{
settings:{disableEditor:true}
});
var editorApp = editorApi.init({},{disableEditor:true},{});
should.not.exist(editorApp);
comms.init.called.should.be.false();
info.init.called.should.be.false();
@@ -67,15 +69,13 @@ describe("api/editor/index", function() {
})
require("../../../../red/api/editor/theme").app.restore();
auth.needsPermission.restore();
log.error.restore();
});
before(function() {
app = editorApi.init({},{
log:{audit:function(){},error:function(msg){errors.push(msg)}},
settings:{httpNodeRoot:true, httpAdminRoot: true,disableEditor:false,exportNodeSettings:function(){}},
events:{on:function(){},removeListener:function(){}},
isStarted: function() { return isStarted; },
nodes: {paletteEditorEnabled: function() { return false }}
sinon.stub(log,"error",function(err) { errors.push(err)})
app = editorApi.init({},{httpNodeRoot:true, httpAdminRoot: true,disableEditor:false,exportNodeSettings:function(){}},{
isStarted: () => Promise.resolve(isStarted)
});
});
it('serves the editor', function(done) {
@@ -117,7 +117,7 @@ describe("api/editor/index", function() {
done();
});
});
// it('GET /settings', function(done) {
// it.skip('GET /settings', function(done) {
// request(app).get("/settings").expect(200).end(function(err,res) {
// if (err) {
// return done(err);

View File

@@ -16,334 +16,287 @@
var should = require("should");
var sinon = require("sinon");
var fs = require("fs");
var fspath = require('path');
var request = require('supertest');
var express = require('express');
var bodyParser = require('body-parser');
var when = require('when');
var app;
var library = require("../../../../red/api/editor/library");
var auth = require("../../../../red/api/auth");
describe("api/editor/library", function() {
function initLibrary(_flows,_libraryEntries,_examples,_exampleFlowPathFunction) {
var flows = _flows;
var libraryEntries = _libraryEntries;
library.init(app,{
log:{audit:function(){},_:function(){},warn:function(){}},
storage: {
init: function() {
return when.resolve();
},
getAllFlows: function() {
return when.resolve(flows);
},
getFlow: function(fn) {
if (flows[fn]) {
return when.resolve(flows[fn]);
} else if (fn.indexOf("..")!==-1) {
var err = new Error();
err.code = 'forbidden';
return when.reject(err);
} else {
return when.reject();
}
},
saveFlow: function(fn,data) {
if (fn.indexOf("..")!==-1) {
var err = new Error();
err.code = 'forbidden';
return when.reject(err);
}
flows[fn] = data;
return when.resolve();
},
getLibraryEntry: function(type,path) {
if (path.indexOf("..")!==-1) {
var err = new Error();
err.code = 'forbidden';
return when.reject(err);
}
if (libraryEntries[type] && libraryEntries[type][path]) {
return when.resolve(libraryEntries[type][path]);
} else {
return when.reject();
}
},
saveLibraryEntry: function(type,path,meta,body) {
if (path.indexOf("..")!==-1) {
var err = new Error();
err.code = 'forbidden';
return when.reject(err);
}
libraryEntries[type][path] = body;
return when.resolve();
before(function() {
app = express();
app.use(bodyParser.json());
app.get("/library/flows",library.getAll);
app.post(/library\/([^\/]+)\/(.*)/,library.saveEntry);
app.get(/library\/([^\/]+)(?:$|\/(.*))/,library.getEntry);
});
after(function() {
});
it('returns all flows', function(done) {
library.init({
library: {
getEntries: function(opts) {
return Promise.resolve({a:1,b:2});
}
},
events: {
on: function(){},
removeListener: function(){}
},
nodes: {
getNodeExampleFlows: function() {
return _examples;
},
getNodeExampleFlowPath: _exampleFlowPathFunction
}
});
}
describe("flows", function() {
before(function() {
app = express();
app.use(bodyParser.json());
app.get("/library/flows",library.getAll);
app.post(new RegExp("/library/flows\/(.*)"),library.post);
app.get(new RegExp("/library/flows\/(.*)"),library.get);
app.response.sendFile = function (path) {
app.response.json.call(this, {sendFile: path});
};
sinon.stub(fs,"statSync",function() { return true; });
});
after(function() {
fs.statSync.restore();
});
it('returns empty result', function(done) {
initLibrary({},{flows:{}});
request(app)
.get('/library/flows')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.not.have.property('f');
res.body.should.not.have.property('d');
done();
});
});
it('returns 404 for non-existent entry', function(done) {
initLibrary({},{flows:{}});
request(app)
.get('/library/flows/foo')
.expect(404)
.end(done);
});
it('can store and retrieve item', function(done) {
initLibrary({},{flows:{}});
var flow = '[]';
request(app)
.post('/library/flows/foo')
.set('Content-Type', 'application/json')
.send(flow)
.expect(204).end(function (err, res) {
if (err) {
throw err;
}
request(app)
.get('/library/flows/foo')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.text.should.equal(flow);
done();
});
});
});
it('lists a stored item', function(done) {
initLibrary({f:["bar"]});
request(app)
.get('/library/flows')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.have.property('f');
should.deepEqual(res.body.f,['bar']);
done();
});
});
it('returns 403 for malicious get attempt', function(done) {
initLibrary({});
// without the userDir override the malicious url would be
// http://127.0.0.1:1880/library/flows/../../package to
// obtain package.json from the node-red root.
request(app)
.get('/library/flows/../../../../../package')
.expect(403)
.end(done);
});
it('returns 403 for malicious post attempt', function(done) {
initLibrary({});
// without the userDir override the malicious url would be
// http://127.0.0.1:1880/library/flows/../../package to
// obtain package.json from the node-red root.
request(app)
.post('/library/flows/../../../../../package')
.expect(403)
.end(done);
});
it('includes examples flows if set', function(done) {
var examples = {"d":{"node-module":{"f":["example-one"]}}};
initLibrary({},{},examples);
request(app)
.get('/library/flows')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.have.property('d');
res.body.d.should.have.property('_examples_');
should.deepEqual(res.body.d._examples_,examples);
done();
});
});
it('can retrieve an example flow', function(done) {
var examples = {"d":{"node-module":{"f":["example-one"]}}};
initLibrary({},{},examples,function(module,path) {
return module + ':' + path
request(app)
.get('/library/flows')
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('a',1);
res.body.should.have.property('b',2);
done();
});
request(app)
.get('/library/flows/_examples_/node-module/example-one')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.have.property('sendFile',
'node-module:example-one');
done();
});
})
it('returns an error on all flows', function(done) {
library.init({
library: {
getEntries: function(opts) {
var err = new Error("message");
err.code = "random_error";
err.status = 400;
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
}
});
it('can retrieve an example flow in an org scoped package', function(done) {
var examples = {"d":{"@org_scope/node_package":{"f":["example-one"]}}};
initLibrary({},{},examples,function(module,path) {
return module + ':' + path
request(app)
.get('/library/flows')
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('code');
res.body.code.should.be.equal("random_error");
res.body.should.have.property('message');
res.body.message.should.be.equal("message");
done();
});
request(app)
.get('/library/flows/_examples_/@org_scope/node_package/example-one')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.have.property('sendFile',
'@org_scope/node_package:example-one');
done();
});
});
});
describe("type", function() {
before(function() {
app = express();
app.use(bodyParser.json());
initLibrary({},{});
auth.init({settings:{}});
library.register("test");
it('returns an individual entry - flow type', function(done) {
var opts;
library.init({
library: {
getEntry: function(_opts) {
opts = _opts;
return Promise.resolve('{"a":1,"b":2}');
}
}
});
it('returns empty result', function(done) {
initLibrary({},{'test':{"":[]}});
request(app)
.get('/library/test')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.not.have.property('f');
done();
});
request(app)
.get('/library/flows/abc')
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('a',1);
res.body.should.have.property('b',2);
opts.should.have.property('type','flows');
opts.should.have.property('path','abc');
done();
});
})
it('returns a directory listing - flow type', function(done) {
var opts;
library.init({
library: {
getEntry: function(_opts) {
opts = _opts;
return Promise.resolve({"a":1,"b":2});
}
}
});
it('returns 404 for non-existent entry', function(done) {
initLibrary({},{});
request(app)
.get('/library/test/foo')
.expect(404)
.end(done);
request(app)
.get('/library/flows/abc/def')
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('a',1);
res.body.should.have.property('b',2);
opts.should.have.property('type','flows');
opts.should.have.property('path','abc/def');
done();
});
})
it('returns an individual entry - non-flow type', function(done) {
var opts;
library.init({
library: {
getEntry: function(_opts) {
opts = _opts;
return Promise.resolve('{"a":1,"b":2}');
}
}
});
it('can store and retrieve item', function(done) {
initLibrary({},{'test':{}});
var flow = {text:"test content"};
request(app)
.post('/library/test/foo')
.set('Content-Type', 'application/json')
.send(flow)
.expect(204).end(function (err, res) {
if (err) {
throw err;
}
request(app)
.get('/library/test/foo')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.text.should.equal(flow.text);
done();
});
});
request(app)
.get('/library/non-flow/abc')
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
opts.should.have.property('type','non-flow');
opts.should.have.property('path','abc');
res.text.should.eql('{"a":1,"b":2}');
done();
});
})
it('returns a directory listing - non-flow type', function(done) {
var opts;
library.init({
library: {
getEntry: function(_opts) {
opts = _opts;
return Promise.resolve({"a":1,"b":2});
}
}
});
request(app)
.get('/library/non-flow/abc/def')
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('a',1);
res.body.should.have.property('b',2);
opts.should.have.property('type','non-flow');
opts.should.have.property('path','abc/def');
done();
});
})
it('lists a stored item', function(done) {
initLibrary({},{'test':{'a':['abc','def']}});
request(app)
.get('/library/test/a')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
// This response isn't strictly accurate - but it
// verifies the api returns what storage gave it
should.deepEqual(res.body,['abc','def']);
done();
});
it('returns an error on individual get', function(done) {
var opts;
library.init({
library: {
getEntry: function(_opts) {
opts = _opts;
var err = new Error("message");
err.code = "random_error";
err.status = 400;
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
}
});
request(app)
.get('/library/flows/123')
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
opts.should.have.property('type','flows');
opts.should.have.property('path','123');
res.body.should.have.property('code');
res.body.code.should.be.equal("random_error");
res.body.should.have.property('message');
res.body.message.should.be.equal("message");
done();
});
});
it('returns 403 for malicious access attempt', function(done) {
request(app)
.get('/library/test/../../../../../../../../../../etc/passwd')
.expect(403)
.end(done);
it('saves an individual entry - flow type', function(done) {
var opts;
library.init({
library: {
saveEntry: function(_opts) {
opts = _opts;
return Promise.resolve();
}
}
});
request(app)
.post('/library/flows/abc/def')
.expect(204)
.send({a:1,b:2,c:3})
.end(function(err,res) {
if (err) {
return done(err);
}
opts.should.have.property('type','flows');
opts.should.have.property('path','abc/def');
opts.should.have.property('meta',{});
opts.should.have.property('body',JSON.stringify({a:1,b:2,c:3}));
done();
});
})
it('returns 403 for malicious access attempt', function(done) {
request(app)
.get('/library/test/..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\etc\\passwd')
.expect(403)
.end(done);
it('saves an individual entry - non-flow type', function(done) {
var opts;
library.init({
library: {
saveEntry: function(_opts) {
opts = _opts;
return Promise.resolve();
}
}
});
request(app)
.post('/library/non-flow/abc/def')
.expect(204)
.send({a:1,b:2,text:"123"})
.end(function(err,res) {
if (err) {
return done(err);
}
opts.should.have.property('type','non-flow');
opts.should.have.property('path','abc/def');
opts.should.have.property('meta',{a:1,b:2});
opts.should.have.property('body',"123");
done();
});
})
it('returns 403 for malicious access attempt', function(done) {
request(app)
.post('/library/test/../../../../../../../../../../etc/passwd')
.set('Content-Type', 'text/plain')
.send('root:x:0:0:root:/root:/usr/bin/tclsh')
.expect(403)
.end(done);
it('returns an error on individual save', function(done) {
var opts;
library.init({
library: {
saveEntry: function(_opts) {
opts = _opts;
var err = new Error("message");
err.code = "random_error";
err.status = 400;
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
}
});
request(app)
.post('/library/non-flow/abc/def')
.send({a:1,b:2,text:"123"})
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
opts.should.have.property('type','non-flow');
opts.should.have.property('path','abc/def');
res.body.should.have.property('code');
res.body.code.should.be.equal("random_error");
res.body.should.have.property('message');
res.body.message.should.be.equal("message");
done();
});
});
});

View File

@@ -20,6 +20,7 @@ var express = require('express');
var sinon = require('sinon');
var locales = require("../../../../red/api/editor/locales");
var i18n = require("../../../../red/util").i18n;
describe("api/editor/locales", function() {
beforeEach(function() {
@@ -29,25 +30,40 @@ describe("api/editor/locales", function() {
describe('get named resource catalog',function() {
var app;
before(function() {
// locales.init({
// i18n: {
// i: {
// language: function() { return 'en-US'},
// changeLanguage: function(lang,callback) {
// if (callback) {
// callback();
// }
// },
// getResourceBundle: function(lang, namespace) {
// return {namespace:namespace, lang:lang};
// }
// },
// }
// });
locales.init({});
// bit of a mess of internal workings
locales.init({
i18n: {
i: {
language: function() { return 'en-US'},
changeLanguage: function(lang,callback) {
if (callback) {
callback();
}
},
getResourceBundle: function(lang, namespace) {
return {namespace:namespace, lang:lang};
}
},
}
});
sinon.stub(i18n.i,'changeLanguage',function(lang,callback) { if (callback) {callback();}});
if (i18n.i.getResourceBundle) {
sinon.stub(i18n.i,'getResourceBundle',function(lang, namespace) {return {namespace:namespace, lang:lang};});
} else {
// If i18n.init has not been called, then getResourceBundle isn't
// defined - so hardcode a stub
i18n.i.getResourceBundle = function(lang, namespace) {return {namespace:namespace, lang:lang};};
i18n.i.getResourceBundle.restore = function() { delete i18n.i.getResourceBundle };
}
app = express();
app.get(/locales\/(.+)\/?$/,locales.get);
});
after(function() {
i18n.i.changeLanguage.restore();
i18n.i.getResourceBundle.restore();
})
it('returns with default language', function(done) {
request(app)
.get("/locales/message-catalog")
@@ -75,50 +91,49 @@ describe("api/editor/locales", function() {
});
});
describe('get all node resource catalogs',function() {
var app;
before(function() {
// bit of a mess of internal workings
locales.init({
i18n: {
i:{
getResourceBundle: function(lang, namespace) {
return {
"node-red": "should not return",
"test-module-a-id": "test-module-a-catalog",
"test-module-b-id": "test-module-b-catalog",
"test-module-c-id": "test-module-c-catalog"
}[namespace]
}
}
},
nodes: {
getNodeList: function() {
return [
{module:"node-red",id:"node-red-id"},
{module:"test-module-a",id:"test-module-a-id"},
{module:"test-module-b",id:"test-module-b-id"}
];
}
}
});
app = express();
app.get("/locales/nodes",locales.getAllNodes);
});
it('returns with the node catalogs', function(done) {
request(app)
.get("/locales/nodes")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.eql({
'test-module-a-id': 'test-module-a-catalog',
'test-module-b-id': 'test-module-b-catalog'
});
done();
});
});
});
// describe('get all node resource catalogs',function() {
// var app;
// before(function() {
// // bit of a mess of internal workings
// sinon.stub(i18n,'catalog',function(namespace, lang) {
// return {
// "node-red": "should not return",
// "test-module-a-id": "test-module-a-catalog",
// "test-module-b-id": "test-module-b-catalog",
// "test-module-c-id": "test-module-c-catalog"
// }[namespace]
// });
// locales.init({
// nodes: {
// getNodeList: function(opts) {
// return Promise.resolve([
// {module:"node-red",id:"node-red-id"},
// {module:"test-module-a",id:"test-module-a-id"},
// {module:"test-module-b",id:"test-module-b-id"}
// ]);
// }
// }
// });
// app = express();
// app.get("/locales/nodes",locales.getAllNodes);
// });
// after(function() {
// i18n.catalog.restore();
// })
// it('returns with the node catalogs', function(done) {
// request(app)
// .get("/locales/nodes")
// .expect(200)
// .end(function(err,res) {
// if (err) {
// return done(err);
// }
// res.body.should.eql({
// 'test-module-a-id': 'test-module-a-catalog',
// 'test-module-b-id': 'test-module-b-catalog'
// });
// done();
// });
// });
// });
});

View File

@@ -17,267 +17,103 @@
var should = require("should");
var request = require('supertest');
var express = require('express');
var bodyParser = require("body-parser");
var sinon = require('sinon');
var when = require('when');
var app = express();
var app;
var info = require("../../../../red/api/editor/settings");
var theme = require("../../../../red/api/editor/theme");
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.runtimeSettings);
app.get("/settingsWithUser",function(req,res,next) {
req.user = {
username: "nick",
permissions: "*",
image: "http://example.com",
anonymous: false,
private: "secret"
before(function() {
sinon.stub(theme,"settings",function() { return { test: 456 };});
app = express();
app.use(bodyParser.json());
app.get("/settings",info.runtimeSettings);
app.get("/settings/user",function(req,res,next) {req.user = "fred"; next()}, info.userSettings);
app.post("/settings/user",function(req,res,next) {req.user = "fred"; next()},info.updateUserSettings);
});
after(function() {
theme.settings.restore();
});
it('returns the runtime settings', function(done) {
info.init({
settings: {
getRuntimeSettings: function(opts) {
return Promise.resolve({
a:1,
b:2
})
}
next();
},info.runtimeSettings);
}
});
after(function() {
theme.settings.restore();
request(app)
.get("/settings")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property("a",1);
res.body.should.have.property("b",2);
res.body.should.have.property("editorTheme",{test:456});
done();
});
it('returns the filtered settings', function(done) {
info.init({
settings: {
foo: 123,
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion",
paletteCategories :["red","blue","green"],
exportNodeSettings: function(obj) {
obj.testNodeSetting = "helloWorld";
}
},
nodes: {
paletteEditorEnabled: function() { return true; },
getCredentialKeyType: function() { return "test-key-type"},
listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}}
},
log: { error: console.error },
storage: {}
});
request(app)
.get("/settings")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property("httpNodeRoot","testHttpNodeRoot");
res.body.should.have.property("version","testVersion");
res.body.should.have.property("context",{default: "foo", stores: ["foo","bar"]});
res.body.should.have.property("paletteCategories",["red","blue","green"]);
res.body.should.have.property("editorTheme",{test:456});
res.body.should.have.property("testNodeSetting","helloWorld");
res.body.should.not.have.property("foo",123);
res.body.should.have.property("flowEncryptionType","test-key-type");
res.body.should.not.have.property("user");
done();
});
});
it('returns the filtered user in settings', function(done) {
info.init({
settings: {
foo: 123,
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion",
paletteCategories :["red","blue","green"],
exportNodeSettings: function(obj) {
obj.testNodeSetting = "helloWorld";
}
},
nodes: {
paletteEditorEnabled: function() { return true; },
getCredentialKeyType: function() { return "test-key-type"},
listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}}
},
log: { error: console.error },
storage: {}
});
request(app)
.get("/settingsWithUser")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property("user");
res.body.user.should.have.property("username","nick");
res.body.user.should.have.property("permissions","*");
res.body.user.should.have.property("image","http://example.com");
res.body.user.should.have.property("anonymous",false);
res.body.user.should.not.have.property("private");
done();
});
});
it('includes project settings if projects available', function(done) {
info.init({
settings: {
foo: 123,
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion",
paletteCategories :["red","blue","green"],
exportNodeSettings: function(obj) {
obj.testNodeSetting = "helloWorld";
}
},
nodes: {
paletteEditorEnabled: function() { return true; },
getCredentialKeyType: function() { return "test-key-type"},
listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}}
},
log: { error: console.error },
storage: {
projects: {
getActiveProject: () => 'test-active-project',
getFlowFilename: () => 'test-flow-file',
getCredentialsFilename: () => 'test-creds-file',
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
});
it('returns the user settings', function(done) {
info.init({
settings: {
getUserSettings: function(opts) {
if (opts.user !== "fred") {
return Promise.reject(new Error("Unknown user"));
}
return Promise.resolve({
c:3,
d:4
})
}
});
request(app)
.get("/settings")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property("project","test-active-project");
res.body.should.not.have.property("files");
res.body.should.have.property("git");
res.body.git.should.have.property("globalUser",{name:'foo',email:'foo@example.com'});
done();
});
}
});
it('includes existing files details if projects enabled but no active project and files exist', function(done) {
info.init({
settings: {
foo: 123,
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion",
paletteCategories :["red","blue","green"],
exportNodeSettings: function(obj) {
obj.testNodeSetting = "helloWorld";
}
},
nodes: {
paletteEditorEnabled: function() { return true; },
getCredentialKeyType: function() { return "test-key-type"},
listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}}
},
log: { error: console.error },
storage: {
projects: {
flowFileExists: () => true,
getActiveProject: () => false,
getFlowFilename: () => 'test-flow-file',
getCredentialsFilename: () => 'test-creds-file',
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
request(app)
.get("/settings/user")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.eql({c:3,d:4});
done();
});
});
it('updates the user settings', function(done) {
var update;
info.init({
settings: {
updateUserSettings: function(opts) {
if (opts.user !== "fred") {
return Promise.reject(new Error("Unknown user"));
}
update = opts.settings;
return Promise.resolve()
}
});
request(app)
.get("/settings")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.not.have.property("project");
res.body.should.have.property("files");
res.body.files.should.have.property("flow",'test-flow-file');
res.body.files.should.have.property("credentials",'test-creds-file');
res.body.should.have.property("git");
res.body.git.should.have.property("globalUser",{name:'foo',email:'foo@example.com'});
done();
});
}
});
it('does not include file details if projects enabled but no active project and files do not exist', function(done) {
info.init({
settings: {
foo: 123,
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion",
paletteCategories :["red","blue","green"],
exportNodeSettings: function(obj) {
obj.testNodeSetting = "helloWorld";
}
},
nodes: {
paletteEditorEnabled: function() { return true; },
getCredentialKeyType: function() { return "test-key-type"},
listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}}
},
log: { error: console.error },
storage: {
projects: {
flowFileExists: () => false,
getActiveProject: () => false,
getFlowFilename: () => 'test-flow-file',
getCredentialsFilename: () => 'test-creds-file',
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
}
}
});
request(app)
.get("/settings")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.not.have.property("project");
res.body.should.not.have.property("files");
res.body.should.have.property("git");
res.body.git.should.have.property("globalUser",{name:'foo',email:'foo@example.com'});
done();
});
});
it('overrides palette editable if runtime says it is disabled', function(done) {
info.init({
settings: {
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion",
paletteCategories :["red","blue","green"],
exportNodeSettings: function() {}
},
nodes: {
paletteEditorEnabled: function() { return false; },
getCredentialKeyType: function() { return "test-key-type"},
listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}}
},
log: { error: console.error },
storage: {}
});
request(app)
.get("/settings")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property("httpNodeRoot","testHttpNodeRoot");
res.body.should.have.property("version","testVersion");
res.body.should.have.property("paletteCategories",["red","blue","green"]);
res.body.should.have.property("editorTheme");
res.body.editorTheme.should.have.property("test",456);
res.body.editorTheme.should.have.property("palette",{editable:false});
done();
});
request(app)
.post("/settings/user")
.send({
e:4,
f:5
})
.expect(204)
.end(function(err,res) {
if (err) {
return done(err);
}
update.should.eql({e:4,f:5});
done();
});
});
});

View File

@@ -18,83 +18,42 @@ var should = require("should");
var sinon = require("sinon");
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");
var auth = require("../../../../red/api/auth");
var sshkeys = require("../../../../red/api/editor/sshkeys");
var when = require("when");
var bodyParser = require("body-parser");
var fs = require("fs-extra");
var fspath = require("path");
describe("api/editor/sshkeys", function() {
var app;
var mockList = [
'library','theme','locales','credentials','comms'
]
var isStarted = true;
var errors = [];
var session_data = {};
var mockRuntime = {
settings:{
httpNodeRoot: true,
httpAdminRoot: true,
disableEditor: false,
exportNodeSettings:function(){},
storage: {
getSessions: function(){
return when.resolve(session_data);
},
setSessions: function(_session) {
session_data = _session;
return when.resolve();
}
}
},
log:{audit:function(){},error:function(msg){errors.push(msg)},trace:function(){}},
storage: {
projects: {
ssh: {
init: function(){},
listSSHKeys: function(){},
getSSHKey: function(){},
generateSSHKey: function(){},
deleteSSHKey: function(){}
}
}
},
events:{on:function(){},removeListener:function(){}},
isStarted: function() { return isStarted; },
nodes: {paletteEditorEnabled: function() { return false }}
};
settings: {
getUserKeys: function() {},
getUserKey: function() {},
generateUserKey: function() {},
removeUserKey: function() {}
}
}
before(function() {
auth.init(mockRuntime);
sshkeys.init(mockRuntime);
app = express();
app.use(bodyParser.json());
app.use(editorApi.init({},mockRuntime));
app.use("/settings/user/keys", sshkeys.app());
});
after(function() {
})
beforeEach(function() {
sinon.stub(mockRuntime.storage.projects.ssh, "listSSHKeys");
sinon.stub(mockRuntime.storage.projects.ssh, "getSSHKey");
sinon.stub(mockRuntime.storage.projects.ssh, "generateSSHKey");
sinon.stub(mockRuntime.storage.projects.ssh, "deleteSSHKey");
sinon.stub(mockRuntime.settings, "getUserKeys");
sinon.stub(mockRuntime.settings, "getUserKey");
sinon.stub(mockRuntime.settings, "generateUserKey");
sinon.stub(mockRuntime.settings, "removeUserKey");
})
afterEach(function() {
mockRuntime.storage.projects.ssh.listSSHKeys.restore();
mockRuntime.storage.projects.ssh.getSSHKey.restore();
mockRuntime.storage.projects.ssh.generateSSHKey.restore();
mockRuntime.storage.projects.ssh.deleteSSHKey.restore();
mockRuntime.settings.getUserKeys.restore();
mockRuntime.settings.getUserKey.restore();
mockRuntime.settings.generateUserKey.restore();
mockRuntime.settings.removeUserKey.restore();
})
it('GET /settings/user/keys --- return empty list', function(done) {
mockRuntime.storage.projects.ssh.listSSHKeys.returns(Promise.resolve([]));
mockRuntime.settings.getUserKeys.returns(Promise.resolve([]));
request(app)
.get("/settings/user/keys")
.expect(200)
@@ -118,7 +77,7 @@ describe("api/editor/sshkeys", function() {
name: elem
};
});
mockRuntime.storage.projects.ssh.listSSHKeys.returns(Promise.resolve(retList));
mockRuntime.settings.getUserKeys.returns(Promise.resolve(retList));
request(app)
.get("/settings/user/keys")
.expect(200)
@@ -139,16 +98,16 @@ describe("api/editor/sshkeys", function() {
errInstance.code = "test_code";
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.storage.projects.ssh.listSSHKeys.returns(p);
mockRuntime.settings.getUserKeys.returns(p);
request(app)
.get("/settings/user/keys")
.expect(400)
.expect(500)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal(errInstance.code);
res.body.should.have.property('code');
res.body.code.should.be.equal(errInstance.code);
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.message);
done();
@@ -156,7 +115,12 @@ describe("api/editor/sshkeys", function() {
});
it('GET /settings/user/keys/<key_file_name> --- return 404', function(done) {
mockRuntime.storage.projects.ssh.getSSHKey.returns(Promise.resolve(null));
var errInstance = new Error("Not Found.");
errInstance.code = "not_found";
errInstance.status = 404;
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.settings.getUserKey.returns(p);
request(app)
.get("/settings/user/keys/NOT_REAL")
.expect(404)
@@ -168,19 +132,19 @@ describe("api/editor/sshkeys", function() {
});
});
it('GET /settings/user/keys --- return Unexpected Error', function(done) {
var errInstance = new Error("Messages.....");
var errInstance = new Error();
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.storage.projects.ssh.listSSHKeys.returns(p);
mockRuntime.settings.getUserKeys.returns(p)
request(app)
.get("/settings/user/keys")
.expect(400)
.expect(500)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal("unexpected_error");
res.body.should.have.property('code');
res.body.code.should.be.equal("unexpected_error");
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.toString());
done();
@@ -190,7 +154,7 @@ describe("api/editor/sshkeys", function() {
it('GET /settings/user/keys/<key_file_name> --- return content', function(done) {
var key_file_name = "test_key";
var fileContent = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQD3a+sgtgzSbbliWxmOq5p6+H/mE+0gjWfLWrkIVmHENd1mifV4uCmIHAR2NfuadUYMQ3+bQ90kpmmEKTMYPsyentsKpHQZxTzG7wOCAIpJnbPTHDMxEJhVTaAwEjbVyMSIzTTPfnhoavWIBu0+uMgKDDlBm+RjlgkFlyhXyCN6UwFrIUUMH6Gw+eQHLiooKIl8ce7uDxIlt+9b7hFCU+sQ3kvuse239DZluu6+8buMWqJvrEHgzS9adRFKku8nSPAEPYn85vDi7OgVAcLQufknNgs47KHBAx9h04LeSrFJ/P5J1b//ItRpMOIme+O9d1BR46puzhvUaCHLdvO9czj+OmW+dIm+QIk6lZIOOMnppG72kZxtLfeKT16ur+2FbwAdL9ItBp4BI/YTlBPoa5mLMxpuWfmX1qHntvtGc9wEwS1P7YFfmF3XiK5apxalzrn0Qlr5UmDNbVIqJb1OlbC0w03Z0oktti1xT+R2DGOLWM4lBbpXDHV1BhQ7oYOvbUD8Cnof55lTP0WHHsOHlQc/BGDti1XA9aBX/OzVyzBUYEf0pkimsD0RYo6aqt7QwehJYdlz9x1NBguBffT0s4NhNb9IWr+ASnFPvNl2sw4XH/8U0J0q8ZkMpKkbLM1Zdp1Fv00GF0f5UNRokai6uM3w/ccantJ3WvZ6GtctqytWrw== \n";
mockRuntime.storage.projects.ssh.getSSHKey.returns(Promise.resolve(fileContent));
mockRuntime.settings.getUserKey.returns(Promise.resolve(fileContent));
request(app)
.get("/settings/user/keys/" + key_file_name)
.expect(200)
@@ -198,7 +162,8 @@ describe("api/editor/sshkeys", function() {
if (err) {
return done(err);
}
mockRuntime.storage.projects.ssh.getSSHKey.called.should.be.true();
mockRuntime.settings.getUserKey.called.should.be.true();
mockRuntime.settings.getUserKey.firstCall.args[0].should.eql({ user: undefined, id: 'test_key' });
res.body.should.be.deepEqual({ publickey: fileContent });
done();
});
@@ -210,16 +175,16 @@ describe("api/editor/sshkeys", function() {
errInstance.code = "test_code";
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.storage.projects.ssh.getSSHKey.returns(p);
mockRuntime.settings.getUserKey.returns(p);
request(app)
.get("/settings/user/keys/" + key_file_name)
.expect(400)
.expect(500)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal(errInstance.code);
res.body.should.have.property('code');
res.body.code.should.be.equal(errInstance.code);
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.message);
done();
@@ -231,25 +196,25 @@ describe("api/editor/sshkeys", function() {
var errInstance = new Error("Messages.....");
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.storage.projects.ssh.getSSHKey.returns(p);
mockRuntime.settings.getUserKey.returns(p);
request(app)
.get("/settings/user/keys/" + key_file_name)
.expect(400)
.expect(500)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal("unexpected_error");
res.body.should.have.property('code');
res.body.code.should.be.equal("unexpected_error");
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.toString());
res.body.message.should.be.equal("Messages.....");
done();
});
});
it('POST /settings/user/keys --- success', function(done) {
var key_file_name = "test_key";
mockRuntime.storage.projects.ssh.generateSSHKey.returns(Promise.resolve(key_file_name));
mockRuntime.settings.generateUserKey.returns(Promise.resolve(key_file_name));
request(app)
.post("/settings/user/keys")
.send({ name: key_file_name })
@@ -262,41 +227,23 @@ describe("api/editor/sshkeys", function() {
});
});
it('POST /settings/user/keys --- return parameter error', function(done) {
var key_file_name = "test_key";
mockRuntime.storage.projects.ssh.generateSSHKey.returns(Promise.resolve(key_file_name));
request(app)
.post("/settings/user/keys")
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal("unexpected_error");
res.body.should.have.property('message');
res.body.message.should.be.equal("You need to have body or body.name");
done();
});
});
it('POST /settings/user/keys --- return Error', function(done) {
var key_file_name = "test_key";
var errInstance = new Error("Messages.....");
errInstance.code = "test_code";
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.storage.projects.ssh.generateSSHKey.returns(p);
mockRuntime.settings.generateUserKey.returns(p);
request(app)
.post("/settings/user/keys")
.send({ name: key_file_name })
.expect(400)
.expect(500)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal("test_code");
res.body.should.have.property('code');
res.body.code.should.be.equal("test_code");
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.message);
done();
@@ -308,26 +255,26 @@ describe("api/editor/sshkeys", function() {
var errInstance = new Error("Messages.....");
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.storage.projects.ssh.generateSSHKey.returns(p);
mockRuntime.settings.generateUserKey.returns(p);
request(app)
.post("/settings/user/keys")
.send({ name: key_file_name })
.expect(400)
.expect(500)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal("unexpected_error");
res.body.should.have.property('code');
res.body.code.should.be.equal("unexpected_error");
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.toString());
res.body.message.should.be.equal("Messages.....");
done();
});
});
it('DELETE /settings/user/keys/<key_file_name> --- success', function(done) {
var key_file_name = "test_key";
mockRuntime.storage.projects.ssh.deleteSSHKey.returns(Promise.resolve(true));
mockRuntime.settings.removeUserKey.returns(Promise.resolve(true));
request(app)
.delete("/settings/user/keys/" + key_file_name)
.expect(204)
@@ -346,16 +293,16 @@ describe("api/editor/sshkeys", function() {
errInstance.code = "test_code";
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.storage.projects.ssh.deleteSSHKey.returns(p);
mockRuntime.settings.removeUserKey.returns(p);
request(app)
.delete("/settings/user/keys/" + key_file_name)
.expect(400)
.expect(500)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal("test_code");
res.body.should.have.property('code');
res.body.code.should.be.equal("test_code");
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.message);
done();
@@ -367,18 +314,18 @@ describe("api/editor/sshkeys", function() {
var errInstance = new Error("Messages.....");
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.storage.projects.ssh.deleteSSHKey.returns(p);
mockRuntime.settings.removeUserKey.returns(p);
request(app)
.delete("/settings/user/keys/" + key_file_name)
.expect(400)
.expect(500)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal("unexpected_error");
res.body.should.have.property('code');
res.body.code.should.be.equal("unexpected_error");
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.toString());
res.body.message.should.be.equal('Messages.....');
done();
});
});

View File

@@ -33,7 +33,7 @@ describe("api/editor/theme", function() {
fs.statSync.restore();
});
it("applies the default theme", function() {
var result = theme.init({settings:{},version:function() { return '123.456'}});
var result = theme.init({});
should.not.exist(result);
var context = theme.context();
@@ -43,13 +43,12 @@ describe("api/editor/theme", function() {
context.should.have.a.property("header");
context.header.should.have.a.property("title","Node-RED");
context.header.should.have.a.property("image","red/images/node-red.png");
context.should.have.a.property("version","123.456");
should.not.exist(theme.settings());
});
it("picks up custom theme", function() {
theme.init({settings:{
theme.init({
editorTheme: {
page: {
title: "Test Page Title",
@@ -84,7 +83,7 @@ describe("api/editor/theme", function() {
image: "/absolute/path/to/login/page/big/image" // a 256x256 image
}
}
}});
});
theme.app();

View File

@@ -20,8 +20,6 @@ var express = require("express");
var fs = require("fs");
var path = require("path");
var EventEmitter = require('events').EventEmitter;
var events = new EventEmitter();
var ui = require("../../../../red/api/editor/ui");
@@ -30,10 +28,13 @@ describe("api/editor/ui", function() {
before(function() {
ui.init({
events:events,
nodes: {
getNodeIconPath: function(module,icon) {
return path.resolve(__dirname+'/../../../../public/icons/arrow-in.png');
getIcon: function(opts) {
return new Promise(function(resolve,reject) {
fs.readFile(path.resolve(__dirname+'/../../../../public/icons/arrow-in.png'), function(err,data) {
resolve(data);
})
});
}
}
});

View File

@@ -23,7 +23,6 @@ var fs = require("fs");
var path = require("path");
var api = require("../../../red/api");
var apiUtil = require("../../../red/api/util");
var apiAuth = require("../../../red/api/auth");
var apiEditor = require("../../../red/api/editor");
var apiAdmin = require("../../../red/api/admin");
@@ -31,7 +30,6 @@ var apiAdmin = require("../../../red/api/admin");
describe("api/index", function() {
var beforeEach = function() {
sinon.stub(apiUtil,"init",function(){});
sinon.stub(apiAuth,"init",function(){});
sinon.stub(apiEditor,"init",function(){
var app = express();
@@ -48,7 +46,6 @@ describe("api/index", function() {
});
};
var afterEach = function() {
apiUtil.init.restore();
apiAuth.init.restore();
apiAuth.login.restore();
apiEditor.init.restore();
@@ -59,18 +56,14 @@ describe("api/index", function() {
afterEach(afterEach);
it("does not setup admin api if httpAdminRoot is false", function(done) {
api.init({},{
settings: { httpAdminRoot: false }
});
api.init({},{ httpAdminRoot: false },{},{});
should.not.exist(api.adminApp);
done();
});
describe('initalises admin api without adminAuth', function(done) {
before(function() {
beforeEach();
api.init({},{
settings: { }
});
api.init({},{},{},{});
});
after(afterEach);
it('exposes the editor',function(done) {
@@ -87,9 +80,7 @@ describe("api/index", function() {
describe('initalises admin api without editor', function(done) {
before(function() {
beforeEach();
api.init({},{
settings: { disableEditor: true }
});
api.init({},{ disableEditor: true },{},{});
});
after(afterEach);
it('does not expose the editor',function(done) {

View File

@@ -15,11 +15,17 @@
**/
var should = require("should");
var sinon = require("sinon");
var request = require('supertest');
var express = require('express');
var apiUtil = require("../../../red/api/util");
var log = require("../../../red/util").log; // TODO: separate module
var i18n = require("../../../red/util").i18n; // TODO: separate module
describe("api/util", function() {
describe("errorHandler", function() {
var loggedError = null;
@@ -27,17 +33,8 @@ describe("api/util", function() {
var app;
before(function() {
app = express();
apiUtil.init({
log:{
error: function(msg) {
loggedError = msg;
},
audit: function(event) {
loggedEvent = event;
}
},
i18n:{}
})
sinon.stub(log,'error',function(msg) {loggedError = msg;});
sinon.stub(log,'audit',function(event) {loggedEvent = event;});
app.get("/tooLarge", function(req,res) {
var err = new Error();
err.message = "request entity too large";
@@ -49,6 +46,10 @@ describe("api/util", function() {
throw err;
},apiUtil.errorHandler)
});
after(function() {
log.error.restore();
log.audit.restore();
})
beforeEach(function() {
loggedError = null;
loggedEvent = null;
@@ -91,11 +92,13 @@ describe("api/util", function() {
})
describe('determineLangFromHeaders', function() {
var oldDefaultLang;
before(function() {
apiUtil.init({
log:{},
i18n:{defaultLang:"en-US"}
});
oldDefaultLang = i18n.defaultLang;
i18n.defaultLang = "en-US";
})
after(function() {
i18n.defaultLang = oldDefaultLang;
})
it('returns the default lang if non provided', function() {
apiUtil.determineLangFromHeaders(null).should.eql("en-US");

View File

@@ -0,0 +1,242 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
var should = require("should");
var sinon = require("sinon");
var comms = require("../../../red/runtime-api/comms");
describe("runtime-api/comms", function() {
describe("listens for events", function() {
var messages = [];
var clientConnection = {
send: function(topic,data) {
messages.push({topic,data})
}
}
var eventHandlers = {};
before(function(done) {
comms.init({
log: {
trace: function(){}
},
events: {
removeListener: function() {},
on: function(evt,handler) {
eventHandlers[evt] = handler;
}
}
})
comms.addConnection({client: clientConnection}).then(done);
})
after(function(done) {
comms.removeConnection({client: clientConnection}).then(done);
})
afterEach(function() {
messages = [];
})
it('runtime events',function(){
eventHandlers.should.have.property('runtime-event');
eventHandlers['runtime-event']({
id: "my-event",
payload: "my-payload"
})
messages.should.have.length(1);
messages[0].should.have.property("topic","notification/my-event");
messages[0].should.have.property("data","my-payload")
})
it('status events',function(){
eventHandlers.should.have.property('node-status');
eventHandlers['node-status']({
id: "my-event",
status: "my-status"
})
messages.should.have.length(1);
messages[0].should.have.property("topic","status/my-event");
messages[0].should.have.property("data","my-status")
})
it('comms events',function(){
eventHandlers.should.have.property('runtime-event');
eventHandlers['comms']({
topic: "my-topic",
data: "my-payload"
})
messages.should.have.length(1);
messages[0].should.have.property("topic","my-topic");
messages[0].should.have.property("data","my-payload")
})
});
describe("manages connections", function() {
var eventHandlers = {};
var messages = [];
var clientConnection1 = {
send: function(topic,data) {
messages.push({topic,data})
}
}
var clientConnection2 = {
send: function(topic,data) {
messages.push({topic,data})
}
}
before(function() {
comms.init({
log: {
trace: function(){}
},
events: {
removeListener: function() {},
on: function(evt,handler) {
eventHandlers[evt] = handler;
}
}
})
})
afterEach(function(done) {
comms.removeConnection({client: clientConnection1}).then(function() {
comms.removeConnection({client: clientConnection2}).then(done);
});
messages = [];
})
it('adds new connections',function(done){
eventHandlers['comms']({
topic: "my-topic",
data: "my-payload"
})
messages.should.have.length(0);
comms.addConnection({client: clientConnection1}).then(function() {
eventHandlers['comms']({
topic: "my-topic",
data: "my-payload"
})
messages.should.have.length(1);
comms.addConnection({client: clientConnection2}).then(function() {
eventHandlers['comms']({
topic: "my-topic",
data: "my-payload"
})
messages.should.have.length(3);
done();
}).catch(done);
});
});
it('removes connections',function(done){
eventHandlers['comms']({
topic: "my-topic",
data: "my-payload"
})
messages.should.have.length(0);
comms.addConnection({client: clientConnection1}).then(function() {
comms.addConnection({client: clientConnection2}).then(function() {
eventHandlers['comms']({
topic: "my-topic",
data: "my-payload"
})
messages.should.have.length(2);
comms.removeConnection({client: clientConnection1}).then(function() {
eventHandlers['comms']({
topic: "my-topic",
data: "my-payload"
})
messages.should.have.length(3);
done();
});
}).catch(done);
});
})
})
describe("subscriptions", function() {
var messages = [];
var clientConnection = {
send: function(topic,data) {
messages.push({topic,data})
}
}
var clientConnection2 = {
send: function(topic,data) {
messages.push({topic,data})
}
}
var eventHandlers = {};
before(function() {
comms.init({
log: {
trace: function(){}
},
events: {
removeListener: function() {},
on: function(evt,handler) {
eventHandlers[evt] = handler;
}
}
})
})
afterEach(function(done) {
messages = [];
comms.removeConnection({client: clientConnection}).then(done);
})
it('subscribe triggers retained messages',function(done){
eventHandlers['comms']({
topic: "my-event",
data: "my-payload",
retain: true
})
messages.should.have.length(0);
comms.addConnection({client: clientConnection}).then(function() {
return comms.subscribe({client: clientConnection, topic: "my-event"}).then(function() {
messages.should.have.length(1);
messages[0].should.have.property("topic","my-event");
messages[0].should.have.property("data","my-payload");
done();
});
}).catch(done);
})
it('retained messages get cleared',function(done) {
eventHandlers['comms']({
topic: "my-event",
data: "my-payload",
retain: true
})
messages.should.have.length(0);
comms.addConnection({client: clientConnection}).then(function() {
return comms.subscribe({client: clientConnection, topic: "my-event"}).then(function() {
messages.should.have.length(1);
messages[0].should.have.property("topic","my-event");
messages[0].should.have.property("data","my-payload");
// Now we have a retained message, clear it
eventHandlers['comms']({
topic: "my-event",
data: "my-payload-cleared"
});
messages.should.have.length(2);
messages[1].should.have.property("topic","my-event");
messages[1].should.have.property("data","my-payload-cleared");
// Now add a second client and subscribe - no message should arrive
return comms.addConnection({client: clientConnection2}).then(function() {
return comms.subscribe({client: clientConnection2, topic: "my-event"}).then(function() {
messages.should.have.length(2);
done();
});
});
});
}).catch(done);
});
})
});

View File

@@ -0,0 +1,19 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
describe("runtime-api/context", function() {
it.skip("NEEDS TESTS WRITING",function() {});
});

View File

@@ -0,0 +1,413 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
var should = require("should");
var sinon = require("sinon");
var flows = require("../../../red/runtime-api/flows")
var mockLog = () => ({
log: sinon.stub(),
debug: sinon.stub(),
trace: sinon.stub(),
warn: sinon.stub(),
info: sinon.stub(),
metric: sinon.stub(),
audit: sinon.stub(),
_: function() { return "abc"}
})
describe("runtime-api/flows", function() {
describe("getFlows", function() {
it("returns the current flow configuration", function(done) {
flows.init({
log: mockLog(),
nodes: {
getFlows: function() { return [1,2,3] }
}
});
flows.getFlows({}).then(function(result) {
result.should.eql([1,2,3]);
done();
}).catch(done);
});
});
describe("setFlows", function() {
var setFlows;
var loadFlows;
var reloadError = false;
beforeEach(function() {
setFlows = sinon.spy(function(flows,type) {
if (flows[0] === "error") {
var err = new Error("error");
err.code = "error";
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
return Promise.resolve("newRev");
});
loadFlows = sinon.spy(function() {
if (!reloadError) {
return Promise.resolve("newLoadRev");
} else {
var err = new Error("error");
err.code = "error";
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
})
flows.init({
log: mockLog(),
nodes: {
getFlows: function() { return {rev:"currentRev",flows:[]} },
setFlows: setFlows,
loadFlows: loadFlows
}
})
})
it("defaults to full deploy", function(done) {
flows.setFlows({
flows: {flows:[4,5,6]}
}).then(function(result) {
result.should.eql({rev:"newRev"});
setFlows.called.should.be.true();
setFlows.lastCall.args[0].should.eql([4,5,6]);
setFlows.lastCall.args[1].should.eql("full");
done();
}).catch(done);
});
it("passes through other deploy types", function(done) {
flows.setFlows({
deploymentType: "nodes",
flows: {flows:[4,5,6]}
}).then(function(result) {
result.should.eql({rev:"newRev"});
setFlows.called.should.be.true();
setFlows.lastCall.args[0].should.eql([4,5,6]);
setFlows.lastCall.args[1].should.eql("nodes");
done();
}).catch(done);
});
it("triggers a flow reload", function(done) {
flows.setFlows({
deploymentType: "reload"
}).then(function(result) {
result.should.eql({rev:"newLoadRev"});
setFlows.called.should.be.false();
loadFlows.called.should.be.true();
done();
}).catch(done);
});
it("allows update when revision matches", function(done) {
flows.setFlows({
deploymentType: "nodes",
flows: {flows:[4,5,6],rev:"currentRev"}
}).then(function(result) {
result.should.eql({rev:"newRev"});
setFlows.called.should.be.true();
setFlows.lastCall.args[0].should.eql([4,5,6]);
setFlows.lastCall.args[1].should.eql("nodes");
done();
}).catch(done);
});
it("rejects update when revision does not match", function(done) {
flows.setFlows({
deploymentType: "nodes",
flows: {flows:[4,5,6],rev:"notTheCurrentRev"}
}).then(function(result) {
done(new Error("Did not reject rev mismatch"));
}).catch(function(err) {
err.should.have.property('code','version_mismatch');
err.should.have.property('status',409);
done();
}).catch(done);
});
it("rejects when reload fails",function(done) {
reloadError = true;
flows.setFlows({
deploymentType: "reload"
}).then(function(result) {
done(new Error("Did not return internal error"));
}).catch(function(err) {
err.should.have.property('code','error');
done();
}).catch(done);
});
it("rejects when update fails",function(done) {
flows.setFlows({
deploymentType: "full",
flows: {flows:["error",5,6]}
}).then(function(result) {
done(new Error("Did not return internal error"));
}).catch(function(err) {
err.should.have.property('code','error');
done();
}).catch(done);
});
});
describe("addFlow", function() {
var addFlow;
beforeEach(function() {
addFlow = sinon.spy(function(flow) {
if (flow === "error") {
var err = new Error("error");
err.code = "error";
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
return Promise.resolve("newId");
});
flows.init({
log: mockLog(),
nodes: {
addFlow: addFlow
}
});
})
it("adds a flow", function(done) {
flows.addFlow({flow:{a:"123"}}).then(function(id) {
addFlow.called.should.be.true();
addFlow.lastCall.args[0].should.eql({a:"123"});
id.should.eql("newId");
done()
}).catch(done);
});
it("rejects when add fails", function(done) {
flows.addFlow({flow:"error"}).then(function(id) {
done(new Error("Did not return internal error"));
}).catch(function(err) {
err.should.have.property('code','error');
done();
}).catch(done);
});
});
describe("getFlow", function() {
var getFlow;
beforeEach(function() {
getFlow = sinon.spy(function(flow) {
if (flow === "unknown") {
return null;
}
return [1,2,3];
});
flows.init({
log: mockLog(),
nodes: {
getFlow: getFlow
}
});
})
it("gets a flow", function(done) {
flows.getFlow({id:"123"}).then(function(flow) {
flow.should.eql([1,2,3]);
done()
}).catch(done);
});
it("rejects when flow not found", function(done) {
flows.getFlow({id:"unknown"}).then(function(flow) {
done(new Error("Did not return internal error"));
}).catch(function(err) {
err.should.have.property('code','not_found');
err.should.have.property('status',404);
done();
}).catch(done);
});
});
describe("updateFlow", function() {
var updateFlow;
beforeEach(function() {
updateFlow = sinon.spy(function(id,flow) {
if (id === "unknown") {
var err = new Error();
// TODO: quirk of internal api - uses .code for .status
err.code = 404;
throw err;
} else if (id === "error") {
var err = new Error();
// TODO: quirk of internal api - uses .code for .status
err.code = "error";
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
return Promise.resolve();
});
flows.init({
log: mockLog(),
nodes: {
updateFlow: updateFlow
}
});
})
it("updates a flow", function(done) {
flows.updateFlow({id:"123",flow:[1,2,3]}).then(function(id) {
id.should.eql("123");
updateFlow.called.should.be.true();
updateFlow.lastCall.args[0].should.eql("123");
updateFlow.lastCall.args[1].should.eql([1,2,3]);
done()
}).catch(done);
});
it("rejects when flow not found", function(done) {
flows.updateFlow({id:"unknown"}).then(function(flow) {
done(new Error("Did not return internal error"));
}).catch(function(err) {
err.should.have.property('code','not_found');
err.should.have.property('status',404);
done();
}).catch(done);
});
it("rejects when update fails", function(done) {
flows.updateFlow({id:"error"}).then(function(flow) {
done(new Error("Did not return internal error"));
}).catch(function(err) {
err.should.have.property('code','error');
err.should.have.property('status',400);
done();
}).catch(done);
});
});
describe("deleteFlow", function() {
var removeFlow;
beforeEach(function() {
removeFlow = sinon.spy(function(flow) {
if (flow === "unknown") {
var err = new Error();
// TODO: quirk of internal api - uses .code for .status
err.code = 404;
throw err;
} else if (flow === "error") {
var err = new Error();
// TODO: quirk of internal api - uses .code for .status
err.code = "error";
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
return Promise.resolve();
});
flows.init({
log: mockLog(),
nodes: {
removeFlow: removeFlow
}
});
})
it("deletes a flow", function(done) {
flows.deleteFlow({id:"123"}).then(function() {
removeFlow.called.should.be.true();
removeFlow.lastCall.args[0].should.eql("123");
done()
}).catch(done);
});
it("rejects when flow not found", function(done) {
flows.deleteFlow({id:"unknown"}).then(function(flow) {
done(new Error("Did not return internal error"));
}).catch(function(err) {
err.should.have.property('code','not_found');
err.should.have.property('status',404);
done();
}).catch(done);
});
it("rejects when delete fails", function(done) {
flows.deleteFlow({id:"error"}).then(function(flow) {
done(new Error("Did not return internal error"));
}).catch(function(err) {
err.should.have.property('code','error');
err.should.have.property('status',400);
done();
}).catch(done);
});
});
describe("getNodeCredentials", function() {
beforeEach(function() {
flows.init({
log: mockLog(),
nodes: {
getCredentials: function(id) {
if (id === "unknown") {
return undefined;
} else if (id === "known") {
return {
username: "abc",
password: "123"
}
} else if (id === "known2") {
return {
username: "abc",
password: ""
}
} else {
return {};
}
},
getCredentialDefinition: function(type) {
if (type === "node") {
return {
username: {type:"text"},
password: {type:"password"}
}
} else {
return null;
}
}
}
});
})
it("returns an empty object for an unknown node", function(done) {
flows.getNodeCredentials({id:"unknown", type:"node"}).then(function(result) {
result.should.eql({});
done();
}).catch(done);
});
it("gets the filtered credentials for a known node with password", function(done) {
flows.getNodeCredentials({id:"known", type:"node"}).then(function(result) {
result.should.eql({
username: "abc",
has_password: true
});
done();
}).catch(done);
});
it("gets the filtered credentials for a known node without password", function(done) {
flows.getNodeCredentials({id:"known2", type:"node"}).then(function(result) {
result.should.eql({
username: "abc",
has_password: false
});
done();
}).catch(done);
});
it("gets the empty credentials for a known node without a registered definition", function(done) {
flows.getNodeCredentials({id:"known2", type:"unknown-type"}).then(function(result) {
result.should.eql({});
done();
}).catch(done);
});
});
});

View File

@@ -0,0 +1,54 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
var should = require("should");
var sinon = require("sinon");
var index = require("../../../red/runtime-api/index");
describe("runtime-api/index", function() {
before(function() {
["comms","flows","nodes","settings","library","projects"].forEach(n => {
sinon.stub(require(`../../../red/runtime-api/${n}`),"init",()=>{});
})
});
after(function() {
["comms","flows","nodes","settings","library","projects"].forEach(n => {
require(`../../../red/runtime-api/${n}`).init.restore()
})
})
it('isStarted', function(done) {
index.init({
isStarted: ()=>true
});
index.isStarted({}).then(function(started) {
started.should.be.true();
done();
}).catch(done);
})
it('isStarted', function(done) {
index.init({
version: ()=>"1.2.3.4"
});
index.version({}).then(function(version) {
version.should.eql("1.2.3.4");
done();
}).catch(done);
})
});

View File

@@ -0,0 +1,537 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
var should = require("should");
var sinon = require("sinon");
var library = require("../../../red/runtime-api/library")
var mockLog = {
log: sinon.stub(),
debug: sinon.stub(),
trace: sinon.stub(),
warn: sinon.stub(),
info: sinon.stub(),
metric: sinon.stub(),
audit: sinon.stub(),
_: function() { return "abc"}
}
describe("runtime-api/library", function() {
describe("getEntry", function() {
before(function() {
library.init({
log: mockLog,
library: {
getEntry: function(type,path) {
if (type === "known") {
return Promise.resolve("known");
} else if (type === "forbidden") {
var err = new Error("forbidden");
err.code = "forbidden";
var p = Promise.reject(err);
p.catch(()=>{});
return p;
} else if (type === "not_found") {
var err = new Error("forbidden");
err.code = "not_found";
var p = Promise.reject(err);
p.catch(()=>{});
return p;
} else if (type === "error") {
var err = new Error("error");
err.code = "unknown_error";
var p = Promise.reject(err);
p.catch(()=>{});
return p;
} else if (type === "blank") {
return Promise.reject();
}
}
}
})
})
it("returns a known entry", function(done) {
library.getEntry({type: "known", path: "/abc"}).then(function(result) {
result.should.eql("known")
done();
}).catch(done)
})
it("rejects a forbidden entry", function(done) {
library.getEntry({type: "forbidden", path: "/abc"}).then(function(result) {
done(new Error("did not reject"));
}).catch(function(err) {
err.should.have.property("code","forbidden");
err.should.have.property("status",403);
done();
}).catch(done)
})
it("rejects an unknown entry", function(done) {
library.getEntry({type: "not_found", path: "/abc"}).then(function(result) {
done(new Error("did not reject"));
}).catch(function(err) {
err.should.have.property("code","not_found");
err.should.have.property("status",404);
done();
}).catch(done)
})
it("rejects a blank (unknown) entry", function(done) {
library.getEntry({type: "blank", path: "/abc"}).then(function(result) {
done(new Error("did not reject"));
}).catch(function(err) {
err.should.have.property("code","not_found");
err.should.have.property("status",404);
done();
}).catch(done)
})
it("rejects unexpected error", function(done) {
library.getEntry({type: "error", path: "/abc"}).then(function(result) {
done(new Error("did not reject"));
}).catch(function(err) {
err.should.have.property("status",400);
done();
}).catch(done)
})
})
describe("saveEntry", function() {
var opts;
before(function() {
library.init({
log: mockLog,
library: {
saveEntry: function(type,path,meta,body) {
opts = {type,path,meta,body};
if (type === "known") {
return Promise.resolve();
} else if (type === "forbidden") {
var err = new Error("forbidden");
err.code = "forbidden";
var p = Promise.reject(err);
p.catch(()=>{});
return p;
} else if (type === "not_found") {
var err = new Error("forbidden");
err.code = "not_found";
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
}
}
})
})
it("saves an entry", function(done) {
library.saveEntry({type: "known", path: "/abc", meta: {a:1}, body:"123"}).then(function() {
opts.should.have.property("type","known");
opts.should.have.property("path","/abc");
opts.should.have.property("meta",{a:1});
opts.should.have.property("body","123");
done();
}).catch(done)
})
it("rejects a forbidden entry", function(done) {
library.saveEntry({type: "forbidden", path: "/abc", meta: {a:1}, body:"123"}).then(function() {
done(new Error("did not reject"));
}).catch(function(err) {
err.should.have.property("code","forbidden");
err.should.have.property("status",403);
done();
}).catch(done)
})
it("rejects an unknown entry", function(done) {
library.saveEntry({type: "not_found", path: "/abc", meta: {a:1}, body:"123"}).then(function() {
done(new Error("did not reject"));
}).catch(function(err) {
err.should.have.property("status",400);
done();
}).catch(done)
})
})
describe("getEntries", function() {
var opts;
before(function() {
library.init({
log: mockLog,
storage: {
getAllFlows: function() {
return Promise.resolve({a:1});
}
},
nodes: {
getNodeExampleFlows: function() {
return {b:2};
}
}
});
});
it("returns all flows", function(done) {
library.getEntries({type:"flows"}).then(function(result) {
result.should.eql({a:1,d:{_examples_:{b:2}}});
done();
}).catch(done)
});
it("fails for non-flows (currently)", function(done) {
library.getEntries({type:"functions"}).then(function(result) {
done(new Error("did not reject"));
}).catch(function(err) {
done();
}).catch(done)
})
})
});
/*
var should = require("should");
var sinon = require("sinon");
var fs = require("fs");
var fspath = require('path');
var request = require('supertest');
var express = require('express');
var bodyParser = require('body-parser');
var when = require('when');
var app;
var library = require("../../../../red/api/editor/library");
var auth = require("../../../../red/api/auth");
describe("api/editor/library", function() {
function initLibrary(_flows,_libraryEntries,_examples,_exampleFlowPathFunction) {
var flows = _flows;
var libraryEntries = _libraryEntries;
library.init(app,{
log:{audit:function(){},_:function(){},warn:function(){}},
storage: {
init: function() {
return when.resolve();
},
getAllFlows: function() {
return when.resolve(flows);
},
getFlow: function(fn) {
if (flows[fn]) {
return when.resolve(flows[fn]);
} else if (fn.indexOf("..")!==-1) {
var err = new Error();
err.code = 'forbidden';
return when.reject(err);
} else {
return when.reject();
}
},
saveFlow: function(fn,data) {
if (fn.indexOf("..")!==-1) {
var err = new Error();
err.code = 'forbidden';
return when.reject(err);
}
flows[fn] = data;
return when.resolve();
},
getLibraryEntry: function(type,path) {
if (path.indexOf("..")!==-1) {
var err = new Error();
err.code = 'forbidden';
return when.reject(err);
}
if (libraryEntries[type] && libraryEntries[type][path]) {
return when.resolve(libraryEntries[type][path]);
} else {
return when.reject();
}
},
saveLibraryEntry: function(type,path,meta,body) {
if (path.indexOf("..")!==-1) {
var err = new Error();
err.code = 'forbidden';
return when.reject(err);
}
libraryEntries[type][path] = body;
return when.resolve();
}
},
events: {
on: function(){},
removeListener: function(){}
},
nodes: {
getNodeExampleFlows: function() {
return _examples;
},
getNodeExampleFlowPath: _exampleFlowPathFunction
}
});
}
describe("flows", function() {
before(function() {
app = express();
app.use(bodyParser.json());
app.get("/library/flows",library.getAll);
app.post(new RegExp("/library/flows\/(.*)"),library.post);
app.get(new RegExp("/library/flows\/(.*)"),library.get);
app.response.sendFile = function (path) {
app.response.json.call(this, {sendFile: path});
};
sinon.stub(fs,"statSync",function() { return true; });
});
after(function() {
fs.statSync.restore();
});
it('returns empty result', function(done) {
initLibrary({},{flows:{}});
request(app)
.get('/library/flows')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.not.have.property('f');
res.body.should.not.have.property('d');
done();
});
});
it('returns 404 for non-existent entry', function(done) {
initLibrary({},{flows:{}});
request(app)
.get('/library/flows/foo')
.expect(404)
.end(done);
});
it('can store and retrieve item', function(done) {
initLibrary({},{flows:{}});
var flow = '[]';
request(app)
.post('/library/flows/foo')
.set('Content-Type', 'application/json')
.send(flow)
.expect(204).end(function (err, res) {
if (err) {
throw err;
}
request(app)
.get('/library/flows/foo')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.text.should.equal(flow);
done();
});
});
});
it('lists a stored item', function(done) {
initLibrary({f:["bar"]});
request(app)
.get('/library/flows')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.have.property('f');
should.deepEqual(res.body.f,['bar']);
done();
});
});
it('returns 403 for malicious get attempt', function(done) {
initLibrary({});
// without the userDir override the malicious url would be
// http://127.0.0.1:1880/library/flows/../../package to
// obtain package.json from the node-red root.
request(app)
.get('/library/flows/../../../../../package')
.expect(403)
.end(done);
});
it('returns 403 for malicious post attempt', function(done) {
initLibrary({});
// without the userDir override the malicious url would be
// http://127.0.0.1:1880/library/flows/../../package to
// obtain package.json from the node-red root.
request(app)
.post('/library/flows/../../../../../package')
.expect(403)
.end(done);
});
it('includes examples flows if set', function(done) {
var examples = {"d":{"node-module":{"f":["example-one"]}}};
initLibrary({},{},examples);
request(app)
.get('/library/flows')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.have.property('d');
res.body.d.should.have.property('_examples_');
should.deepEqual(res.body.d._examples_,examples);
done();
});
});
it('can retrieve an example flow', function(done) {
var examples = {"d":{"node-module":{"f":["example-one"]}}};
initLibrary({},{},examples,function(module,path) {
return module + ':' + path
});
request(app)
.get('/library/flows/_examples_/node-module/example-one')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.have.property('sendFile',
'node-module:example-one');
done();
});
});
it('can retrieve an example flow in an org scoped package', function(done) {
var examples = {"d":{"@org_scope/node_package":{"f":["example-one"]}}};
initLibrary({},{},examples,function(module,path) {
return module + ':' + path
});
request(app)
.get('/library/flows/_examples_/@org_scope/node_package/example-one')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.have.property('sendFile',
'@org_scope/node_package:example-one');
done();
});
});
});
describe("type", function() {
before(function() {
app = express();
app.use(bodyParser.json());
initLibrary({},{});
auth.init({settings:{}});
library.register("test");
});
it('returns empty result', function(done) {
initLibrary({},{'test':{"":[]}});
request(app)
.get('/library/test')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.not.have.property('f');
done();
});
});
it('returns 404 for non-existent entry', function(done) {
initLibrary({},{});
request(app)
.get('/library/test/foo')
.expect(404)
.end(done);
});
it('can store and retrieve item', function(done) {
initLibrary({},{'test':{}});
var flow = {text:"test content"};
request(app)
.post('/library/test/foo')
.set('Content-Type', 'application/json')
.send(flow)
.expect(204).end(function (err, res) {
if (err) {
throw err;
}
request(app)
.get('/library/test/foo')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.text.should.equal(flow.text);
done();
});
});
});
it('lists a stored item', function(done) {
initLibrary({},{'test':{'a':['abc','def']}});
request(app)
.get('/library/test/a')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
// This response isn't strictly accurate - but it
// verifies the api returns what storage gave it
should.deepEqual(res.body,['abc','def']);
done();
});
});
it('returns 403 for malicious access attempt', function(done) {
request(app)
.get('/library/test/../../../../../../../../../../etc/passwd')
.expect(403)
.end(done);
});
it('returns 403 for malicious access attempt', function(done) {
request(app)
.get('/library/test/..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\etc\\passwd')
.expect(403)
.end(done);
});
it('returns 403 for malicious access attempt', function(done) {
request(app)
.post('/library/test/../../../../../../../../../../etc/passwd')
.set('Content-Type', 'text/plain')
.send('root:x:0:0:root:/root:/usr/bin/tclsh')
.expect(403)
.end(done);
});
});
});
*/

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,670 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* 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.
**/
var should = require("should");
var sinon = require("sinon");
var settings = require("../../../red/runtime-api/settings")
var mockLog = () => ({
log: sinon.stub(),
debug: sinon.stub(),
trace: sinon.stub(),
warn: sinon.stub(),
info: sinon.stub(),
metric: sinon.stub(),
audit: sinon.stub(),
_: function() { return "abc"}
})
describe("runtime-api/settings", function() {
describe.skip("getRuntimeSettings", function() {});
describe.skip("getUserSettings", function() {});
describe.skip("updateUserSettings", function() {});
describe.skip("getUserKeys", function() {});
describe.skip("getUserKey", function() {});
describe.skip("generateUserKey", function() {});
describe.skip("removeUserKey", function() {});
});
/*
before(function() {
sinon.stub(theme,"settings",function() { return { test: 456 };});
app = express();
app.get("/settings",info.runtimeSettings);
app.get("/settingsWithUser",function(req,res,next) {
req.user = {
username: "nick",
permissions: "*",
image: "http://example.com",
anonymous: false,
private: "secret"
}
next();
},info.runtimeSettings);
});
after(function() {
theme.settings.restore();
});
it('returns the filtered settings', function(done) {
info.init({
settings: {
foo: 123,
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion",
paletteCategories :["red","blue","green"],
exportNodeSettings: function(obj) {
obj.testNodeSetting = "helloWorld";
}
},
nodes: {
paletteEditorEnabled: function() { return true; },
getCredentialKeyType: function() { return "test-key-type"}
},
log: { error: console.error },
storage: {}
});
request(app)
.get("/settings")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property("httpNodeRoot","testHttpNodeRoot");
res.body.should.have.property("version","testVersion");
res.body.should.have.property("paletteCategories",["red","blue","green"]);
res.body.should.have.property("editorTheme",{test:456});
res.body.should.have.property("testNodeSetting","helloWorld");
res.body.should.not.have.property("foo",123);
res.body.should.have.property("flowEncryptionType","test-key-type");
res.body.should.not.have.property("user");
done();
});
});
it('returns the filtered user in settings', function(done) {
info.init({
settings: {
foo: 123,
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion",
paletteCategories :["red","blue","green"],
exportNodeSettings: function(obj) {
obj.testNodeSetting = "helloWorld";
}
},
nodes: {
paletteEditorEnabled: function() { return true; },
getCredentialKeyType: function() { return "test-key-type"}
},
log: { error: console.error },
storage: {}
});
request(app)
.get("/settingsWithUser")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property("user");
res.body.user.should.have.property("username","nick");
res.body.user.should.have.property("permissions","*");
res.body.user.should.have.property("image","http://example.com");
res.body.user.should.have.property("anonymous",false);
res.body.user.should.not.have.property("private");
done();
});
});
it('includes project settings if projects available', function(done) {
info.init({
settings: {
foo: 123,
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion",
paletteCategories :["red","blue","green"],
exportNodeSettings: function(obj) {
obj.testNodeSetting = "helloWorld";
}
},
nodes: {
paletteEditorEnabled: function() { return true; },
getCredentialKeyType: function() { return "test-key-type"}
},
log: { error: console.error },
storage: {
projects: {
getActiveProject: () => 'test-active-project',
getFlowFilename: () => 'test-flow-file',
getCredentialsFilename: () => 'test-creds-file',
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
}
}
});
request(app)
.get("/settings")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property("project","test-active-project");
res.body.should.not.have.property("files");
res.body.should.have.property("git");
res.body.git.should.have.property("globalUser",{name:'foo',email:'foo@example.com'});
done();
});
});
it('includes existing files details if projects enabled but no active project and files exist', function(done) {
info.init({
settings: {
foo: 123,
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion",
paletteCategories :["red","blue","green"],
exportNodeSettings: function(obj) {
obj.testNodeSetting = "helloWorld";
}
},
nodes: {
paletteEditorEnabled: function() { return true; },
getCredentialKeyType: function() { return "test-key-type"}
},
log: { error: console.error },
storage: {
projects: {
flowFileExists: () => true,
getActiveProject: () => false,
getFlowFilename: () => 'test-flow-file',
getCredentialsFilename: () => 'test-creds-file',
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
}
}
});
request(app)
.get("/settings")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.not.have.property("project");
res.body.should.have.property("files");
res.body.files.should.have.property("flow",'test-flow-file');
res.body.files.should.have.property("credentials",'test-creds-file');
res.body.should.have.property("git");
res.body.git.should.have.property("globalUser",{name:'foo',email:'foo@example.com'});
done();
});
});
it('does not include file details if projects enabled but no active project and files do not exist', function(done) {
info.init({
settings: {
foo: 123,
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion",
paletteCategories :["red","blue","green"],
exportNodeSettings: function(obj) {
obj.testNodeSetting = "helloWorld";
}
},
nodes: {
paletteEditorEnabled: function() { return true; },
getCredentialKeyType: function() { return "test-key-type"}
},
log: { error: console.error },
storage: {
projects: {
flowFileExists: () => false,
getActiveProject: () => false,
getFlowFilename: () => 'test-flow-file',
getCredentialsFilename: () => 'test-creds-file',
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
}
}
});
request(app)
.get("/settings")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.not.have.property("project");
res.body.should.not.have.property("files");
res.body.should.have.property("git");
res.body.git.should.have.property("globalUser",{name:'foo',email:'foo@example.com'});
done();
});
});
it('overrides palette editable if runtime says it is disabled', function(done) {
info.init({
settings: {
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion",
paletteCategories :["red","blue","green"],
exportNodeSettings: function() {}
},
nodes: {
paletteEditorEnabled: function() { return false; },
getCredentialKeyType: function() { return "test-key-type"}
},
log: { error: console.error },
storage: {}
});
request(app)
.get("/settings")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property("httpNodeRoot","testHttpNodeRoot");
res.body.should.have.property("version","testVersion");
res.body.should.have.property("paletteCategories",["red","blue","green"]);
res.body.should.have.property("editorTheme");
res.body.editorTheme.should.have.property("test",456);
res.body.editorTheme.should.have.property("palette",{editable:false});
done();
});
})
*/
/*
var should = require("should");
var sinon = require("sinon");
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");
var auth = require("../../../../red/api/auth");
var sshkeys = require("../../../../red/api/editor/sshkeys");
var when = require("when");
var bodyParser = require("body-parser");
var fs = require("fs-extra");
var fspath = require("path");
describe("api/editor/sshkeys", function() {
var app;
var mockList = [
'library','theme','locales','credentials','comms'
]
var isStarted = true;
var errors = [];
var session_data = {};
var mockRuntime = {
settings:{
httpNodeRoot: true,
httpAdminRoot: true,
disableEditor: false,
exportNodeSettings:function(){},
storage: {
getSessions: function(){
return when.resolve(session_data);
},
setSessions: function(_session) {
session_data = _session;
return when.resolve();
}
}
},
log:{audit:function(){},error:function(msg){errors.push(msg)},trace:function(){}},
storage: {
projects: {
ssh: {
init: function(){},
listSSHKeys: function(){},
getSSHKey: function(){},
generateSSHKey: function(){},
deleteSSHKey: function(){}
}
}
},
events:{on:function(){},removeListener:function(){}},
isStarted: function() { return isStarted; },
nodes: {paletteEditorEnabled: function() { return false }}
};
before(function() {
auth.init(mockRuntime);
app = express();
app.use(bodyParser.json());
app.use(editorApi.init({},mockRuntime));
});
after(function() {
})
beforeEach(function() {
sinon.stub(mockRuntime.storage.projects.ssh, "listSSHKeys");
sinon.stub(mockRuntime.storage.projects.ssh, "getSSHKey");
sinon.stub(mockRuntime.storage.projects.ssh, "generateSSHKey");
sinon.stub(mockRuntime.storage.projects.ssh, "deleteSSHKey");
})
afterEach(function() {
mockRuntime.storage.projects.ssh.listSSHKeys.restore();
mockRuntime.storage.projects.ssh.getSSHKey.restore();
mockRuntime.storage.projects.ssh.generateSSHKey.restore();
mockRuntime.storage.projects.ssh.deleteSSHKey.restore();
})
it('GET /settings/user/keys --- return empty list', function(done) {
mockRuntime.storage.projects.ssh.listSSHKeys.returns(Promise.resolve([]));
request(app)
.get("/settings/user/keys")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('keys');
res.body.keys.should.be.empty();
done();
});
});
it('GET /settings/user/keys --- return normal list', function(done) {
var fileList = [
'test_key01',
'test_key02'
];
var retList = fileList.map(function(elem) {
return {
name: elem
};
});
mockRuntime.storage.projects.ssh.listSSHKeys.returns(Promise.resolve(retList));
request(app)
.get("/settings/user/keys")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('keys');
for (var item of retList) {
res.body.keys.should.containEql(item);
}
done();
});
});
it('GET /settings/user/keys --- return Error', function(done) {
var errInstance = new Error("Messages here.....");
errInstance.code = "test_code";
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.storage.projects.ssh.listSSHKeys.returns(p);
request(app)
.get("/settings/user/keys")
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal(errInstance.code);
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.message);
done();
});
});
it('GET /settings/user/keys/<key_file_name> --- return 404', function(done) {
mockRuntime.storage.projects.ssh.getSSHKey.returns(Promise.resolve(null));
request(app)
.get("/settings/user/keys/NOT_REAL")
.expect(404)
.end(function(err,res) {
if (err) {
return done(err);
}
done();
});
});
it('GET /settings/user/keys --- return Unexpected Error', function(done) {
var errInstance = new Error("Messages.....");
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.storage.projects.ssh.listSSHKeys.returns(p);
request(app)
.get("/settings/user/keys")
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal("unexpected_error");
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.toString());
done();
});
});
it('GET /settings/user/keys/<key_file_name> --- return content', function(done) {
var key_file_name = "test_key";
var fileContent = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQD3a+sgtgzSbbliWxmOq5p6+H/mE+0gjWfLWrkIVmHENd1mifV4uCmIHAR2NfuadUYMQ3+bQ90kpmmEKTMYPsyentsKpHQZxTzG7wOCAIpJnbPTHDMxEJhVTaAwEjbVyMSIzTTPfnhoavWIBu0+uMgKDDlBm+RjlgkFlyhXyCN6UwFrIUUMH6Gw+eQHLiooKIl8ce7uDxIlt+9b7hFCU+sQ3kvuse239DZluu6+8buMWqJvrEHgzS9adRFKku8nSPAEPYn85vDi7OgVAcLQufknNgs47KHBAx9h04LeSrFJ/P5J1b//ItRpMOIme+O9d1BR46puzhvUaCHLdvO9czj+OmW+dIm+QIk6lZIOOMnppG72kZxtLfeKT16ur+2FbwAdL9ItBp4BI/YTlBPoa5mLMxpuWfmX1qHntvtGc9wEwS1P7YFfmF3XiK5apxalzrn0Qlr5UmDNbVIqJb1OlbC0w03Z0oktti1xT+R2DGOLWM4lBbpXDHV1BhQ7oYOvbUD8Cnof55lTP0WHHsOHlQc/BGDti1XA9aBX/OzVyzBUYEf0pkimsD0RYo6aqt7QwehJYdlz9x1NBguBffT0s4NhNb9IWr+ASnFPvNl2sw4XH/8U0J0q8ZkMpKkbLM1Zdp1Fv00GF0f5UNRokai6uM3w/ccantJ3WvZ6GtctqytWrw== \n";
mockRuntime.storage.projects.ssh.getSSHKey.returns(Promise.resolve(fileContent));
request(app)
.get("/settings/user/keys/" + key_file_name)
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
mockRuntime.storage.projects.ssh.getSSHKey.called.should.be.true();
res.body.should.be.deepEqual({ publickey: fileContent });
done();
});
});
it('GET /settings/user/keys/<key_file_name> --- return Error', function(done) {
var key_file_name = "test_key";
var errInstance = new Error("Messages.....");
errInstance.code = "test_code";
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.storage.projects.ssh.getSSHKey.returns(p);
request(app)
.get("/settings/user/keys/" + key_file_name)
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal(errInstance.code);
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.message);
done();
});
});
it('GET /settings/user/keys/<key_file_name> --- return Unexpected Error', function(done) {
var key_file_name = "test_key";
var errInstance = new Error("Messages.....");
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.storage.projects.ssh.getSSHKey.returns(p);
request(app)
.get("/settings/user/keys/" + key_file_name)
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal("unexpected_error");
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.toString());
done();
});
});
it('POST /settings/user/keys --- success', function(done) {
var key_file_name = "test_key";
mockRuntime.storage.projects.ssh.generateSSHKey.returns(Promise.resolve(key_file_name));
request(app)
.post("/settings/user/keys")
.send({ name: key_file_name })
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
done();
});
});
it('POST /settings/user/keys --- return parameter error', function(done) {
var key_file_name = "test_key";
mockRuntime.storage.projects.ssh.generateSSHKey.returns(Promise.resolve(key_file_name));
request(app)
.post("/settings/user/keys")
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal("unexpected_error");
res.body.should.have.property('message');
res.body.message.should.be.equal("You need to have body or body.name");
done();
});
});
it('POST /settings/user/keys --- return Error', function(done) {
var key_file_name = "test_key";
var errInstance = new Error("Messages.....");
errInstance.code = "test_code";
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.storage.projects.ssh.generateSSHKey.returns(p);
request(app)
.post("/settings/user/keys")
.send({ name: key_file_name })
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal("test_code");
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.message);
done();
});
});
it('POST /settings/user/keys --- return Unexpected error', function(done) {
var key_file_name = "test_key";
var errInstance = new Error("Messages.....");
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.storage.projects.ssh.generateSSHKey.returns(p);
request(app)
.post("/settings/user/keys")
.send({ name: key_file_name })
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal("unexpected_error");
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.toString());
done();
});
});
it('DELETE /settings/user/keys/<key_file_name> --- success', function(done) {
var key_file_name = "test_key";
mockRuntime.storage.projects.ssh.deleteSSHKey.returns(Promise.resolve(true));
request(app)
.delete("/settings/user/keys/" + key_file_name)
.expect(204)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.be.deepEqual({});
done();
});
});
it('DELETE /settings/user/keys/<key_file_name> --- return Error', function(done) {
var key_file_name = "test_key";
var errInstance = new Error("Messages.....");
errInstance.code = "test_code";
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.storage.projects.ssh.deleteSSHKey.returns(p);
request(app)
.delete("/settings/user/keys/" + key_file_name)
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal("test_code");
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.message);
done();
});
});
it('DELETE /settings/user/keys/<key_file_name> --- return Unexpected Error', function(done) {
var key_file_name = "test_key";
var errInstance = new Error("Messages.....");
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.storage.projects.ssh.deleteSSHKey.returns(p);
request(app)
.delete("/settings/user/keys/" + key_file_name)
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('error');
res.body.error.should.be.equal("unexpected_error");
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.toString());
done();
});
});
});
*/

View File

@@ -16,7 +16,7 @@
var should = require("should");
var deprecated = require("../../../../../red/runtime/nodes/registry/deprecated.js");
var deprecated = require("../../../red/runtime-registry/deprecated.js");
describe('deprecated', function() {
it('should return info on a node',function() {

View File

@@ -20,16 +20,13 @@ var path = require("path");
var when = require("when");
var fs = require("fs");
var registry = require("../../../../../red/runtime/nodes/registry");
var registry = require("../../../red/runtime-registry");
var installer = require("../../../../../red/runtime/nodes/registry/installer");
var loader = require("../../../../../red/runtime/nodes/registry/loader");
var typeRegistry = require("../../../../../red/runtime/nodes/registry/registry");
var installer = require("../../../red/runtime-registry/installer");
var loader = require("../../../red/runtime-registry/loader");
var typeRegistry = require("../../../red/runtime-registry/registry");
describe('red/nodes/registry/index', function() {
describe('red/registry/index', function() {
var stubs = [];
afterEach(function() {
while(stubs.length) {
@@ -60,7 +57,7 @@ describe('red/nodes/registry/index', function() {
registry.addModule("foo").then(function(info) {
info.should.eql("info");
done();
}).otherwise(function(err) { done(err); });
}).catch(function(err) { done(err); });
});
it('rejects if loader rejects', function(done) {
stubs.push(sinon.stub(loader,"addModule",function(module) {
@@ -71,7 +68,7 @@ describe('red/nodes/registry/index', function() {
}));
registry.addModule("foo").then(function(info) {
done(new Error("unexpected resolve"));
}).otherwise(function(err) {
}).catch(function(err) {
err.should.eql("error");
done();
})
@@ -90,7 +87,7 @@ describe('red/nodes/registry/index', function() {
typeRegistry.enableNodeSet.called.should.be.true();
ns.should.have.a.property('id','node-set');
done();
}).otherwise(function(err) { done(err); });
}).catch(function(err) { done(err); });
});
it('rejects if node unknown',function() {
@@ -121,7 +118,7 @@ describe('red/nodes/registry/index', function() {
ns.should.have.a.property('id','node-set');
ns.should.have.a.property('loaded',true);
done();
}).otherwise(function(err) { done(err); });
}).catch(function(err) { done(err); });
});
});

View File

@@ -22,14 +22,24 @@ var fs = require('fs');
var EventEmitter = require('events');
var child_process = require('child_process');
var installer = require("../../../../../red/runtime/nodes/registry/installer");
var registry = require("../../../../../red/runtime/nodes/registry/index");
var typeRegistry = require("../../../../../red/runtime/nodes/registry/registry");
var installer = require("../../../red/runtime-registry/installer");
var registry = require("../../../red/runtime-registry/index");
var typeRegistry = require("../../../red/runtime-registry/registry");
describe('nodes/registry/installer', function() {
var mockLog = {
log: sinon.stub(),
debug: sinon.stub(),
trace: sinon.stub(),
warn: sinon.stub(),
info: sinon.stub(),
metric: sinon.stub(),
_: function() { return "abc"}
}
before(function() {
installer.init({});
installer.init({log:mockLog, settings:{}, events: new EventEmitter()});
});
afterEach(function() {
if (child_process.spawn.restore) {
@@ -74,7 +84,7 @@ describe('nodes/registry/installer', function() {
});
installer.installModule("this_wont_exist").catch(function(err) {
err.code.should.be.eql(404);
err.should.have.property("code",404);
done();
});
});
@@ -156,7 +166,7 @@ describe('nodes/registry/installer', function() {
});
it("rejects when non-existant path is provided", function(done) {
this.timeout(20000);
var resourcesDir = path.resolve(path.join(__dirname,"..","resources","local","TestNodeModule","node_modules","NonExistant"));
var resourcesDir = path.resolve(path.join(__dirname,"resources","local","TestNodeModule","node_modules","NonExistant"));
installer.installModule(resourcesDir).then(function() {
done(new Error("Unexpected success"));
}).catch(function(err) {
@@ -176,7 +186,7 @@ describe('nodes/registry/installer', function() {
var addModule = sinon.stub(registry,"addModule",function(md) {
return when.resolve(nodeInfo);
});
var resourcesDir = path.resolve(path.join(__dirname,"..","resources","local","TestNodeModule","node_modules","TestNodeModule"));
var resourcesDir = path.resolve(path.join(__dirname,"resources","local","TestNodeModule","node_modules","TestNodeModule"));
sinon.stub(child_process,"spawn",function(cmd,args,opt) {
var ee = new EventEmitter();
ee.stdout = new EventEmitter();

Some files were not shown because too many files have changed in this diff Show More