node-red
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
src
|
||||
docs
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
Copyright OpenJS Foundation and other contributors, https://openjsf.org/
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -0,0 +1,12 @@
|
||||
@node-red/editor-api
|
||||
====================
|
||||
|
||||
Node-RED editor api module.
|
||||
|
||||
This provides an Express application that can be used to serve the Node-RED
|
||||
editor.
|
||||
|
||||
|
||||
### Source
|
||||
|
||||
The main Node-RED modules are maintained as a monorepo on [GitHub](https://github.com/node-red/node-red).
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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 apiUtils = require("../util");
|
||||
|
||||
var runtimeAPI;
|
||||
|
||||
|
||||
module.exports = {
|
||||
init: function(_runtimeAPI) {
|
||||
runtimeAPI = _runtimeAPI;
|
||||
},
|
||||
|
||||
get: function(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
scope: req.params.scope,
|
||||
id: req.params.id,
|
||||
key: req.params[0],
|
||||
store: req.query['store'],
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
if (req.query['keysOnly'] !== undefined) {
|
||||
opts.keysOnly = true
|
||||
}
|
||||
runtimeAPI.context.getValue(opts).then(function(result) {
|
||||
res.json(result);
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
},
|
||||
|
||||
delete: function(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
scope: req.params.scope,
|
||||
id: req.params.id,
|
||||
key: req.params[0],
|
||||
store: req.query['store'],
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.context.delete(opts).then(function(result) {
|
||||
res.status(204).end();
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
let runtimeAPI;
|
||||
let settings;
|
||||
const apiUtil = require("../util");
|
||||
module.exports = {
|
||||
init: function(_settings, _runtimeAPI) {
|
||||
settings = _settings;
|
||||
runtimeAPI = _runtimeAPI;
|
||||
},
|
||||
getReport: function(req, res) {
|
||||
const diagnosticsOpts = settings.diagnostics || {};
|
||||
const opts = {
|
||||
user: req.user,
|
||||
scope: diagnosticsOpts.level || "basic"
|
||||
}
|
||||
if(diagnosticsOpts.enabled === false || diagnosticsOpts.enabled === "false") {
|
||||
apiUtil.rejectHandler(req, res, {message: "diagnostics are disabled", status: 403, code: "diagnostics.disabled" })
|
||||
} else {
|
||||
runtimeAPI.diagnostics.get(opts)
|
||||
.then(function(result) { res.json(result); })
|
||||
.catch(err => apiUtil.rejectHandler(req, res, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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 runtimeAPI;
|
||||
var apiUtils = require("../util");
|
||||
|
||||
module.exports = {
|
||||
init: function(_runtimeAPI) {
|
||||
runtimeAPI = _runtimeAPI;
|
||||
},
|
||||
get: function(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
id: req.params.id,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.flows.getFlow(opts).then(function(result) {
|
||||
return res.json(result);
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
},
|
||||
post: function(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
flow: req.body,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.flows.addFlow(opts).then(function(id) {
|
||||
return res.json({id:id});
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
},
|
||||
put: function(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
id: req.params.id,
|
||||
flow: req.body,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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 opts = {
|
||||
user: req.user,
|
||||
id: req.params.id,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.flows.deleteFlow(opts).then(function() {
|
||||
res.status(204).end();
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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 runtimeAPI;
|
||||
var apiUtils = require("../util");
|
||||
|
||||
module.exports = {
|
||||
init: function(_runtimeAPI) {
|
||||
runtimeAPI = _runtimeAPI;
|
||||
},
|
||||
get: function(req,res) {
|
||||
var version = req.get("Node-RED-API-Version")||"v1";
|
||||
if (!/^v[12]$/.test(version)) {
|
||||
return res.status(400).json({code:"invalid_api_version", message:"Invalid API Version requested"});
|
||||
}
|
||||
var opts = {
|
||||
user: req.user,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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)) {
|
||||
return res.status(400).json({code:"invalid_api_version", message:"Invalid API Version requested"});
|
||||
}
|
||||
var opts = {
|
||||
user: req.user,
|
||||
deploymentType: req.get("Node-RED-Deployment-Type")||"full",
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
|
||||
if (opts.deploymentType !== 'reload') {
|
||||
if (version === "v1") {
|
||||
opts.flows = {flows: req.body}
|
||||
} else {
|
||||
opts.flows = req.body;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
})
|
||||
},
|
||||
getState: function(req,res) {
|
||||
const opts = {
|
||||
user: req.user,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.flows.getState(opts).then(function(result) {
|
||||
res.json(result);
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
},
|
||||
postState: function(req,res) {
|
||||
const opts = {
|
||||
user: req.user,
|
||||
state: req.body.state || "",
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.flows.setState(opts).then(function(result) {
|
||||
res.json(result);
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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 nodes = require("./nodes");
|
||||
var flows = require("./flows");
|
||||
var flow = require("./flow");
|
||||
var context = require("./context");
|
||||
var auth = require("../auth");
|
||||
var info = require("./settings");
|
||||
var plugins = require("./plugins");
|
||||
var diagnostics = require("./diagnostics");
|
||||
|
||||
var apiUtil = require("../util");
|
||||
|
||||
module.exports = {
|
||||
init: function(settings,runtimeAPI) {
|
||||
flows.init(runtimeAPI);
|
||||
flow.init(runtimeAPI);
|
||||
nodes.init(runtimeAPI);
|
||||
context.init(runtimeAPI);
|
||||
info.init(settings,runtimeAPI);
|
||||
plugins.init(runtimeAPI);
|
||||
diagnostics.init(settings, runtimeAPI);
|
||||
|
||||
const needsPermission = auth.needsPermission;
|
||||
|
||||
const adminApp = apiUtil.createExpressApp(settings)
|
||||
|
||||
// Flows
|
||||
adminApp.get("/flows",needsPermission("flows.read"),flows.get,apiUtil.errorHandler);
|
||||
adminApp.post("/flows",needsPermission("flows.write"),flows.post,apiUtil.errorHandler);
|
||||
|
||||
// Flows/state
|
||||
adminApp.get("/flows/state", needsPermission("flows.read"), flows.getState, apiUtil.errorHandler);
|
||||
if (settings.runtimeState && settings.runtimeState.enabled === true) {
|
||||
adminApp.post("/flows/state", needsPermission("flows.write"), flows.postState, apiUtil.errorHandler);
|
||||
}
|
||||
|
||||
// Flow
|
||||
adminApp.get("/flow/:id",needsPermission("flows.read"),flow.get,apiUtil.errorHandler);
|
||||
adminApp.post("/flow",needsPermission("flows.write"),flow.post,apiUtil.errorHandler);
|
||||
adminApp.delete("/flow/:id",needsPermission("flows.write"),flow.delete,apiUtil.errorHandler);
|
||||
adminApp.put("/flow/:id",needsPermission("flows.write"),flow.put,apiUtil.errorHandler);
|
||||
|
||||
// Nodes
|
||||
adminApp.get("/nodes",needsPermission("nodes.read"),nodes.getAll,apiUtil.errorHandler);
|
||||
|
||||
if (!settings.externalModules || !settings.externalModules.palette || settings.externalModules.palette.allowInstall !== false) {
|
||||
if (!settings.externalModules || !settings.externalModules.palette || settings.externalModules.palette.allowUpload !== false) {
|
||||
const multer = require('multer');
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
adminApp.post("/nodes",needsPermission("nodes.write"),upload.single("tarball"),nodes.post,apiUtil.errorHandler);
|
||||
} else {
|
||||
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);
|
||||
adminApp.get(/^\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.read"),nodes.getSet,apiUtil.errorHandler);
|
||||
adminApp.put(/^\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.write"),nodes.putSet,apiUtil.errorHandler);
|
||||
|
||||
// Context
|
||||
adminApp.get("/context/:scope(global)",needsPermission("context.read"),context.get,apiUtil.errorHandler);
|
||||
adminApp.get("/context/:scope(global)/*",needsPermission("context.read"),context.get,apiUtil.errorHandler);
|
||||
adminApp.get("/context/:scope(node|flow)/:id",needsPermission("context.read"),context.get,apiUtil.errorHandler);
|
||||
adminApp.get("/context/:scope(node|flow)/:id/*",needsPermission("context.read"),context.get,apiUtil.errorHandler);
|
||||
|
||||
// adminApp.delete("/context/:scope(global)",needsPermission("context.write"),context.delete,apiUtil.errorHandler);
|
||||
adminApp.delete("/context/:scope(global)/*",needsPermission("context.write"),context.delete,apiUtil.errorHandler);
|
||||
// adminApp.delete("/context/:scope(node|flow)/:id",needsPermission("context.write"),context.delete,apiUtil.errorHandler);
|
||||
adminApp.delete("/context/:scope(node|flow)/:id/*",needsPermission("context.write"),context.delete,apiUtil.errorHandler);
|
||||
|
||||
adminApp.get("/settings",needsPermission("settings.read"),info.runtimeSettings,apiUtil.errorHandler);
|
||||
|
||||
// Plugins
|
||||
adminApp.get("/plugins", needsPermission("plugins.read"), plugins.getAll, apiUtil.errorHandler);
|
||||
adminApp.get("/plugins/messages", needsPermission("plugins.read"), plugins.getCatalogs, apiUtil.errorHandler);
|
||||
adminApp.get(/^\/plugins\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("plugins.read"),plugins.getConfig,apiUtil.errorHandler);
|
||||
|
||||
adminApp.get("/diagnostics", needsPermission("diagnostics.read"), diagnostics.getReport, apiUtil.errorHandler);
|
||||
|
||||
return adminApp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* 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 apiUtils = require("../util");
|
||||
|
||||
var runtimeAPI;
|
||||
|
||||
module.exports = {
|
||||
init: function(_runtimeAPI) {
|
||||
runtimeAPI = _runtimeAPI;
|
||||
},
|
||||
getAll: function(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
if (req.get("accept") == "application/json") {
|
||||
runtimeAPI.nodes.getNodeList(opts).then(function(list) {
|
||||
res.json(list);
|
||||
})
|
||||
} else {
|
||||
opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages());
|
||||
if (/[^0-9a-z=\-\*]/i.test(opts.lang)) {
|
||||
opts.lang = "en-US";
|
||||
}
|
||||
runtimeAPI.nodes.getNodeConfigs(opts).then(function(configs) {
|
||||
res.send(configs);
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
post: function(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
module: req.body.module,
|
||||
version: req.body.version,
|
||||
url: req.body.url,
|
||||
tarball: undefined,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
if (!runtimeAPI.settings.editorTheme || !runtimeAPI.settings.editorTheme.palette || runtimeAPI.settings.editorTheme.palette.upload !== false) {
|
||||
if (req.file) {
|
||||
opts.tarball = {
|
||||
name: req.file.originalname,
|
||||
size: req.file.size,
|
||||
buffer: req.file.buffer
|
||||
}
|
||||
}
|
||||
}
|
||||
runtimeAPI.nodes.addModule(opts).then(function(info) {
|
||||
res.json(info);
|
||||
}).catch(function(err) {
|
||||
console.log(err.stack);
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
},
|
||||
|
||||
delete: function(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
module: req.params[0],
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.nodes.removeModule(opts).then(function() {
|
||||
res.status(204).end();
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
},
|
||||
|
||||
getSet: function(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
id: req.params[0] + "/" + req.params[2],
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
if (req.get("accept") === "application/json") {
|
||||
runtimeAPI.nodes.getNodeInfo(opts).then(function(result) {
|
||||
res.send(result);
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
} else {
|
||||
opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages());
|
||||
if (/[^0-9a-z=\-\*]/i.test(opts.lang)) {
|
||||
opts.lang = "en-US";
|
||||
}
|
||||
runtimeAPI.nodes.getNodeConfig(opts).then(function(result) {
|
||||
return res.send(result);
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
getModule: function(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
module: req.params[0],
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.nodes.getModuleInfo(opts).then(function(result) {
|
||||
res.send(result);
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
},
|
||||
|
||||
putSet: function(req,res) {
|
||||
var body = req.body;
|
||||
if (!body.hasOwnProperty("enabled")) {
|
||||
// log.audit({event: "nodes.module.set",error:"invalid_request"},req);
|
||||
res.status(400).json({code:"invalid_request", message:"Invalid request"});
|
||||
return;
|
||||
}
|
||||
var opts = {
|
||||
user: req.user,
|
||||
id: req.params[0] + "/" + req.params[2],
|
||||
enabled: body.enabled,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.nodes.setNodeSetState(opts).then(function(result) {
|
||||
res.send(result);
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
},
|
||||
|
||||
putModule: function(req,res) {
|
||||
var body = req.body;
|
||||
if (!body.hasOwnProperty("enabled")) {
|
||||
// log.audit({event: "nodes.module.set",error:"invalid_request"},req);
|
||||
res.status(400).json({code:"invalid_request", message:"Invalid request"});
|
||||
return;
|
||||
}
|
||||
var opts = {
|
||||
user: req.user,
|
||||
module: req.params[0],
|
||||
enabled: body.enabled,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
if (/[^0-9a-z=\-\*]/i.test(opts.lang)) {
|
||||
opts.lang = "en-US";
|
||||
}
|
||||
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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
if (/[^0-9a-z=\-\*]/i.test(opts.lang)) {
|
||||
opts.lang = "en-US";
|
||||
}
|
||||
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) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.nodes.getIconList(opts).then(function(list) {
|
||||
res.json(list);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
var apiUtils = require("../util");
|
||||
|
||||
var runtimeAPI;
|
||||
|
||||
module.exports = {
|
||||
init: function(_runtimeAPI) {
|
||||
runtimeAPI = _runtimeAPI;
|
||||
},
|
||||
getAll: function(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
if (req.get("accept") == "application/json") {
|
||||
runtimeAPI.plugins.getPluginList(opts).then(function(list) {
|
||||
res.json(list);
|
||||
})
|
||||
} else {
|
||||
opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages());
|
||||
if (/[^0-9a-z=\-\*]/i.test(opts.lang)) {
|
||||
opts.lang = "en-US";
|
||||
}
|
||||
runtimeAPI.plugins.getPluginConfigs(opts).then(function(configs) {
|
||||
res.send(configs);
|
||||
})
|
||||
}
|
||||
},
|
||||
getCatalogs: function(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
lang: req.query.lng,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
if (/[^0-9a-z=\-\*]/i.test(opts.lang)) {
|
||||
opts.lang = "en-US";
|
||||
}
|
||||
runtimeAPI.plugins.getPluginCatalogs(opts).then(function(result) {
|
||||
res.json(result);
|
||||
}).catch(function(err) {
|
||||
console.log(err.stack);
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
},
|
||||
getConfig: function(req, res) {
|
||||
|
||||
let opts = {
|
||||
user: req.user,
|
||||
module: req.params[0],
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
|
||||
if (req.get("accept") === "application/json") {
|
||||
runtimeAPI.nodes.getNodeInfo(opts.module).then(function(result) {
|
||||
res.send(result);
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
} else {
|
||||
opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages());
|
||||
if (/[^0-9a-z=\-\*]/i.test(opts.lang)) {
|
||||
opts.lang = "en-US";
|
||||
}
|
||||
runtimeAPI.plugins.getPluginConfig(opts).then(function(result) {
|
||||
return res.send(result);
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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 runtimeAPI;
|
||||
var settings;
|
||||
var theme = require("../editor/theme");
|
||||
var clone = require("clone");
|
||||
|
||||
var i18n = require("@node-red/util").i18n
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: function(_settings,_runtimeAPI) {
|
||||
runtimeAPI = _runtimeAPI;
|
||||
settings = _settings;
|
||||
},
|
||||
runtimeSettings: function(req,res) {
|
||||
var opts = {
|
||||
user: req.user
|
||||
}
|
||||
runtimeAPI.settings.getRuntimeSettings(opts).then(function(result) {
|
||||
if (!settings.disableEditor) {
|
||||
result.editorTheme = result.editorTheme||{};
|
||||
var themeSettings = theme.settings();
|
||||
if (themeSettings) {
|
||||
// result.editorTheme may already exist with the palette
|
||||
// disabled. Need to merge that into the receive settings
|
||||
result.editorTheme = extend(clone(themeSettings),result.editorTheme);
|
||||
}
|
||||
result.editorTheme.languages = i18n.availableLanguages("editor");
|
||||
}
|
||||
res.json(result);
|
||||
});
|
||||
},
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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 clients = [
|
||||
{id:"node-red-editor",secret:"not_available"},
|
||||
{id:"node-red-admin",secret:"not_available"}
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
get: function(id) {
|
||||
for (var i=0;i<clients.length;i++) {
|
||||
if (clients[i].id == id) {
|
||||
return Promise.resolve(clients[i]);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* 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.
|
||||
**/
|
||||
|
||||
|
||||
/**
|
||||
* @mixin @node-red/editor-api_auth
|
||||
*/
|
||||
|
||||
var passport = require("passport");
|
||||
var oauth2orize = require("oauth2orize");
|
||||
|
||||
var strategies = require("./strategies");
|
||||
var Tokens = require("./tokens");
|
||||
var Users = require("./users");
|
||||
var permissions = require("./permissions");
|
||||
|
||||
var theme = require("../editor/theme");
|
||||
|
||||
var settings = null;
|
||||
var log = require("@node-red/util").log; // TODO: separate module
|
||||
|
||||
|
||||
passport.use(strategies.bearerStrategy.BearerStrategy);
|
||||
passport.use(strategies.clientPasswordStrategy.ClientPasswordStrategy);
|
||||
passport.use(strategies.anonymousStrategy);
|
||||
passport.use(strategies.tokensStrategy);
|
||||
|
||||
var server = oauth2orize.createServer();
|
||||
|
||||
server.exchange(oauth2orize.exchange.password(strategies.passwordTokenExchange));
|
||||
|
||||
function init(_settings,storage) {
|
||||
settings = _settings;
|
||||
if (settings.adminAuth) {
|
||||
var mergedAdminAuth = Object.assign({}, settings.adminAuth, settings.adminAuth.module);
|
||||
Users.init(mergedAdminAuth);
|
||||
Tokens.init(mergedAdminAuth,storage);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns an Express middleware function that ensures the user making a request
|
||||
* has the necessary permission.
|
||||
*
|
||||
* @param {String} permission - the permission required for the request, such as `flows.write`
|
||||
* @return {Function} - an Express middleware
|
||||
* @memberof @node-red/editor-api_auth
|
||||
*/
|
||||
function needsPermission(permission) {
|
||||
return function(req,res,next) {
|
||||
if (settings && settings.adminAuth) {
|
||||
return passport.authenticate(['bearer','tokens','anon'],{ session: false })(req,res,function() {
|
||||
if (!req.user) {
|
||||
return next();
|
||||
}
|
||||
if (permissions.hasPermission(req.authInfo.scope,permission)) {
|
||||
return next();
|
||||
}
|
||||
log.audit({event: "permission.fail", permissions: permission},req);
|
||||
return res.status(401).end();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureClientSecret(req,res,next) {
|
||||
if (!req.body.client_secret) {
|
||||
req.body.client_secret = 'not_available';
|
||||
}
|
||||
next();
|
||||
}
|
||||
function authenticateClient(req,res,next) {
|
||||
return passport.authenticate(['oauth2-client-password'], {session: false})(req,res,next);
|
||||
}
|
||||
function getToken(req,res,next) {
|
||||
return server.token()(req,res,next);
|
||||
}
|
||||
|
||||
async function login(req,res) {
|
||||
var response = {};
|
||||
if (settings.adminAuth) {
|
||||
var mergedAdminAuth = Object.assign({}, settings.adminAuth, settings.adminAuth.module);
|
||||
if (mergedAdminAuth.type === "credentials") {
|
||||
response = {
|
||||
"type":"credentials",
|
||||
"prompts":[{id:"username",type:"text",label:"user.username"},{id:"password",type:"password",label:"user.password"}]
|
||||
}
|
||||
} else if (mergedAdminAuth.type === "strategy") {
|
||||
|
||||
var urlPrefix = (settings.httpAdminRoot||"").replace(/\/$/,"");
|
||||
if (urlPrefix.length > 0) {
|
||||
urlPrefix += "/";
|
||||
}
|
||||
response = {
|
||||
"type":"strategy"
|
||||
}
|
||||
if (mergedAdminAuth.strategy.autoLogin) {
|
||||
response.autoLogin = true
|
||||
response.loginRedirect = urlPrefix + "auth/strategy"
|
||||
}
|
||||
response.prompts = [
|
||||
{type:"button",label:mergedAdminAuth.strategy.label, url: urlPrefix + "auth/strategy"}
|
||||
]
|
||||
if (mergedAdminAuth.strategy.icon) {
|
||||
response.prompts[0].icon = mergedAdminAuth.strategy.icon;
|
||||
}
|
||||
if (mergedAdminAuth.strategy.image) {
|
||||
response.prompts[0].image = theme.serveFile('/login/',mergedAdminAuth.strategy.image);
|
||||
}
|
||||
}
|
||||
let themeContext = await theme.context();
|
||||
if (themeContext.login && themeContext.login.image) {
|
||||
response.image = themeContext.login.image;
|
||||
}
|
||||
if (themeContext.login?.message) {
|
||||
response.loginMessage = themeContext.login?.message
|
||||
}
|
||||
if (themeContext.login?.button) {
|
||||
response.prompts = [
|
||||
{ type: "button", ...themeContext.login.button }
|
||||
]
|
||||
}
|
||||
}
|
||||
res.json(response);
|
||||
}
|
||||
|
||||
function revoke(req,res) {
|
||||
var token = req.body.token;
|
||||
// TODO: audit log
|
||||
Tokens.revoke(token).then(function() {
|
||||
log.audit({event: "auth.login.revoke"},req);
|
||||
if (settings.editorTheme && settings.editorTheme.logout && settings.editorTheme.logout.redirect) {
|
||||
res.json({redirect:settings.editorTheme.logout.redirect});
|
||||
} else {
|
||||
res.status(200).end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function completeVerify(profile,done) {
|
||||
Users.authenticate(profile).then(function(user) {
|
||||
if (user) {
|
||||
Tokens.create(user.username,"node-red-editor",user.permissions).then(function(tokens) {
|
||||
log.audit({event: "auth.login",user,username:user.username,scope:user.permissions});
|
||||
user.tokens = tokens;
|
||||
done(null,user);
|
||||
});
|
||||
} else {
|
||||
log.audit({event: "auth.login.fail.oauth",username:typeof profile === "string"?profile:profile.username});
|
||||
done(null,false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function genericStrategy(adminApp,strategy) {
|
||||
const crypto = require("crypto")
|
||||
const session = require('express-session')
|
||||
const MemoryStore = require('memorystore')(session)
|
||||
|
||||
const sessionOptions = {
|
||||
// As the session is only used across the life-span of an auth
|
||||
// hand-shake, we can use a instance specific random string
|
||||
secret: crypto.randomBytes(20).toString('hex'),
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
store: new MemoryStore({
|
||||
checkPeriod: 86400000 // prune expired entries every 24h
|
||||
})
|
||||
}
|
||||
if (settings.httpAdminCookieOptions) {
|
||||
sessionOptions.cookie = {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
maxAge: null,
|
||||
...settings.httpAdminCookieOptions
|
||||
}
|
||||
if (sessionOptions.cookie.name){
|
||||
sessionOptions.name = sessionOptions.cookie.name
|
||||
delete sessionOptions.cookie.name
|
||||
}
|
||||
}
|
||||
adminApp.use(session(sessionOptions));
|
||||
//TODO: all passport references ought to be in ./auth
|
||||
adminApp.use(passport.initialize());
|
||||
adminApp.use(passport.session());
|
||||
|
||||
var options = strategy.options;
|
||||
var verify = function() {
|
||||
var originalDone = arguments[arguments.length-1];
|
||||
if (options.verify) {
|
||||
var args = Array.from(arguments);
|
||||
args[args.length-1] = function(err,profile) {
|
||||
if (err) {
|
||||
return originalDone(err);
|
||||
} else {
|
||||
return completeVerify(profile,originalDone);
|
||||
}
|
||||
};
|
||||
|
||||
options.verify.apply(this,args);
|
||||
} else {
|
||||
var profile = arguments[arguments.length - 2];
|
||||
return completeVerify(profile,originalDone);
|
||||
}
|
||||
};
|
||||
// Give our callback the same arity as the original one from options
|
||||
if (options.verify) {
|
||||
Object.defineProperty(verify, "length", { value: options.verify.length })
|
||||
}
|
||||
|
||||
passport.use(new strategy.strategy(options, verify));
|
||||
|
||||
adminApp.get('/auth/strategy',
|
||||
passport.authenticate(strategy.name, {
|
||||
session:false,
|
||||
failWithError: true,
|
||||
failureMessage: true
|
||||
}),
|
||||
completeGenericStrategyAuth,
|
||||
handleStrategyError
|
||||
);
|
||||
|
||||
var callbackMethodFunc = adminApp.get;
|
||||
if (/^post$/i.test(options.callbackMethod)) {
|
||||
callbackMethodFunc = adminApp.post;
|
||||
}
|
||||
callbackMethodFunc.call(adminApp,'/auth/strategy/callback',
|
||||
passport.authenticate(strategy.name, {
|
||||
session:false,
|
||||
failureMessage: true,
|
||||
failWithError: true
|
||||
}),
|
||||
completeGenericStrategyAuth,
|
||||
handleStrategyError
|
||||
);
|
||||
|
||||
}
|
||||
function completeGenericStrategyAuth(req,res) {
|
||||
var tokens = req.user.tokens;
|
||||
delete req.user.tokens;
|
||||
// Successful authentication, redirect home.
|
||||
res.redirect(settings.httpAdminRoot + '?access_token='+tokens.accessToken);
|
||||
}
|
||||
function handleStrategyError(err, req, res, next) {
|
||||
if (res.headersSent) {
|
||||
return next(err)
|
||||
}
|
||||
// Remove the header that passport auto-adds as we don't need it
|
||||
res.removeHeader('WWW-Authenticate')
|
||||
log.audit({event: "auth.login.fail.oauth",error:err.toString()});
|
||||
res.redirect(settings.httpAdminRoot + '?session_message='+err.toString());
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: init,
|
||||
needsPermission: needsPermission,
|
||||
ensureClientSecret: ensureClientSecret,
|
||||
authenticateClient: authenticateClient,
|
||||
getToken: getToken,
|
||||
errorHandler: function(err,req,res,next) {
|
||||
//TODO: audit log statment
|
||||
//console.log(err.stack);
|
||||
//log.log({level:"audit",type:"auth",msg:err.toString()});
|
||||
return server.errorHandler()(err,req,res,next);
|
||||
},
|
||||
login: login,
|
||||
revoke: revoke,
|
||||
genericStrategy: genericStrategy
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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 util = require('util');
|
||||
|
||||
var readRE = /^((.+)\.)?read$/
|
||||
var writeRE = /^((.+)\.)?write$/
|
||||
|
||||
function hasPermission(userScope,permission) {
|
||||
if (permission === "") {
|
||||
return true;
|
||||
}
|
||||
var i;
|
||||
|
||||
if (Array.isArray(permission)) {
|
||||
// Multiple permissions requested - check each one
|
||||
for (i=0;i<permission.length;i++) {
|
||||
if (!hasPermission(userScope,permission[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// All permissions check out
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Array.isArray(userScope)) {
|
||||
if (userScope.length === 0) {
|
||||
return false;
|
||||
}
|
||||
for (i=0;i<userScope.length;i++) {
|
||||
if (hasPermission(userScope[i],permission)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (userScope === "*" || userScope === permission) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (userScope === "read" || userScope === "*.read") {
|
||||
return readRE.test(permission);
|
||||
} else if (userScope === "write" || userScope === "*.write") {
|
||||
return writeRE.test(permission);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
hasPermission: hasPermission,
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* 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 BearerStrategy = require('passport-http-bearer').Strategy;
|
||||
var ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy;
|
||||
|
||||
var passport = require("passport");
|
||||
var util = require("util");
|
||||
|
||||
var Tokens = require("./tokens");
|
||||
var Users = require("./users");
|
||||
var Clients = require("./clients");
|
||||
var permissions = require("./permissions");
|
||||
|
||||
var log = require("@node-red/util").log; // TODO: separate module
|
||||
|
||||
var bearerStrategy = function (accessToken, done) {
|
||||
// is this a valid token?
|
||||
Tokens.get(accessToken).then(function(token) {
|
||||
if (token) {
|
||||
Users.get(token.user).then(function(user) {
|
||||
if (user) {
|
||||
done(null,user,{scope:token.scope});
|
||||
} else {
|
||||
log.audit({event: "auth.invalid-token"});
|
||||
done(null,false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log.audit({event: "auth.invalid-token"});
|
||||
done(null,false);
|
||||
}
|
||||
});
|
||||
}
|
||||
bearerStrategy.BearerStrategy = new BearerStrategy(bearerStrategy);
|
||||
|
||||
var clientPasswordStrategy = function(clientId, clientSecret, done) {
|
||||
Clients.get(clientId).then(function(client) {
|
||||
if (client && client.secret == clientSecret) {
|
||||
done(null,client);
|
||||
} else {
|
||||
log.audit({event: "auth.invalid-client",client:clientId});
|
||||
done(null,false);
|
||||
}
|
||||
});
|
||||
}
|
||||
clientPasswordStrategy.ClientPasswordStrategy = new ClientPasswordStrategy(clientPasswordStrategy);
|
||||
|
||||
var loginAttempts = [];
|
||||
var loginSignInWindow = 600000; // 10 minutes
|
||||
|
||||
|
||||
var passwordTokenExchange = function(client, username, password, scope, done) {
|
||||
var now = Date.now();
|
||||
loginAttempts = loginAttempts.filter(function(logEntry) {
|
||||
return logEntry.time + loginSignInWindow > now;
|
||||
});
|
||||
loginAttempts.push({time:now, user:username});
|
||||
var attemptCount = 0;
|
||||
loginAttempts.forEach(function(logEntry) {
|
||||
/* istanbul ignore else */
|
||||
if (logEntry.user == username) {
|
||||
attemptCount++;
|
||||
}
|
||||
});
|
||||
if (attemptCount > 5) {
|
||||
log.audit({event: "auth.login.fail.too-many-attempts",username:username,client:client.id});
|
||||
done(new Error("Too many login attempts. Wait 10 minutes and try again"),false);
|
||||
return;
|
||||
}
|
||||
|
||||
Users.authenticate(username,password).then(function(user) {
|
||||
if (user) {
|
||||
if (scope === "") {
|
||||
scope = user.permissions;
|
||||
}
|
||||
if (permissions.hasPermission(user.permissions,scope)) {
|
||||
loginAttempts = loginAttempts.filter(function(logEntry) {
|
||||
return logEntry.user !== username;
|
||||
});
|
||||
// Check if the user contains a user defined token and use it
|
||||
// instead of generating a new token
|
||||
if(user.token){
|
||||
done(null,user.token,null,null);
|
||||
} else {
|
||||
Tokens.create(username,client.id,scope).then(function(tokens) {
|
||||
log.audit({event: "auth.login",user,username:username,client:client.id,scope:scope});
|
||||
done(null,tokens.accessToken,null,{expires_in:tokens.expires_in});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log.audit({event: "auth.login.fail.permissions",username:username,client:client.id,scope:scope});
|
||||
done(null,false);
|
||||
}
|
||||
} else {
|
||||
log.audit({event: "auth.login.fail.credentials",username:username,client:client.id,scope:scope});
|
||||
done(null,false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function AnonymousStrategy() {
|
||||
passport.Strategy.call(this);
|
||||
this.name = 'anon';
|
||||
}
|
||||
util.inherits(AnonymousStrategy, passport.Strategy);
|
||||
AnonymousStrategy.prototype.authenticate = function(req) {
|
||||
var self = this;
|
||||
Users.default().then(function(anon) {
|
||||
if (anon) {
|
||||
self.success(anon,{scope:anon.permissions});
|
||||
} else {
|
||||
self.fail(401);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function authenticateUserToken(req) {
|
||||
return new Promise( (resolve,reject) => {
|
||||
var token = null;
|
||||
var tokenHeader = Users.tokenHeader();
|
||||
if (Users.tokenHeader() === null) {
|
||||
// No custom user token provided. Fail the request
|
||||
reject();
|
||||
return;
|
||||
} else if (Users.tokenHeader() === 'authorization') {
|
||||
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
|
||||
token = req.headers.authorization.split(' ')[1];
|
||||
}
|
||||
} else {
|
||||
token = req.headers[Users.tokenHeader()];
|
||||
}
|
||||
if (token) {
|
||||
Users.tokens(token).then(function(user) {
|
||||
if (user) {
|
||||
resolve(user);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
}).catch(reject);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function TokensStrategy() {
|
||||
passport.Strategy.call(this);
|
||||
this.name = 'tokens';
|
||||
}
|
||||
util.inherits(TokensStrategy, passport.Strategy);
|
||||
TokensStrategy.prototype.authenticate = function(req) {
|
||||
authenticateUserToken(req).then(user => {
|
||||
this.success(user,{scope:user.permissions});
|
||||
}).catch(err => {
|
||||
if (err) {
|
||||
log.trace("token authentication failure: "+err.stack?err.stack:err)
|
||||
}
|
||||
this.fail(401);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = {
|
||||
bearerStrategy: bearerStrategy,
|
||||
clientPasswordStrategy: clientPasswordStrategy,
|
||||
passwordTokenExchange: passwordTokenExchange,
|
||||
anonymousStrategy: new AnonymousStrategy(),
|
||||
tokensStrategy: new TokensStrategy(),
|
||||
authenticateUserToken: authenticateUserToken
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 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.
|
||||
**/
|
||||
|
||||
const crypto = require("crypto");
|
||||
|
||||
var storage;
|
||||
var sessionExpiryTime
|
||||
var sessions = {};
|
||||
var loadedSessions = null;
|
||||
var apiAccessTokens;
|
||||
var sessionExpiryListeners = [];
|
||||
var expiryTimeout;
|
||||
|
||||
function expireSessions() {
|
||||
if (expiryTimeout) {
|
||||
clearTimeout(expiryTimeout);
|
||||
expiryTimeout = null;
|
||||
}
|
||||
var nextExpiry = Number.MAX_SAFE_INTEGER;
|
||||
var now = Date.now();
|
||||
var modified = false;
|
||||
for (var t in sessions) {
|
||||
if (sessions.hasOwnProperty(t)) {
|
||||
var session = sessions[t];
|
||||
if (!session.hasOwnProperty("expires") || session.expires < now) {
|
||||
sessionExpiryListeners.forEach(listener => { listener(session) })
|
||||
delete sessions[t];
|
||||
modified = true;
|
||||
} else {
|
||||
if (session.expires < nextExpiry) {
|
||||
nextExpiry = session.expires;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nextExpiry < Number.MAX_SAFE_INTEGER) {
|
||||
// Allow 5 seconds grace
|
||||
expiryTimeout = setTimeout(expireSessions,Math.min(2147483647,(nextExpiry - Date.now()) + 5000))
|
||||
}
|
||||
if (modified) {
|
||||
return storage.saveSessions(sessions);
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
function loadSessions() {
|
||||
if (loadedSessions === null) {
|
||||
loadedSessions = storage.getSessions().then(function(_sessions) {
|
||||
sessions = _sessions||{};
|
||||
return expireSessions();
|
||||
});
|
||||
}
|
||||
return loadedSessions;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: function(adminAuthSettings, _storage) {
|
||||
storage = _storage;
|
||||
|
||||
sessionExpiryListeners = [];
|
||||
|
||||
sessionExpiryTime = adminAuthSettings.sessionExpiryTime || 604800; // 1 week in seconds
|
||||
// At this point, storage will not have been initialised, so defer loading
|
||||
// the sessions until there's a request for them.
|
||||
loadedSessions = null;
|
||||
|
||||
apiAccessTokens = {};
|
||||
if ( Array.isArray(adminAuthSettings.tokens) ) {
|
||||
apiAccessTokens = adminAuthSettings.tokens.reduce(function(prev, current) {
|
||||
prev[current.token] = {
|
||||
user: current.user,
|
||||
scope: current.scope
|
||||
};
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
get: function(token) {
|
||||
return loadSessions().then(function() {
|
||||
var info = apiAccessTokens[token] || null;
|
||||
|
||||
if (info) {
|
||||
return Promise.resolve(info);
|
||||
} else {
|
||||
if (sessions[token]) {
|
||||
if (sessions[token].expires < Date.now()) {
|
||||
return expireSessions().then(function() { return null });
|
||||
}
|
||||
}
|
||||
return Promise.resolve(sessions[token]);
|
||||
}
|
||||
});
|
||||
},
|
||||
create: function(user,client,scope) {
|
||||
return loadSessions().then(function() {
|
||||
var accessToken = crypto.randomBytes(128).toString('base64');
|
||||
|
||||
var accessTokenExpiresAt = Date.now() + (sessionExpiryTime*1000);
|
||||
|
||||
var session = {
|
||||
user:user,
|
||||
client:client,
|
||||
scope:scope,
|
||||
accessToken: accessToken,
|
||||
expires: accessTokenExpiresAt
|
||||
};
|
||||
sessions[accessToken] = session;
|
||||
|
||||
if (!expiryTimeout) {
|
||||
expiryTimeout = setTimeout(expireSessions,Math.min(2147483647,(accessTokenExpiresAt - Date.now()) + 5000))
|
||||
}
|
||||
|
||||
return storage.saveSessions(sessions).then(function() {
|
||||
return {
|
||||
accessToken: accessToken,
|
||||
expires_in: sessionExpiryTime
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
revoke: function(token) {
|
||||
return loadSessions().then(function() {
|
||||
delete sessions[token];
|
||||
return storage.saveSessions(sessions);
|
||||
});
|
||||
},
|
||||
onSessionExpiry: function(callback) {
|
||||
sessionExpiryListeners.push(callback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* 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 util = require("util");
|
||||
var clone = require("clone");
|
||||
var bcrypt;
|
||||
try { bcrypt = require('@node-rs/bcrypt'); }
|
||||
catch(e) { bcrypt = require('bcryptjs'); }
|
||||
var users = {};
|
||||
var defaultUser = null;
|
||||
|
||||
function authenticate() {
|
||||
var username = arguments[0];
|
||||
if (typeof username !== 'string') {
|
||||
username = username.username;
|
||||
}
|
||||
const args = Array.from(arguments);
|
||||
return api.get(username).then(function(user) {
|
||||
if (user) {
|
||||
if (args.length === 2) {
|
||||
// Username/password authentication
|
||||
var password = args[1];
|
||||
return bcrypt.compare(password, user.password).then(res => {
|
||||
return res ? cleanUser(user) : null
|
||||
}).catch(err => {
|
||||
return null
|
||||
})
|
||||
} else {
|
||||
// Try to extract common profile information
|
||||
if (args[0].hasOwnProperty('photos') && args[0].photos.length > 0) {
|
||||
user.image = args[0].photos[0].value;
|
||||
}
|
||||
return cleanUser(user);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
function get(username) {
|
||||
return Promise.resolve(users[username]);
|
||||
}
|
||||
function getDefaultUser() {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
var api = {
|
||||
get: get,
|
||||
authenticate: authenticate,
|
||||
default: getDefaultUser,
|
||||
tokens: getDefaultUser,
|
||||
tokenHeader: null
|
||||
}
|
||||
|
||||
function init(config) {
|
||||
users = {};
|
||||
defaultUser = null;
|
||||
if (config.type == "credentials" || config.type == "strategy") {
|
||||
if (config.users) {
|
||||
if (typeof config.users === "function") {
|
||||
api.get = config.users;
|
||||
} else {
|
||||
var us = config.users;
|
||||
/* istanbul ignore else */
|
||||
if (!Array.isArray(us)) {
|
||||
us = [us];
|
||||
}
|
||||
for (var i=0;i<us.length;i++) {
|
||||
var u = us[i];
|
||||
users[u.username] = clone(u);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (config.authenticate && typeof config.authenticate === "function") {
|
||||
api.authenticate = config.authenticate;
|
||||
} else {
|
||||
api.authenticate = authenticate;
|
||||
}
|
||||
} else {
|
||||
api.get = get;
|
||||
api.authenticate = authenticate;
|
||||
api.default = api.default;
|
||||
}
|
||||
if (config.default) {
|
||||
if (typeof config.default === "function") {
|
||||
api.default = config.default;
|
||||
} else {
|
||||
api.default = function() {
|
||||
return Promise.resolve({
|
||||
"anonymous": true,
|
||||
"permissions":config.default.permissions
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
api.default = getDefaultUser;
|
||||
}
|
||||
if (config.tokens && typeof config.tokens === "function") {
|
||||
api.tokens = config.tokens;
|
||||
if (config.tokenHeader && typeof config.tokenHeader === "string") {
|
||||
api.tokenHeader = config.tokenHeader.toLowerCase();
|
||||
} else {
|
||||
api.tokenHeader = "authorization";
|
||||
}
|
||||
}
|
||||
}
|
||||
function cleanUser(user) {
|
||||
if (user && user.hasOwnProperty('password')) {
|
||||
user = clone(user);
|
||||
delete user.password;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: init,
|
||||
get: function(username) { return api.get(username).then(cleanUser)},
|
||||
authenticate: function() { return api.authenticate.apply(null, arguments) },
|
||||
default: function() { return api.default(); },
|
||||
tokens: function(token) { return api.tokens(token); },
|
||||
tokenHeader: function() { return api.tokenHeader }
|
||||
};
|
||||
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* 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 ws = require("ws");
|
||||
var url = require("url");
|
||||
const crypto = require("crypto");
|
||||
|
||||
var log = require("@node-red/util").log; // TODO: separate module
|
||||
var Tokens;
|
||||
var Users;
|
||||
var Permissions;
|
||||
var Strategies;
|
||||
|
||||
var server;
|
||||
var settings;
|
||||
var runtimeAPI;
|
||||
|
||||
var wsServer;
|
||||
var activeConnections = [];
|
||||
|
||||
var anonymousUser;
|
||||
|
||||
var heartbeatTimer;
|
||||
var lastSentTime;
|
||||
|
||||
function init(_server,_settings,_runtimeAPI) {
|
||||
server = _server;
|
||||
settings = _settings;
|
||||
runtimeAPI = _runtimeAPI;
|
||||
Tokens = require("../auth/tokens");
|
||||
Tokens.onSessionExpiry(handleSessionExpiry);
|
||||
Users = require("../auth/users");
|
||||
Permissions = require("../auth/permissions");
|
||||
Strategies = require("../auth/strategies");
|
||||
|
||||
}
|
||||
function handleSessionExpiry(session) {
|
||||
activeConnections.forEach(connection => {
|
||||
if (connection.token === session.accessToken) {
|
||||
connection.ws.send(JSON.stringify({auth:"fail"}));
|
||||
connection.ws.close();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function CommsConnection(ws, user) {
|
||||
this.session = crypto.randomBytes(32).toString('base64');
|
||||
this.ws = ws;
|
||||
this.stack = [];
|
||||
this.user = user;
|
||||
this.lastSentTime = 0;
|
||||
var self = this;
|
||||
|
||||
log.audit({event: "comms.open"});
|
||||
log.trace("comms.open "+self.session);
|
||||
var preAuthed = !!user;
|
||||
var pendingAuth = !this.user && (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);
|
||||
});
|
||||
|
||||
const handleAuthPacket = function(msg) {
|
||||
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(msg, client.scope,msg.auth,true);
|
||||
} else {
|
||||
log.audit({event: "comms.auth.fail"});
|
||||
completeConnection(msg, null,null,false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Users.tokens(msg.auth).then(function(user) {
|
||||
if (user) {
|
||||
self.user = user;
|
||||
log.audit({event: "comms.auth",user:self.user});
|
||||
completeConnection(msg, user.permissions,msg.auth,true);
|
||||
} else {
|
||||
log.audit({event: "comms.auth.fail"});
|
||||
completeConnection(msg, null,null,false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
const completeConnection = function(msg, userScope, session, sendAck) {
|
||||
try {
|
||||
if (!userScope || !Permissions.hasPermission(userScope,"status.read")) {
|
||||
ws.send(JSON.stringify({auth:"fail"}));
|
||||
ws.close();
|
||||
} else {
|
||||
pendingAuth = false;
|
||||
addActiveConnection(self);
|
||||
self.token = msg.auth;
|
||||
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.
|
||||
}
|
||||
}
|
||||
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.auth) {
|
||||
handleAuthPacket(msg)
|
||||
} else if (msg.subscribe) {
|
||||
self.subscribe(msg.subscribe);
|
||||
// handleRemoteSubscription(ws,msg.subscribe);
|
||||
} else if (msg.topic) {
|
||||
runtimeAPI.comms.receive({
|
||||
user: self.user,
|
||||
client: self,
|
||||
topic: msg.topic,
|
||||
data: msg.data
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (msg.auth) {
|
||||
handleAuthPacket(msg)
|
||||
} else {
|
||||
if (anonymousUser) {
|
||||
log.audit({event: "comms.auth",user:anonymousUser});
|
||||
self.user = anonymousUser;
|
||||
completeConnection(msg, anonymousUser.permissions, null, 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(msg, null,null,false);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
ws.on('error', function(err) {
|
||||
log.warn(log._("comms.error",{message:err.toString()}));
|
||||
});
|
||||
}
|
||||
|
||||
CommsConnection.prototype.send = function(topic,data) {
|
||||
if (topic && data) {
|
||||
this.stack.push({topic:topic,data:data});
|
||||
}
|
||||
this._queueSend();
|
||||
}
|
||||
CommsConnection.prototype._queueSend = function() {
|
||||
var self = this;
|
||||
if (!this._xmitTimer) {
|
||||
this._xmitTimer = setTimeout(function() {
|
||||
try {
|
||||
self.ws.send(JSON.stringify(self.stack.splice(0,50)));
|
||||
self.lastSentTime = Date.now();
|
||||
} catch(err) {
|
||||
removeActiveConnection(self);
|
||||
log.warn(log._("comms.error-send",{message:err.toString()}));
|
||||
}
|
||||
delete self._xmitTimer;
|
||||
if (self.stack.length > 0) {
|
||||
self._queueSend();
|
||||
}
|
||||
},50);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
CommsConnection.prototype.subscribe = function(topic) {
|
||||
runtimeAPI.comms.subscribe({
|
||||
user: this.user,
|
||||
client: this,
|
||||
topic: topic
|
||||
})
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (!settings.disableEditor) {
|
||||
Users.default().then(function(_anonymousUser) {
|
||||
anonymousUser = _anonymousUser;
|
||||
var webSocketKeepAliveTime = settings.webSocketKeepAliveTime || 15000;
|
||||
var commsPath = settings.httpAdminRoot || "/";
|
||||
commsPath = (commsPath.slice(0,1) != "/" ? "/":"") + commsPath + (commsPath.slice(-1) == "/" ? "":"/") + "comms";
|
||||
wsServer = new ws.Server({ noServer: true });
|
||||
wsServer.on('connection',function(ws, request, user) {
|
||||
var commsConnection = new CommsConnection(ws, user);
|
||||
});
|
||||
wsServer.on('error', function(err) {
|
||||
log.warn(log._("comms.error-server",{message:err.toString()}));
|
||||
});
|
||||
|
||||
server.on('upgrade', function upgrade(request, socket, head) {
|
||||
const pathname = url.parse(request.url).pathname;
|
||||
if (pathname === commsPath) {
|
||||
if (Users.tokenHeader() !== null && request.headers[Users.tokenHeader()]) {
|
||||
// The user has provided custom token handling. For the websocket,
|
||||
// the token could be provided in two ways:
|
||||
// - as an http header (only possible with a reverse proxy setup)
|
||||
// - passed over the connected websock in an auth packet
|
||||
// If the header is present, verify the token. If not, use the auth
|
||||
// packet over the connected socket
|
||||
//
|
||||
Strategies.authenticateUserToken(request).then(user => {
|
||||
wsServer.handleUpgrade(request, socket, head, function done(ws) {
|
||||
wsServer.emit('connection', ws, request, user);
|
||||
});
|
||||
}).catch(err => {
|
||||
log.audit({event: "comms.auth.fail"});
|
||||
socket.destroy();
|
||||
})
|
||||
return
|
||||
}
|
||||
wsServer.handleUpgrade(request, socket, head, function done(ws) {
|
||||
wsServer.emit('connection', ws, request, null);
|
||||
});
|
||||
}
|
||||
// Don't destroy the socket as other listeners may want to handle the
|
||||
// event.
|
||||
});
|
||||
|
||||
lastSentTime = Date.now();
|
||||
|
||||
heartbeatTimer = setInterval(function() {
|
||||
var now = Date.now();
|
||||
if (now-lastSentTime > webSocketKeepAliveTime) {
|
||||
activeConnections.forEach(connection => connection.send("hb",lastSentTime));
|
||||
}
|
||||
}, webSocketKeepAliveTime);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (heartbeatTimer) {
|
||||
clearInterval(heartbeatTimer);
|
||||
heartbeatTimer = null;
|
||||
}
|
||||
if (wsServer) {
|
||||
wsServer.close();
|
||||
wsServer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function addActiveConnection(connection) {
|
||||
activeConnections.push(connection);
|
||||
runtimeAPI.comms.addConnection({client: connection});
|
||||
}
|
||||
function removeActiveConnection(connection) {
|
||||
for (var i=0;i<activeConnections.length;i++) {
|
||||
if (activeConnections[i] === connection) {
|
||||
activeConnections.splice(i,1);
|
||||
runtimeAPI.comms.removeConnection({client:connection})
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init:init,
|
||||
start:start,
|
||||
stop:stop
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 runtimeAPI;
|
||||
var apiUtils = require("../util");
|
||||
|
||||
module.exports = {
|
||||
init: function(_runtimeAPI) {
|
||||
runtimeAPI = _runtimeAPI
|
||||
},
|
||||
get: function (req, res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
type: req.params.type,
|
||||
id: req.params.id
|
||||
}
|
||||
runtimeAPI.flows.getNodeCredentials(opts).then(function(result) {
|
||||
res.json(result);
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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 path = require('path');
|
||||
|
||||
var comms = require("./comms");
|
||||
var info = require("./settings");
|
||||
|
||||
var auth = require("../auth");
|
||||
var nodes = require("../admin/nodes"); // TODO: move /icons into here
|
||||
var needsPermission;
|
||||
var runtimeAPI;
|
||||
var log = require("@node-red/util").log;
|
||||
var i18n = require("@node-red/util").i18n;
|
||||
|
||||
var apiUtil = require("../util");
|
||||
|
||||
var ensureRuntimeStarted = function(req,res,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, settings, _runtimeAPI) {
|
||||
runtimeAPI = _runtimeAPI;
|
||||
needsPermission = auth.needsPermission;
|
||||
if (!settings.disableEditor) {
|
||||
info.init(settings, runtimeAPI);
|
||||
comms.init(server,settings,runtimeAPI);
|
||||
|
||||
var ui = require("./ui");
|
||||
|
||||
ui.init(settings, runtimeAPI);
|
||||
|
||||
const editorApp = apiUtil.createExpressApp(settings)
|
||||
|
||||
if (settings.requireHttps === true) {
|
||||
editorApp.enable('trust proxy');
|
||||
editorApp.use(function (req, res, next) {
|
||||
if (req.secure) {
|
||||
next();
|
||||
} else {
|
||||
res.redirect('https://' + req.headers.host + req.originalUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
var defaultServerSettings = {
|
||||
"x-powered-by": false
|
||||
}
|
||||
var serverSettings = Object.assign({},defaultServerSettings,settings.httpServerOptions||{});
|
||||
for (var eOption in serverSettings) {
|
||||
editorApp.set(eOption, serverSettings[eOption]);
|
||||
}
|
||||
editorApp.get("/",ensureRuntimeStarted,ui.ensureSlash,ui.editor);
|
||||
|
||||
editorApp.get("/icons",needsPermission("nodes.read"),nodes.getIcons,apiUtil.errorHandler);
|
||||
editorApp.get("/icons/:module/:icon",ui.icon);
|
||||
editorApp.get("/icons/:scope/:module/:icon",ui.icon);
|
||||
|
||||
editorApp.get(/^\/resources\/((?:@[^\/]+\/)?[^\/]+)\/(.+)$/,ui.moduleResource);
|
||||
|
||||
var theme = require("./theme");
|
||||
theme.init(settings, runtimeAPI);
|
||||
editorApp.use("/theme",theme.app());
|
||||
editorApp.use("/",ui.editorResources);
|
||||
|
||||
//Projects
|
||||
var projects = require("./projects");
|
||||
projects.init(settings, runtimeAPI);
|
||||
editorApp.use("/projects",projects.app());
|
||||
|
||||
// Locales
|
||||
var locales = require("./locales");
|
||||
locales.init(runtimeAPI);
|
||||
editorApp.get(/^\/locales\/(.+)\/?$/,locales.get,apiUtil.errorHandler);
|
||||
|
||||
// Library
|
||||
var library = require("./library");
|
||||
library.init(runtimeAPI);
|
||||
// editorApp.get("/library/:id",needsPermission("library.read"),library.getLibraryConfig);
|
||||
editorApp.get(/^\/library\/([^\/]+)\/([^\/]+)(?:$|\/(.*))/,needsPermission("library.read"),library.getEntry);
|
||||
editorApp.post(/^\/library\/([^\/]+)\/([^\/]+)\/(.*)/,needsPermission("library.write"),library.saveEntry);
|
||||
|
||||
|
||||
// Credentials
|
||||
var credentials = require("./credentials");
|
||||
credentials.init(runtimeAPI);
|
||||
editorApp.get('/credentials/:type/:id', needsPermission("credentials.read"),credentials.get,apiUtil.errorHandler);
|
||||
|
||||
// Settings
|
||||
// Main /settings route is an admin route - see lib/admin/settings.js
|
||||
// User Settings
|
||||
editorApp.get("/settings/user",needsPermission("settings.read"),info.userSettings,apiUtil.errorHandler);
|
||||
// User Settings
|
||||
editorApp.post("/settings/user",needsPermission("settings.write"),info.updateUserSettings,apiUtil.errorHandler);
|
||||
// SSH keys
|
||||
editorApp.use("/settings/user/keys",needsPermission("settings.write"),info.sshkeys());
|
||||
|
||||
return editorApp;
|
||||
}
|
||||
},
|
||||
start: function() {
|
||||
var catalogPath = path.resolve(path.join(path.dirname(require.resolve("@node-red/editor-client")),"locales"));
|
||||
return i18n.registerMessageCatalogs([
|
||||
{namespace: "editor", dir: catalogPath, file:"editor.json"},
|
||||
{namespace: "jsonata", dir: catalogPath, file:"jsonata.json"},
|
||||
{namespace: "infotips", dir: catalogPath, file:"infotips.json"}
|
||||
]).then(function(){
|
||||
comms.start();
|
||||
});
|
||||
},
|
||||
stop: comms.stop
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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 apiUtils = require("../util");
|
||||
|
||||
var runtimeAPI;
|
||||
|
||||
module.exports = {
|
||||
init: function(_runtimeAPI) {
|
||||
runtimeAPI = _runtimeAPI;
|
||||
},
|
||||
// getLibraryConfig: function(req,res) {
|
||||
// var opts = {
|
||||
// user: req.user,
|
||||
// library: req.params.id
|
||||
// }
|
||||
// runtimeAPI.library.getConfig(opts).then(function(result) {
|
||||
// res.json(result);
|
||||
// }).catch(function(err) {
|
||||
// apiUtils.rejectHandler(req,res,err);
|
||||
// });
|
||||
// },
|
||||
getEntry: function(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
library: req.params[0],
|
||||
type: req.params[1],
|
||||
path: req.params[2]||""
|
||||
}
|
||||
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);
|
||||
});
|
||||
},
|
||||
saveEntry: function(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
library: req.params[0],
|
||||
type: req.params[1],
|
||||
path: req.params[2]||""
|
||||
}
|
||||
// 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) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 i18n = require("@node-red/util").i18n; // TODO: separate module
|
||||
|
||||
var runtimeAPI;
|
||||
|
||||
function loadResource(lang, namespace) {
|
||||
var catalog = i18n.i.getResourceBundle(lang, namespace);
|
||||
if (!catalog) {
|
||||
var parts = lang.split("-");
|
||||
if (parts.length == 2) {
|
||||
var new_lang = parts[0];
|
||||
return i18n.i.getResourceBundle(new_lang, namespace);
|
||||
}
|
||||
}
|
||||
return catalog;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: function(_runtimeAPI) {
|
||||
runtimeAPI = _runtimeAPI;
|
||||
},
|
||||
get: function(req,res) {
|
||||
var namespace = req.params[0];
|
||||
namespace = namespace.replace(/\.json$/,"");
|
||||
var lang = req.query.lng || i18n.defaultLang; //apiUtil.determineLangFromHeaders(req.acceptsLanguages() || []);
|
||||
if (/[^0-9a-z=\-\*]/i.test(lang)) {
|
||||
res.json({});
|
||||
return;
|
||||
}
|
||||
var prevLang = i18n.i.language;
|
||||
// Trigger a load from disk of the language if it is not the default
|
||||
i18n.i.changeLanguage(lang, function(){
|
||||
i18n.i.changeLanguage(prevLang, function() {
|
||||
var catalog = loadResource(lang, namespace);
|
||||
res.json(catalog||{});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,544 @@
|
||||
/**
|
||||
* 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 apiUtils = require("../util");
|
||||
|
||||
var settings;
|
||||
var runtimeAPI;
|
||||
var needsPermission = require("../auth").needsPermission;
|
||||
|
||||
function listProjects(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.projects.listProjects(opts).then(function(result) {
|
||||
res.json(result);
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
});
|
||||
}
|
||||
function getProject(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
id: req.params.id,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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);
|
||||
})
|
||||
}
|
||||
function getProjectStatus(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
id: req.params.id,
|
||||
remote: req.query.remote,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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);
|
||||
})
|
||||
}
|
||||
function getProjectRemotes(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
id: req.params.id,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.projects.getRemotes(opts).then(function(data) {
|
||||
res.json(data);
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
}
|
||||
module.exports = {
|
||||
init: function(_settings, _runtimeAPI) {
|
||||
settings = _settings;
|
||||
runtimeAPI = _runtimeAPI;
|
||||
},
|
||||
app: function() {
|
||||
var app = apiUtils.createExpressApp(settings)
|
||||
|
||||
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"),listProjects);
|
||||
|
||||
// Create project
|
||||
app.post("/", needsPermission("projects.write"), function(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
project: req.body,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
|
||||
if (req.body.active) {
|
||||
opts.clearContext = req.body.hasOwnProperty('clearContext')?req.body.clearContext:true
|
||||
runtimeAPI.projects.setActiveProject(opts).then(function() {
|
||||
listProjects(req,res);
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
} else if (req.body.initialise) {
|
||||
runtimeAPI.projects.initialiseProject(opts).then(function() {
|
||||
getProject(req,res);
|
||||
}).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('version') ||
|
||||
req.body.hasOwnProperty('files') ||
|
||||
req.body.hasOwnProperty('git')) {
|
||||
runtimeAPI.projects.updateProject(opts).then(function() {
|
||||
getProject(req,res);
|
||||
}).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"), getProject);
|
||||
|
||||
// Delete project
|
||||
app.delete("/:id", needsPermission("projects.write"), function(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
id: req.params.id,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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"), getProjectStatus);
|
||||
|
||||
|
||||
// Project file listing
|
||||
app.get("/:id/files", needsPermission("projects.read"), function(req,res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
id: req.params.id,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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],
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
|
||||
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],
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.projects.stageFile(opts).then(function() {
|
||||
getProjectStatus(req,res);
|
||||
}).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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.projects.stageFile(opts).then(function() {
|
||||
getProjectStatus(req,res);
|
||||
}).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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.projects.commit(opts).then(function() {
|
||||
getProjectStatus(req,res);
|
||||
}).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],
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.projects.unstageFile(opts).then(function() {
|
||||
getProjectStatus(req,res);
|
||||
}).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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.projects.unstageFile(opts).then(function() {
|
||||
getProjectStatus(req,res);
|
||||
}).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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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],
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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"), getProjectRemotes);
|
||||
|
||||
// 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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
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) {
|
||||
getProjectRemotes(req,res);
|
||||
}).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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.projects.removeRemote(opts).then(function(data) {
|
||||
getProjectRemotes(req,res);
|
||||
}).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,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.projects.updateRemote(opts).then(function() {
|
||||
res.status(204).end();
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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 apiUtils = require("../util");
|
||||
var runtimeAPI;
|
||||
var sshkeys = require("./sshkeys");
|
||||
|
||||
module.exports = {
|
||||
init: function(settings, _runtimeAPI) {
|
||||
runtimeAPI = _runtimeAPI;
|
||||
sshkeys.init(settings, runtimeAPI);
|
||||
},
|
||||
userSettings: function(req, res) {
|
||||
var opts = {
|
||||
user: req.user
|
||||
}
|
||||
runtimeAPI.settings.getUserSettings(opts).then(function(result) {
|
||||
res.json(result);
|
||||
});
|
||||
},
|
||||
updateUserSettings: function(req,res) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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 apiUtils = require("../util");
|
||||
var runtimeAPI;
|
||||
var settings;
|
||||
|
||||
module.exports = {
|
||||
init: function(_settings, _runtimeAPI) {
|
||||
runtimeAPI = _runtimeAPI;
|
||||
settings = _settings;
|
||||
},
|
||||
app: function() {
|
||||
const app = apiUtils.createExpressApp(settings);
|
||||
|
||||
// List all SSH keys
|
||||
app.get("/", function(req,res) {
|
||||
var opts = {
|
||||
user: req.user
|
||||
}
|
||||
runtimeAPI.settings.getUserKeys(opts).then(function(list) {
|
||||
res.json({
|
||||
keys: list
|
||||
});
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
});
|
||||
});
|
||||
|
||||
// Get SSH key detail
|
||||
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("/", 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
|
||||
});
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
});
|
||||
});
|
||||
|
||||
// Delete a SSH key
|
||||
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) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* 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 util = require("util");
|
||||
var path = require("path");
|
||||
var fs = require("fs");
|
||||
var clone = require("clone");
|
||||
const apiUtil = require("../util")
|
||||
|
||||
var defaultContext = {
|
||||
page: {
|
||||
title: "Node-RED",
|
||||
favicon: "favicon.ico",
|
||||
tabicon: {
|
||||
icon: "red/images/node-red-icon-black.svg",
|
||||
colour: "#8f0000"
|
||||
}
|
||||
},
|
||||
header: {
|
||||
title: "Node-RED",
|
||||
image: "red/images/node-red.svg"
|
||||
},
|
||||
asset: {
|
||||
red: "red/red.min.js",
|
||||
main: "red/main.min.js",
|
||||
vendorMonaco: ""
|
||||
}
|
||||
};
|
||||
var settings;
|
||||
|
||||
var theme = null;
|
||||
var themeContext = clone(defaultContext);
|
||||
var themeSettings = null;
|
||||
|
||||
var activeTheme = null;
|
||||
var activeThemeInitialised = false;
|
||||
|
||||
var runtimeAPI;
|
||||
var themeApp;
|
||||
|
||||
function serveFile(app,baseUrl,file) {
|
||||
try {
|
||||
var stats = fs.statSync(file);
|
||||
var url = baseUrl+path.basename(file);
|
||||
//console.log(url,"->",file);
|
||||
app.get(url,function(req, res) {
|
||||
res.sendFile(file);
|
||||
});
|
||||
return "theme"+url;
|
||||
} catch(err) {
|
||||
//TODO: log filenotfound
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function serveFilesFromTheme(themeValue, themeApp, directory, baseDirectory) {
|
||||
var result = [];
|
||||
if (themeValue) {
|
||||
var array = themeValue;
|
||||
if (!Array.isArray(array)) {
|
||||
array = [array];
|
||||
}
|
||||
|
||||
for (var i=0;i<array.length;i++) {
|
||||
let fullPath = array[i];
|
||||
if (baseDirectory) {
|
||||
fullPath = path.resolve(baseDirectory,array[i]);
|
||||
if (fullPath.indexOf(path.resolve(baseDirectory)) !== 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
var url = serveFile(themeApp,directory,fullPath);
|
||||
if (url) {
|
||||
result.push(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: function(_settings, _runtimeAPI) {
|
||||
settings = _settings;
|
||||
runtimeAPI = _runtimeAPI;
|
||||
themeContext = clone(defaultContext);
|
||||
if (process.env.NODE_ENV == "development") {
|
||||
themeContext.asset.red = "red/red.js";
|
||||
themeContext.asset.main = "red/main.js";
|
||||
}
|
||||
themeSettings = null;
|
||||
theme = settings.editorTheme || {};
|
||||
themeContext.asset.vendorMonaco = "vendor/monaco/monaco-bootstrap.js"
|
||||
if (theme.codeEditor && theme.codeEditor.lib === 'ace') {
|
||||
themeContext.asset.vendorMonaco = ''
|
||||
}
|
||||
activeTheme = theme.theme;
|
||||
},
|
||||
|
||||
app: function() {
|
||||
var i;
|
||||
var url;
|
||||
themeSettings = {};
|
||||
|
||||
themeApp = apiUtil.createExpressApp(settings);
|
||||
|
||||
const defaultServerSettings = {
|
||||
"x-powered-by": false
|
||||
}
|
||||
const serverSettings = Object.assign({},defaultServerSettings,settings.httpServerOptions||{});
|
||||
for (const eOption in serverSettings) {
|
||||
themeApp.set(eOption, serverSettings[eOption]);
|
||||
}
|
||||
|
||||
if (theme.page) {
|
||||
|
||||
themeContext.page.css = serveFilesFromTheme(
|
||||
theme.page.css,
|
||||
themeApp,
|
||||
"/css/")
|
||||
themeContext.page.scripts = serveFilesFromTheme(
|
||||
theme.page.scripts,
|
||||
themeApp,
|
||||
"/scripts/")
|
||||
|
||||
if (theme.page.favicon) {
|
||||
url = serveFile(themeApp,"/favicon/",theme.page.favicon)
|
||||
if (url) {
|
||||
themeContext.page.favicon = url;
|
||||
}
|
||||
}
|
||||
|
||||
if (theme.page.tabicon) {
|
||||
let icon = theme.page.tabicon.icon || theme.page.tabicon
|
||||
url = serveFile(themeApp,"/tabicon/", icon)
|
||||
if (url) {
|
||||
themeContext.page.tabicon.icon = url;
|
||||
}
|
||||
if (theme.page.tabicon.colour) {
|
||||
themeContext.page.tabicon.colour = theme.page.tabicon.colour
|
||||
}
|
||||
}
|
||||
|
||||
themeContext.page.title = theme.page.title || themeContext.page.title;
|
||||
|
||||
// Store the resolved urls to these resources so nodes (such as Debug)
|
||||
// can access them
|
||||
theme.page._ = {
|
||||
css: themeContext.page.css,
|
||||
scripts: themeContext.page.scripts,
|
||||
favicon: themeContext.page.favicon
|
||||
}
|
||||
}
|
||||
|
||||
if (theme.header) {
|
||||
|
||||
themeContext.header.title = theme.header.title || themeContext.header.title;
|
||||
|
||||
if (theme.header.hasOwnProperty("url")) {
|
||||
themeContext.header.url = theme.header.url;
|
||||
}
|
||||
|
||||
if (theme.header.hasOwnProperty("image")) {
|
||||
if (theme.header.image) {
|
||||
url = serveFile(themeApp,"/header/",theme.header.image);
|
||||
if (url) {
|
||||
themeContext.header.image = url;
|
||||
}
|
||||
} else {
|
||||
themeContext.header.image = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (theme.deployButton) {
|
||||
themeSettings.deployButton = {};
|
||||
if (theme.deployButton.label) {
|
||||
themeSettings.deployButton.label = theme.deployButton.label;
|
||||
}
|
||||
if (theme.deployButton.type == "simple") {
|
||||
themeSettings.deployButton.type = theme.deployButton.type;
|
||||
if (theme.deployButton.icon) {
|
||||
url = serveFile(themeApp,"/deploy/",theme.deployButton.icon);
|
||||
if (url) {
|
||||
themeSettings.deployButton.icon = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (theme.hasOwnProperty("userMenu")) {
|
||||
themeSettings.userMenu = theme.userMenu;
|
||||
}
|
||||
|
||||
if (theme.login) {
|
||||
let themeContextLogin = {}
|
||||
let hasLoginTheme = false
|
||||
if (theme.login.image) {
|
||||
url = serveFile(themeApp,"/login/",theme.login.image);
|
||||
if (url) {
|
||||
themeContextLogin.image = url
|
||||
hasLoginTheme = true
|
||||
}
|
||||
}
|
||||
if (theme.login.message) {
|
||||
themeContextLogin.message = theme.login.message
|
||||
hasLoginTheme = true
|
||||
}
|
||||
if (theme.login.button) {
|
||||
themeContextLogin.button = theme.login.button
|
||||
hasLoginTheme = true
|
||||
}
|
||||
if (hasLoginTheme) {
|
||||
themeContext.login = themeContextLogin
|
||||
}
|
||||
}
|
||||
themeApp.get("/", async function(req,res) {
|
||||
const themePluginList = await runtimeAPI.plugins.getPluginsByType({type:"node-red-theme"});
|
||||
themeContext.themes = themePluginList.map(theme => theme.id);
|
||||
res.json(themeContext);
|
||||
})
|
||||
|
||||
if (theme.hasOwnProperty("menu")) {
|
||||
themeSettings.menu = theme.menu;
|
||||
}
|
||||
|
||||
if (theme.hasOwnProperty("palette")) {
|
||||
themeSettings.palette = theme.palette;
|
||||
}
|
||||
|
||||
if (theme.hasOwnProperty("projects")) {
|
||||
themeSettings.projects = theme.projects;
|
||||
}
|
||||
|
||||
if (theme.hasOwnProperty("multiplayer")) {
|
||||
themeSettings.multiplayer = theme.multiplayer;
|
||||
}
|
||||
|
||||
if (theme.hasOwnProperty("keymap")) {
|
||||
themeSettings.keymap = theme.keymap;
|
||||
}
|
||||
|
||||
if (theme.theme) {
|
||||
themeSettings.theme = theme.theme;
|
||||
}
|
||||
|
||||
if (theme.hasOwnProperty("tours")) {
|
||||
themeSettings.tours = theme.tours;
|
||||
}
|
||||
|
||||
return themeApp;
|
||||
},
|
||||
context: async function() {
|
||||
if (activeTheme && !activeThemeInitialised) {
|
||||
const themePlugin = await runtimeAPI.plugins.getPlugin({
|
||||
id:activeTheme
|
||||
});
|
||||
if (themePlugin) {
|
||||
if (themePlugin.css) {
|
||||
const cssFiles = serveFilesFromTheme(
|
||||
themePlugin.css,
|
||||
themeApp,
|
||||
"/css/",
|
||||
themePlugin.path
|
||||
);
|
||||
themeContext.page.css = cssFiles.concat(themeContext.page.css || [])
|
||||
theme.page = theme.page || {_:{}}
|
||||
theme.page._.css = cssFiles.concat(theme.page._.css || [])
|
||||
}
|
||||
if (themePlugin.scripts) {
|
||||
const scriptFiles = serveFilesFromTheme(
|
||||
themePlugin.scripts,
|
||||
themeApp,
|
||||
"/scripts/",
|
||||
themePlugin.path
|
||||
)
|
||||
themeContext.page.scripts = scriptFiles.concat(themeContext.page.scripts || [])
|
||||
theme.page = theme.page || {_:{}}
|
||||
theme.page._.scripts = scriptFiles.concat(theme.page._.scripts || [])
|
||||
}
|
||||
// check and load page settings from theme
|
||||
if (themePlugin.page) {
|
||||
if (themePlugin.page.favicon && !theme.page.favicon) {
|
||||
const result = serveFilesFromTheme(
|
||||
[themePlugin.page.favicon],
|
||||
themeApp,
|
||||
"/",
|
||||
themePlugin.path
|
||||
)
|
||||
if(result && result.length > 0) {
|
||||
// update themeContext page favicon
|
||||
themeContext.page.favicon = result[0]
|
||||
theme.page = theme.page || {_:{}}
|
||||
theme.page._.favicon = result[0]
|
||||
}
|
||||
}
|
||||
if (themePlugin.page.tabicon && themePlugin.page.tabicon.icon && !theme.page.tabicon) {
|
||||
const result = serveFilesFromTheme(
|
||||
[themePlugin.page.tabicon.icon],
|
||||
themeApp,
|
||||
"/page/",
|
||||
themePlugin.path
|
||||
)
|
||||
if(result && result.length > 0) {
|
||||
// update themeContext page tabicon
|
||||
themeContext.page.tabicon.icon = result[0]
|
||||
themeContext.page.tabicon.colour = themeContext.page.tabicon.colour || themeContext.page.tabicon.colour
|
||||
theme.page = theme.page || {_:{}}
|
||||
theme.page._.tabicon = theme.page._.tabicon || {}
|
||||
theme.page._.tabicon.icon = themeContext.page.tabicon.icon
|
||||
theme.page._.tabicon.colour = themeContext.page.tabicon.colour
|
||||
}
|
||||
}
|
||||
// if the plugin has a title AND the users settings.js does NOT
|
||||
if (themePlugin.page.title && !theme.page.title) {
|
||||
themeContext.page.title = themePlugin.page.title || themeContext.page.title
|
||||
}
|
||||
}
|
||||
// check and load header settings from theme
|
||||
if (themePlugin.header) {
|
||||
if (themePlugin.header.image && !theme.header.image) {
|
||||
const result = serveFilesFromTheme(
|
||||
[themePlugin.header.image],
|
||||
themeApp,
|
||||
"/header/",
|
||||
themePlugin.path
|
||||
)
|
||||
if(result && result.length > 0) {
|
||||
// update themeContext header image
|
||||
themeContext.header.image = result[0]
|
||||
}
|
||||
}
|
||||
// if the plugin has a title AND the users settings.js does NOT have a title
|
||||
if (themePlugin.header.title && !theme.header.title) {
|
||||
themeContext.header.title = themePlugin.header.title || themeContext.header.title
|
||||
}
|
||||
// if the plugin has a header url AND the users settings.js does NOT
|
||||
if (themePlugin.header.url && !theme.header.url) {
|
||||
themeContext.header.url = themePlugin.header.url || themeContext.header.url
|
||||
}
|
||||
}
|
||||
theme.codeEditor = theme.codeEditor || {}
|
||||
theme.codeEditor.options = Object.assign({}, themePlugin.monacoOptions, theme.codeEditor.options);
|
||||
|
||||
theme.mermaid = Object.assign({}, themePlugin.mermaid, theme.mermaid)
|
||||
}
|
||||
activeThemeInitialised = true;
|
||||
}
|
||||
return themeContext;
|
||||
},
|
||||
settings: function() {
|
||||
return themeSettings;
|
||||
},
|
||||
serveFile: function(baseUrl,file) {
|
||||
return serveFile(themeApp,baseUrl,file);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 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.
|
||||
**/
|
||||
const crypto = require('crypto')
|
||||
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 runtimeAPI;
|
||||
let settings;
|
||||
var editorClientDir = path.dirname(require.resolve("@node-red/editor-client"));
|
||||
var defaultNodeIcon = path.join(editorClientDir,"public","red","images","icons","arrow-in.svg");
|
||||
var editorTemplatePath = path.join(editorClientDir,"templates","index.mst");
|
||||
var editorTemplate;
|
||||
let cacheBuster
|
||||
|
||||
module.exports = {
|
||||
init: function(_settings, _runtimeAPI) {
|
||||
settings = _settings;
|
||||
runtimeAPI = _runtimeAPI;
|
||||
editorTemplate = fs.readFileSync(editorTemplatePath,"utf8");
|
||||
Mustache.parse(editorTemplate);
|
||||
},
|
||||
|
||||
ensureSlash: function(req,res,next) {
|
||||
var parts = req.originalUrl.split("?");
|
||||
if (parts[0].slice(-1) != "/") {
|
||||
parts[0] += "/";
|
||||
var redirect = parts.join("?");
|
||||
res.redirect(301,redirect);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
icon: function(req,res) {
|
||||
var icon = req.params.icon;
|
||||
var scope = req.params.scope;
|
||||
var module = scope ? scope + '/' + req.params.module : req.params.module;
|
||||
var opts = {
|
||||
user: req.user,
|
||||
module: module,
|
||||
icon: icon
|
||||
}
|
||||
runtimeAPI.nodes.getIcon(opts).then(function(data) {
|
||||
if (data) {
|
||||
var contentType = mime.getType(icon);
|
||||
res.set("Content-Type", contentType);
|
||||
res.send(data);
|
||||
} else {
|
||||
res.sendFile(defaultNodeIcon);
|
||||
}
|
||||
}).catch(function(err) {
|
||||
console.log(err.stack);
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
},
|
||||
|
||||
moduleResource: function(req, res) {
|
||||
let resourcePath = req.params[1];
|
||||
let opts = {
|
||||
user: req.user,
|
||||
module: req.params[0],
|
||||
path: resourcePath
|
||||
}
|
||||
runtimeAPI.nodes.getModuleResource(opts).then(function(data) {
|
||||
if (data) {
|
||||
var contentType = mime.getType(resourcePath);
|
||||
res.set("Content-Type", contentType);
|
||||
res.send(data);
|
||||
} else {
|
||||
res.status(404).end()
|
||||
}
|
||||
}).catch(function(err) {
|
||||
console.log(err.stack);
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
},
|
||||
|
||||
editor: async function(req,res) {
|
||||
if (!cacheBuster) {
|
||||
// settings.instanceId is set asynchronously to the editor-api
|
||||
// being initiaised. So we defer calculating the cacheBuster hash
|
||||
// until the first load of the editor
|
||||
cacheBuster = crypto.createHash('sha1').update(`${settings.version || 'version'}-${settings.instanceId || 'instanceId'}`).digest("hex").substring(0,12)
|
||||
}
|
||||
|
||||
let sessionMessages;
|
||||
if (req.session && req.session.messages) {
|
||||
sessionMessages = JSON.stringify(req.session.messages);
|
||||
delete req.session.messages
|
||||
}
|
||||
res.send(Mustache.render(editorTemplate,{
|
||||
sessionMessages,
|
||||
cacheBuster,
|
||||
...await theme.context()
|
||||
}));
|
||||
},
|
||||
editorResources: express.static(path.join(editorClientDir,'public'))
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 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.
|
||||
**/
|
||||
|
||||
|
||||
/**
|
||||
* This module provides an Express application to serve the Node-RED editor.
|
||||
*
|
||||
* It implements the Node-RED HTTP Admin API the Editor uses to interact
|
||||
* with the Node-RED runtime.
|
||||
*
|
||||
* @namespace @node-red/editor-api
|
||||
*/
|
||||
|
||||
var bodyParser = require("body-parser");
|
||||
var passport = require('passport');
|
||||
|
||||
var auth = require("./auth");
|
||||
var apiUtil = require("./util");
|
||||
|
||||
var adminApp;
|
||||
var server;
|
||||
var editor;
|
||||
|
||||
/**
|
||||
* Initialise the module.
|
||||
* @param {Object} settings The runtime settings
|
||||
* @param {HTTPServer} _server An instance of HTTP Server
|
||||
* @param {Storage} storage An instance of Node-RED Storage
|
||||
* @param {Runtime} runtimeAPI An instance of Node-RED Runtime
|
||||
* @memberof @node-red/editor-api
|
||||
*/
|
||||
function init(settings,_server,storage,runtimeAPI) {
|
||||
server = _server;
|
||||
if (settings.httpAdminRoot !== false) {
|
||||
adminApp = apiUtil.createExpressApp(settings);
|
||||
|
||||
var cors = require('cors');
|
||||
var corsHandler = cors({
|
||||
origin: "*",
|
||||
methods: "GET,PUT,POST,DELETE"
|
||||
});
|
||||
adminApp.use(corsHandler);
|
||||
|
||||
if (settings.httpAdminMiddleware) {
|
||||
if (typeof settings.httpAdminMiddleware === "function" || Array.isArray(settings.httpAdminMiddleware)) {
|
||||
adminApp.use(settings.httpAdminMiddleware);
|
||||
}
|
||||
}
|
||||
|
||||
auth.init(settings,storage);
|
||||
|
||||
var maxApiRequestSize = settings.apiMaxLength || '5mb';
|
||||
adminApp.use(bodyParser.json({limit:maxApiRequestSize}));
|
||||
adminApp.use(bodyParser.urlencoded({limit:maxApiRequestSize,extended:true}));
|
||||
|
||||
adminApp.get("/auth/login",auth.login,apiUtil.errorHandler);
|
||||
if (settings.adminAuth) {
|
||||
if (settings.adminAuth.type === "strategy") {
|
||||
auth.genericStrategy(adminApp,settings.adminAuth.strategy);
|
||||
} else if (settings.adminAuth.type === "credentials") {
|
||||
adminApp.use(passport.initialize());
|
||||
adminApp.post("/auth/token",
|
||||
auth.ensureClientSecret,
|
||||
auth.authenticateClient,
|
||||
auth.getToken,
|
||||
auth.errorHandler
|
||||
);
|
||||
} else if (settings.adminAuth.tokens) {
|
||||
adminApp.use(passport.initialize());
|
||||
}
|
||||
adminApp.post("/auth/revoke",auth.needsPermission(""),auth.revoke,apiUtil.errorHandler);
|
||||
}
|
||||
|
||||
// Editor
|
||||
if (!settings.disableEditor) {
|
||||
editor = require("./editor");
|
||||
var editorApp = editor.init(server, settings, runtimeAPI);
|
||||
adminApp.use(editorApp);
|
||||
}
|
||||
|
||||
if (settings.httpAdminCors) {
|
||||
var corsHandler = cors(settings.httpAdminCors);
|
||||
adminApp.use(corsHandler);
|
||||
}
|
||||
|
||||
var adminApiApp = require("./admin").init(settings, runtimeAPI);
|
||||
adminApp.use(adminApiApp);
|
||||
} else {
|
||||
adminApp = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the module.
|
||||
* @return {Promise} resolves when the application is ready to handle requests
|
||||
* @memberof @node-red/editor-api
|
||||
*/
|
||||
async function start() {
|
||||
if (editor) {
|
||||
return editor.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the module.
|
||||
* @return {Promise} resolves when the application is stopped
|
||||
* @memberof @node-red/editor-api
|
||||
*/
|
||||
async function stop() {
|
||||
if (editor) {
|
||||
editor.stop();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
start,
|
||||
stop,
|
||||
|
||||
/**
|
||||
* @memberof @node-red/editor-api
|
||||
* @mixes @node-red/editor-api_auth
|
||||
*/
|
||||
auth: {
|
||||
needsPermission: auth.needsPermission
|
||||
},
|
||||
/**
|
||||
* The Express app used to serve the Node-RED Editor
|
||||
* @type ExpressApplication
|
||||
* @memberof @node-red/editor-api
|
||||
*/
|
||||
get httpAdmin() { return adminApp; }
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 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.
|
||||
**/
|
||||
|
||||
const express = require("express");
|
||||
|
||||
const { log, i18n } = require("@node-red/util");
|
||||
|
||||
module.exports = {
|
||||
errorHandler: function(err,req,res,next) {
|
||||
//TODO: why this when rejectHandler also?!
|
||||
|
||||
if (err.message === "request entity too large") {
|
||||
log.error(err);
|
||||
} else {
|
||||
log.error(err.stack);
|
||||
}
|
||||
log.audit({event: "api.error",error:err.code||"unexpected_error",message:err.toString()},req);
|
||||
res.status(400).json({error:"unexpected_error", message:err.toString()});
|
||||
},
|
||||
|
||||
determineLangFromHeaders: function(acceptedLanguages){
|
||||
var lang = i18n.defaultLang;
|
||||
acceptedLanguages = acceptedLanguages || [];
|
||||
if (acceptedLanguages.length >= 1) {
|
||||
lang = acceptedLanguages[0];
|
||||
}
|
||||
return lang;
|
||||
},
|
||||
rejectHandler: function(req,res,err) {
|
||||
//TODO: why this when errorHandler also?!
|
||||
log.audit({event: "api.error",error:err.code||"unexpected_error",message:err.message||err.toString()},req);
|
||||
if (!err.code) {
|
||||
// by definition, an unexpected_error to log
|
||||
log.error(err);
|
||||
}
|
||||
var response = {
|
||||
code: err.code||"unexpected_error",
|
||||
message: err.message||err.toString()
|
||||
};
|
||||
// Handle auth failures on a specific remote
|
||||
// TODO: don't hardcode this here - allow users of rejectHandler to identify extra props to send
|
||||
if (err.remote) {
|
||||
response.remote = err.remote;
|
||||
}
|
||||
res.status(err.status||400).json(response);
|
||||
},
|
||||
getRequestLogObject: function(req) {
|
||||
return {
|
||||
user: req.user,
|
||||
path: req.path,
|
||||
ip: (req.headers && req.headers['x-forwarded-for']) || (req.connection && req.connection.remoteAddress) || undefined
|
||||
}
|
||||
},
|
||||
createExpressApp: function(settings) {
|
||||
const app = express();
|
||||
|
||||
const defaultServerSettings = {
|
||||
"x-powered-by": false
|
||||
}
|
||||
const serverSettings = Object.assign({},defaultServerSettings,settings.httpServerOptions||{});
|
||||
for (let eOption in serverSettings) {
|
||||
app.set(eOption, serverSettings[eOption]);
|
||||
}
|
||||
return app
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@node-red/editor-api",
|
||||
"version": "4.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/node-red/node-red.git"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Nick O'Leary"
|
||||
},
|
||||
{
|
||||
"name": "Dave Conway-Jones"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@node-red/util": "4.1.0",
|
||||
"@node-red/editor-client": "4.1.0",
|
||||
"bcryptjs": "3.0.2",
|
||||
"body-parser": "1.20.3",
|
||||
"clone": "2.1.2",
|
||||
"cors": "2.8.5",
|
||||
"express-session": "1.18.2",
|
||||
"express": "4.21.2",
|
||||
"memorystore": "1.6.7",
|
||||
"mime": "3.0.0",
|
||||
"multer": "2.0.2",
|
||||
"mustache": "4.2.0",
|
||||
"oauth2orize": "1.12.0",
|
||||
"passport-http-bearer": "1.0.1",
|
||||
"passport-oauth2-client-password": "0.1.2",
|
||||
"passport": "0.7.0",
|
||||
"ws": "7.5.10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@node-rs/bcrypt": "1.10.7"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user