├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── defaults └── README.md ├── nodes └── README.md ├── package.json ├── pgstorage.js ├── pgutil.js ├── public ├── css │ ├── simplegrid.css │ └── style.css ├── images │ ├── ets-globe.png │ ├── node-red-title-flow.png │ ├── node-red.png │ ├── nr-image-1.png │ └── tab.png └── index.html └── settings.js /.gitignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .DS_Store 3 | .bash_history 4 | .bash_logout 5 | .bashrc 6 | .cache 7 | .forever 8 | .node-gyp 9 | node_modules 10 | .npm 11 | .profile 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node --max-old-space-size=384 node_modules/node-red/red.js --settings ./settings.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-red-heroku 2 | ================ 3 | 4 | A wrapper for deploying [Node-RED](http://nodered.org) into the [Heroku](https://www.heroku.com). 5 | 6 | ### Deploying Node-RED into Heroku 7 | 8 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/joeartsea/node-red-heroku) 9 | 10 | ### Password protect the flow editor 11 | 12 | By default, the editor is open for anyone to access and modify flows. To password-protect the editor: 13 | 14 | Add the following user-defined variables. 15 | 16 | * NODE_RED_USERNAME - the username to secure the editor with 17 | * NODE_RED_PASSWORD - the password to secure the editor with 18 | 19 | ### Attention 20 | 21 | Since heroku and heroku-postgresql are no longer available for free, a paid environment is required to use this application 22 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Node-RED", 3 | "description": "A visual tool for wiring the Internet of Things.", 4 | "keywords": [ "Node-RED", "IoT", "Internet of Things", "node.js", "node" ], 5 | "website": "http://nodered.org/", 6 | "repository": "https://github.com/joeartsea/node-red-heroku.git", 7 | "logo": "http://nodered.org/node-red.png", 8 | "success_url": "/", 9 | "env": { 10 | "NODE_RED_USERNAME": { 11 | "description": "The username to secure the editor.", 12 | "value": "admin" 13 | }, 14 | "NODE_RED_PASSWORD": { 15 | "description": "The password to secure the editor.", 16 | "value": "password" 17 | } 18 | }, 19 | "addons": [ 20 | { 21 | "plan": "heroku-postgresql:basic", 22 | "options": { 23 | "version": "12" 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /defaults/README.md: -------------------------------------------------------------------------------- 1 | To push an instance of this boilerplate with a predefined set of flows, place 2 | the flows in a file called `flow.json` within this directory. If your flow 3 | has a corresponding credentials file, call that `flow_cred.json`. 4 | -------------------------------------------------------------------------------- /nodes/README.md: -------------------------------------------------------------------------------- 1 | To add additional nodes, either: 2 | - drop them in this directory and add their dependencies to ../package.json 3 | - add their npm package name to ../package.json 4 | 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "node-red-heroku", 3 | "version" : "0.0.2", 4 | "dependencies": { 5 | "when": "~3.x", 6 | "pg": "^8.3.0", 7 | "nano": "~10.x", 8 | "feedparser":"~0.19.2", 9 | "redis":"~0.10.1", 10 | "node-red": "~2.x" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /pgstorage.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Copyright 2014 IBM Corp. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | **/ 17 | 18 | var when = require('when') 19 | var pgutil = require('./pgutil') 20 | const e = require('express') 21 | 22 | var settings 23 | var appname 24 | 25 | function timeoutWrap(func) { 26 | return when.promise(function (resolve, reject, notify) { 27 | var promise = func().timeout(5000, 'timeout') 28 | promise.then(function (a, b, c, d) { 29 | resolve(a, b, c, d) 30 | }) 31 | promise.otherwise(function (err) { 32 | console.log('func', func) 33 | console.log('timeout err', err) 34 | console.log('TIMEOUT: ', func.name) 35 | if (err == 'timeout') { 36 | reject(err) 37 | } 38 | }) 39 | }) 40 | } 41 | 42 | function getFlows() { 43 | console.log('getFlows') 44 | return when.promise(async (resolve, reject, notify) => { 45 | try { 46 | const data = await pgutil.loadConfig(appname) 47 | if (data && data.flows) { 48 | resolve(data.flows) 49 | } else { 50 | resolve([]) 51 | } 52 | } catch (err) { 53 | reject(err) 54 | } 55 | }) 56 | } 57 | 58 | function saveFlows(flows) { 59 | console.log('saveFlows') 60 | return when.promise(async (resolve, reject, notify) => { 61 | try { 62 | let secureLink = process.env.SECURE_LINK 63 | await pgutil.saveConfig(appname, { appname, flows, secureLink }) 64 | resolve() 65 | } catch (err) { 66 | reject(err) 67 | } 68 | }) 69 | } 70 | 71 | function getCredentials() { 72 | console.log('getCredentials') 73 | return when.promise(async (resolve, reject, notify) => { 74 | try { 75 | const data = await pgutil.loadConfig(appname) 76 | if (data && data.credentials) { 77 | resolve(data.credentials) 78 | } else { 79 | resolve({}) 80 | } 81 | } catch (err) { 82 | reject(err) 83 | } 84 | }) 85 | } 86 | 87 | function saveCredentials(credentials) { 88 | console.log('saveCredentials') 89 | return when.promise(async (resolve, reject, notify) => { 90 | try { 91 | await pgutil.saveConfig(appname, { appname, credentials }) 92 | resolve() 93 | } catch (err) { 94 | reject(err) 95 | } 96 | }) 97 | } 98 | 99 | function getSettings() { 100 | console.log('getSettings') 101 | return when.promise(async (resolve, reject, notify) => { 102 | try { 103 | const data = await pgutil.loadConfig(appname) 104 | if (data && data.settings) { 105 | resolve(data.settings) 106 | } else { 107 | resolve({}) 108 | } 109 | } catch (err) { 110 | reject(err) 111 | } 112 | }) 113 | } 114 | 115 | function saveSettings(settings) { 116 | console.log('saveSettings') 117 | return when.promise(async (resolve, reject, notify) => { 118 | try { 119 | await pgutil.saveConfig(appname, { appname, settings }) 120 | resolve() 121 | } catch (err) { 122 | reject(err) 123 | } 124 | }) 125 | } 126 | 127 | function getLibraryEntry(type, path) { 128 | console.log('getLibraryEntry') 129 | return when.promise(async (resolve, reject, notify) => { 130 | try { 131 | const data = await pgutil.loadLib(appname, type, path) 132 | if (data && data.body) { 133 | resolve(data.body) 134 | } else { 135 | if (path != '' && path.substr(-1) != '/') { 136 | path = path + '/' 137 | } 138 | let list = await pgutil.loadLibList(appname, type, path) 139 | let dirs = [] 140 | let files = [] 141 | for (var i = 0; i < list.length; i++) { 142 | let d = list[i] 143 | let subpath = d.path.substr(path.length) 144 | let parts = subpath.split('/') 145 | if (parts.length == 1) { 146 | let meta = d.meta 147 | meta.fn = parts[0] 148 | files.push(meta) 149 | } else if (dirs.indexOf(parts[0]) == -1) { 150 | dirs.push(parts[0]) 151 | } 152 | } 153 | resolve(dirs.concat(files)) 154 | } 155 | } catch (err) { 156 | reject(err) 157 | } 158 | }) 159 | } 160 | 161 | function saveLibraryEntry(type, path, meta, body) { 162 | console.log('saveLibraryEntry') 163 | return when.promise(async (resolve, reject, notify) => { 164 | try { 165 | await pgutil.saveLib(appname, { 166 | appname, 167 | type, 168 | path, 169 | meta, 170 | body 171 | }) 172 | resolve() 173 | } catch (err) { 174 | reject(err) 175 | } 176 | }) 177 | } 178 | 179 | var pgstorage = { 180 | init: function (_settings) { 181 | settings = _settings 182 | appname = settings.pgAppname || require('os').hostname() 183 | return when.promise(async (resolve, reject, notify) => { 184 | try { 185 | const _pool = pgutil.initPG() 186 | resolve(_pool) 187 | } catch (err) { 188 | reject(err) 189 | } 190 | }) 191 | }, 192 | getFlows: function () { 193 | return timeoutWrap(getFlows) 194 | }, 195 | saveFlows: function (flows) { 196 | return timeoutWrap(function () { 197 | return saveFlows(flows) 198 | }) 199 | }, 200 | 201 | getCredentials: function () { 202 | return timeoutWrap(getCredentials) 203 | }, 204 | 205 | saveCredentials: function (credentials) { 206 | return timeoutWrap(function () { 207 | return saveCredentials(credentials) 208 | }) 209 | }, 210 | 211 | getSettings: function () { 212 | return timeoutWrap(getSettings) 213 | }, 214 | 215 | saveSettings: function (data) { 216 | return timeoutWrap(function () { 217 | return saveSettings(data) 218 | }) 219 | }, 220 | 221 | getLibraryEntry: function (type, path) { 222 | return timeoutWrap(function () { 223 | return getLibraryEntry(type, path) 224 | }) 225 | }, 226 | saveLibraryEntry: function (type, path, meta, body) { 227 | return timeoutWrap(function () { 228 | return saveLibraryEntry(type, path, meta, body) 229 | }) 230 | }, 231 | mapNodeTypes: function (flows, credentials) { 232 | for (let props in credentials) { 233 | for (let i = 0; i < flows.length; i++) { 234 | const item = flows[i] 235 | if (item.id === props) { 236 | credentials[props].type = item.type 237 | break 238 | } 239 | } 240 | } 241 | return credentials 242 | } 243 | } 244 | 245 | module.exports = pgstorage -------------------------------------------------------------------------------- /pgutil.js: -------------------------------------------------------------------------------- 1 | const pg = require('pg') 2 | const when = require('when') 3 | const util = require('util') 4 | 5 | let pool 6 | 7 | const initPG = () => { 8 | const pgUrl = process.env.DATABASE_URL 9 | console.log('pgUrl', pgUrl) 10 | pool = new pg.Pool({ 11 | connectionString: pgUrl, 12 | ssl: { rejectUnauthorized: false } 13 | }) 14 | return pool 15 | } 16 | 17 | const createTable = async () => { 18 | if (!pool) throw new Error('No PG instance') 19 | console.log('create pg tables') 20 | const query = ` 21 | CREATE TABLE IF NOT EXISTS "eConfigs" ( 22 | id SERIAL PRIMARY KEY, 23 | appname character varying(255) NOT NULL, 24 | flows text, 25 | credentials text, 26 | packages text, 27 | settings text, 28 | "secureLink" text 29 | ); 30 | CREATE TABLE IF NOT EXISTS "eLibs" ( 31 | id SERIAL PRIMARY KEY, 32 | appname character varying(255) NOT NULL, 33 | type text, 34 | path text, 35 | meta text, 36 | body text 37 | ); 38 | CREATE TABLE IF NOT EXISTS "ePrivateNodes" ( 39 | id SERIAL PRIMARY KEY, 40 | appname character varying(255) NOT NULL, 41 | "packageName" text, 42 | data text 43 | ); 44 | ` 45 | await doSQL(query, null) 46 | } 47 | 48 | const doSQL = async (query, values) => { 49 | let client 50 | try { 51 | if (!pool) throw new Error('No PG instance') 52 | client = await pool.connect() 53 | return await client.query(query, values) 54 | } finally { 55 | if (client) { 56 | client.release() 57 | } 58 | } 59 | } 60 | 61 | const loadConfig = async (appname) => { 62 | const query = 'SELECT * FROM "eConfigs" WHERE appname = $1' 63 | const data = await doSQL(query, [JSON.stringify(appname)]) 64 | if (data && data.rowCount > 0) { 65 | let retData = data.rows[0] 66 | for (let key in retData) { 67 | if (retData[key]) { 68 | retData[key] = JSON.parse(retData[key]) 69 | } 70 | } 71 | return retData 72 | } 73 | return null 74 | } 75 | 76 | const saveConfig = async (appname, params) => { 77 | const columns = [ 78 | 'appname', 79 | 'flows', 80 | 'credentials', 81 | 'packages', 82 | 'settings', 83 | 'secureLink', 84 | 'id' 85 | ] 86 | let data = await loadConfig(appname) 87 | let query 88 | let values 89 | if (data) { 90 | data = Object.assign(data, params) 91 | query = 92 | 'UPDATE "eConfigs" SET appname = $1, flows = $2, credentials = $3, packages = $4, settings = $5, "secureLink" = $6 WHERE id = $7 RETURNING *' 93 | values = columns.map((c) => (data[c] ? JSON.stringify(data[c]) : '')) 94 | } else { 95 | data = params 96 | query = 97 | 'INSERT INTO "eConfigs"(appname, flows, credentials, packages, settings, "secureLink") VALUES($1, $2, $3, $4, $5, $6) RETURNING *' 98 | values = columns 99 | .slice(0, 6) 100 | .map((c) => (data[c] ? JSON.stringify(data[c]) : '')) 101 | } 102 | await doSQL(query, values) 103 | } 104 | 105 | const removeConfig = async (appname) => { 106 | const query = 'DELETE FROM "eConfigs" WHERE appname = $1' 107 | await doSQL(query, [JSON.stringify(appname)]) 108 | } 109 | 110 | const loadLib = async (appname, type, path) => { 111 | const query = 112 | 'SELECT * FROM "eLibs" WHERE appname = $1 and type = $2 and path = $3' 113 | const data = await doSQL(query, [ 114 | JSON.stringify(appname), 115 | JSON.stringify(type), 116 | JSON.stringify(path) 117 | ]) 118 | if (data && data.rowCount > 0) { 119 | let retData = data.rows[0] 120 | for (let key in retData) { 121 | if (retData[key]) { 122 | retData[key] = JSON.parse(retData[key]) 123 | } 124 | } 125 | return retData 126 | } 127 | return null 128 | } 129 | 130 | const loadLibList = async (appname, type, dir) => { 131 | const query = 132 | 'SELECT * FROM "eLibs" WHERE appname = $1 and type = $2 and path LIKE $3 ORDER BY path' 133 | const data = await doSQL(query, [ 134 | JSON.stringify(appname), 135 | JSON.stringify(type), 136 | `"${dir}%` 137 | ]) 138 | let retDataList = data.rows.map((d) => { 139 | let retData = {} 140 | for (let key in d) { 141 | if (d[key]) { 142 | retData[key] = JSON.parse(d[key]) 143 | } 144 | } 145 | return retData 146 | }) 147 | return retDataList 148 | } 149 | 150 | const saveLib = async (appname, params) => { 151 | const columns = ['appname', 'type', 'path', 'meta', 'body', 'id'] 152 | let data = await loadLib(appname, params.type, params.path) 153 | let query 154 | let values 155 | if (data) { 156 | data = Object.assign(data, params) 157 | query = 158 | 'UPDATE "eLibs" SET appname = $1, type = $2, path = $3, meta = $4, body = $5 WHERE id = $6 RETURNING *' 159 | values = columns.map((c) => (data[c] ? JSON.stringify(data[c]) : '')) 160 | } else { 161 | data = params 162 | query = 163 | 'INSERT INTO "eLibs"(appname, type, path, meta, body) VALUES($1, $2, $3, $4, $5) RETURNING *' 164 | values = columns 165 | .slice(0, 5) 166 | .map((c) => (data[c] ? JSON.stringify(data[c]) : '')) 167 | } 168 | await doSQL(query, values) 169 | } 170 | 171 | const loadPrivateNodes = async (appname, packageName) => { 172 | const query = 173 | 'SELECT * FROM "ePrivateNodes" WHERE appname = $1 and "packageName" = $2' 174 | const data = await doSQL(query, [ 175 | JSON.stringify(appname), 176 | JSON.stringify(packageName) 177 | ]) 178 | if (data && data.rowCount > 0) { 179 | let retData = data.rows[0] 180 | for (let key in retData) { 181 | if (retData[key]) { 182 | retData[key] = JSON.parse(retData[key]) 183 | } 184 | } 185 | return retData 186 | } 187 | return null 188 | } 189 | 190 | const savePrivateNodes = async (appname, params) => { 191 | const columns = ['appname', 'packageName', 'data', 'id'] 192 | let data = await loadPrivateNodes(appname, params.packageName) 193 | let query 194 | let values 195 | if (data) { 196 | data = Object.assign(data, params) 197 | query = 198 | 'UPDATE "ePrivateNodes" SET appname = $1, "packageName" = $2, data = $3 WHERE id = $4 RETURNING *' 199 | values = columns.map((c) => (data[c] ? JSON.stringify(data[c]) : '')) 200 | } else { 201 | data = params 202 | query = 203 | 'INSERT INTO "ePrivateNodes"(appname, "packageName", data) VALUES($1, $2, $3) RETURNING *' 204 | values = columns 205 | .slice(0, 3) 206 | .map((c) => (data[c] ? JSON.stringify(data[c]) : '')) 207 | } 208 | await doSQL(query, values) 209 | } 210 | 211 | const removePrivateNodes = async (appname) => { 212 | const query = 'DELETE FROM "ePrivateNodes" WHERE appname = $1' 213 | await doSQL(query, [JSON.stringify(appname)]) 214 | } 215 | 216 | exports.initPG = initPG 217 | exports.createTable = createTable 218 | exports.loadConfig = loadConfig 219 | exports.saveConfig = saveConfig 220 | exports.removeConfig = removeConfig 221 | exports.loadLib = loadLib 222 | exports.loadLibList = loadLibList 223 | exports.saveLib = saveLib 224 | exports.loadPrivateNodes = loadPrivateNodes 225 | exports.savePrivateNodes = savePrivateNodes 226 | exports.removePrivateNodes = removePrivateNodes -------------------------------------------------------------------------------- /public/css/simplegrid.css: -------------------------------------------------------------------------------- 1 | /* 2 | Simple Grid 3 | Learn More - http://dallasbass.com/simple-grid-a-lightweight-responsive-css-grid/ 4 | Project Page - http://thisisdallas.github.com/Simple-Grid/ 5 | Author - Dallas Bass 6 | Site - dallasbass.com 7 | */ 8 | 9 | *, *:after, *:before { 10 | -webkit-box-sizing: border-box; 11 | -moz-box-sizing: border-box; 12 | box-sizing: border-box; 13 | } 14 | 15 | body { 16 | margin: 0px; 17 | } 18 | 19 | [class*='col-'] { 20 | float: left; 21 | padding-left: 17px; 22 | padding-right: 17px; 23 | } 24 | 25 | [class*='col-']:last-of-type { 26 | padding-right: 0px; 27 | } 28 | [class*='col-']:first-of-type { 29 | padding-left: 0px; 30 | } 31 | 32 | .grid { 33 | width: 100%; 34 | max-width: 1155px; 35 | min-width: 755px; 36 | margin: 0 auto; 37 | overflow: hidden; 38 | padding: 0px 75px 0 75px; 39 | } 40 | 41 | .grid:after { 42 | content: ""; 43 | display: table; 44 | clear: both; 45 | } 46 | 47 | 48 | /* Content Columns */ 49 | 50 | .col-1-1 { 51 | width: 100%; 52 | } 53 | .col-2-3, .col-8-12 { 54 | width: 66.66%; 55 | } 56 | 57 | .col-1-2, .col-6-12 { 58 | width: 50%; 59 | } 60 | 61 | .col-1-3, .col-4-12 { 62 | width: 33.33%; 63 | } 64 | 65 | .col-1-4, .col-3-12 { 66 | width: 25%; 67 | } 68 | 69 | .col-1-5 { 70 | width: 20%; 71 | } 72 | 73 | .col-1-6, .col-2-12 { 74 | width: 16.667%; 75 | } 76 | 77 | .col-1-7 { 78 | width: 14.28%; 79 | } 80 | 81 | .col-1-8 { 82 | width: 12.5%; 83 | } 84 | 85 | .col-1-9 { 86 | width: 11.1%; 87 | } 88 | 89 | .col-1-10 { 90 | width: 10%; 91 | } 92 | 93 | .col-1-11 { 94 | width: 9.09%; 95 | } 96 | 97 | .col-1-12 { 98 | width: 8.33% 99 | } 100 | 101 | /* Layout Columns */ 102 | 103 | .col-11-12 { 104 | width: 91.66% 105 | } 106 | 107 | .col-10-12 { 108 | width: 83.333%; 109 | } 110 | 111 | .col-9-12 { 112 | width: 75%; 113 | } 114 | 115 | .col-5-12 { 116 | width: 41.66%; 117 | } 118 | 119 | .col-7-12 { 120 | width: 58.33% 121 | } 122 | 123 | @media handheld, only screen and (max-width: 767px) { 124 | 125 | 126 | .grid { 127 | width: 100%; 128 | min-width: 0; 129 | margin-left: 0px; 130 | margin-right: 0px; 131 | padding-left: 0px; 132 | padding-right: 0px; 133 | } 134 | 135 | [class*='col-'] { 136 | width: auto; 137 | float: none; 138 | margin-left: 0px; 139 | margin-right: 0px; 140 | margin-top: 10px; 141 | margin-bottom: 10px; 142 | padding-left: 20px !important; 143 | padding-right: 20px !important; 144 | } 145 | } -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #555; 3 | } 4 | a { 5 | text-decoration: none; 6 | } 7 | .title, .row2, .nodes { 8 | background: #eee; 9 | } 10 | 11 | 12 | .blurb a { 13 | text-decoration: underline; 14 | color: #555; 15 | } 16 | a.button { 17 | text-decoration: none; 18 | display: inline-block; 19 | padding: 30px 40px; 20 | border-radius: 5px; 21 | background: #aa6767; 22 | color: #eee; 23 | } 24 | a.button:hover { 25 | background: #7F4545; 26 | } 27 | .row3, .row4, .row5 { 28 | background: #fff; 29 | } 30 | 31 | .row9 { 32 | background: #676767; 33 | } 34 | .footer { 35 | background: #333; 36 | } 37 | 38 | .footer .grid { 39 | color: #eee; 40 | padding: 20px 0; 41 | font-family: Arial; 42 | } 43 | .footer a { 44 | color: #eee; 45 | } 46 | 47 | .footer .content { 48 | height: auto; 49 | min-height: 0; 50 | margin: 20px auto; 51 | } 52 | 53 | .footer .headline { 54 | margin: 80px auto 20px auto; 55 | } 56 | .footer { 57 | text-align: center; 58 | } 59 | 60 | .ets-link { 61 | margin: 10px 0px; 62 | 63 | } 64 | .ets-globe { 65 | width: 60px; 66 | vertical-align: middle; 67 | } 68 | 69 | .content { 70 | margin: 80px 0; 71 | } 72 | 73 | .blurb { 74 | font-family: Arial; 75 | font-size: 16px; 76 | line-height: 1.6em; 77 | } 78 | .blurb p { 79 | margin-top: 0; 80 | } 81 | .blurb h3, .nodes h3 { 82 | font-family: "Roboto Slab"; 83 | font-size: 24px; 84 | font-weight: normal; 85 | margin-top: 0; 86 | margin-bottom: 0.5em; 87 | } 88 | .blurb h4 { 89 | font-family: "Roboto Slab"; 90 | font-size: 18px; 91 | font-weight: normal; 92 | margin-top: 0.8em; 93 | margin-bottom: 0.5em; 94 | } 95 | .feature { 96 | max-width: 485px; 97 | margin-left: auto; 98 | margin-right: auto; 99 | text-align: center; 100 | } 101 | 102 | .row3 .feature, .row4 .feature, .row5 .feature { 103 | border: none; 104 | } 105 | .feature img { 106 | max-width: 445px; 107 | width: 100%; 108 | } 109 | .title .content { 110 | margin: 80px 0 20px 0; 111 | height: 280px; 112 | text-align: center; 113 | } 114 | .title h1 { 115 | font-size: 36px; 116 | font-family: "Roboto Slab"; 117 | font-weight: bold; 118 | margin-bottom: 10px; 119 | color: #676767; 120 | } 121 | .title h2 { 122 | margin-top: 0px; 123 | font-size: 20px; 124 | font-family: "Roboto Slab"; 125 | font-weight: normal; 126 | color: #555; 127 | } 128 | .title img { 129 | margin: auto; 130 | max-width: 769px; 131 | width: 100%; 132 | } 133 | 134 | .nodes .content { 135 | text-align: center; 136 | height: auto; 137 | min-height: 0; 138 | margin: 40px 0 40px 0; 139 | } 140 | .nodes .grid { 141 | padding-top: 40px; 142 | padding-bottom: 40px; 143 | } 144 | 145 | .nodes h3 { 146 | text-align: center; 147 | } 148 | .nodes h4 { 149 | font-family: "Roboto Slab"; 150 | text-align: center; 151 | font-weight: normal; 152 | margin-top: 3px; 153 | height: 50px; 154 | } 155 | 156 | .header { 157 | font-family: "Roboto Slab"; 158 | font-size: 20px; 159 | line-height: 50px; 160 | color: #999; 161 | padding: 0px 10px; 162 | height: 50px; 163 | background: #333; 164 | } 165 | .header-content { 166 | width: 100%; 167 | max-width: 1155px; 168 | min-width: 755px; 169 | margin: 0 auto; 170 | } 171 | .header-content .logo { 172 | vertical-align: middle; 173 | height: 20px; 174 | } 175 | .header-content a { 176 | color: #999; 177 | } 178 | .header-content ul { 179 | float: right; 180 | list-style-type: none; 181 | margin: -0; 182 | padding: 0; 183 | background: #333; 184 | } 185 | .header-content li { 186 | display: inline-block; 187 | margin: 0; 188 | padding: 0; 189 | } 190 | .header-content li a { 191 | display: block; 192 | padding: 0 15px 0 15px; 193 | } 194 | .header-content li.current { 195 | background: url(images/tab.png) 50% bottom no-repeat; 196 | } 197 | .header-content li.current a { 198 | color: #fff; 199 | } 200 | -------------------------------------------------------------------------------- /public/images/ets-globe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeartsea/node-red-heroku/3b904b1c8b937639c09118723b77ca7fdeb8af44/public/images/ets-globe.png -------------------------------------------------------------------------------- /public/images/node-red-title-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeartsea/node-red-heroku/3b904b1c8b937639c09118723b77ca7fdeb8af44/public/images/node-red-title-flow.png -------------------------------------------------------------------------------- /public/images/node-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeartsea/node-red-heroku/3b904b1c8b937639c09118723b77ca7fdeb8af44/public/images/node-red.png -------------------------------------------------------------------------------- /public/images/nr-image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeartsea/node-red-heroku/3b904b1c8b937639c09118723b77ca7fdeb8af44/public/images/nr-image-1.png -------------------------------------------------------------------------------- /public/images/tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeartsea/node-red-heroku/3b904b1c8b937639c09118723b77ca7fdeb8af44/public/images/tab.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Node-RED in Heroku 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 | Node-RED in Heroku 22 |
23 |
24 | 25 |
26 |
27 |
28 |
29 |

Node-RED in Heroku

30 |

A visual tool for wiring the Internet of Things

31 | 32 |
33 |
34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 |
42 |

Node-RED provides a browser-based editor that makes it easy to wire together flows that can be deployed to the runtime in a single-click.

43 |

The version running here has been customised for the Heroku.

44 |

More information about Node-RED, including documentation, can be found at nodered.org.

45 |
46 |
47 | 54 |
55 |
56 | 57 | 58 |
59 |
60 |
61 |
62 |

Customising your instance of Node-RED

63 |

This template instance of Node-RED is enough to get you started creating flows.

64 |

You may want to customise it for your needs, 65 | for example replacing this introduction page with your own, adding http authentication to the flow editor or adding new nodes to 66 | the palette.

67 |

68 | 69 |

Password protect the flow editor

70 |

By default, the editor is open for anyone to access and modify flows. To password-protect the editor:

71 |

Add the following user-defined variables.

72 |
    73 |
  • NODE_RED_USERNAME - the username to secure the editor with
  • 74 |
  • NODE_RED_PASSWORD - the password to secure the editor with
  • 75 |
76 | 77 |

Adding new nodes to the palette

78 |
    79 |
  1. There is a growing collection of additional nodes that can be added to the Node-RED editor. 80 | You can search for available nodes in the Node-RED library.
  2. 81 |
  3. Edit the file package.json and add the required node package to the dependencies 82 | section. The format is: 83 |
    "node-red-node-package-name":"x.x.x"
    84 | Where x.x.x is the desired version number.
  4. 85 |
86 | 87 |

Upgrading the version of Node-RED

88 |
  1. This boilerplate is configured to grab the latest stable release of Node-RED whenever the 89 | application is pushed into Heroku.
  2. 90 |
91 | 92 |

Changing the static web content

93 |
  1. The page you are reading now is served as static content from the application. This can be replaced 94 | with whatever content you want in the public directory.
95 | 96 |

Remove static web content and serve the flow editor from the root path

97 |
  1. In the file settings.js, delete the httpStatic and httpAdminRoot entries.
98 |
99 |
100 |
101 |
102 | 103 | 111 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 IBM Corp. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var path = require("path"); 18 | var when = require("when"); 19 | var pgutil = require('./pgutil'); 20 | 21 | process.env.NODE_RED_HOME = __dirname; 22 | 23 | var settings = module.exports = { 24 | uiPort: process.env.PORT || 1880, 25 | mqttReconnectTime: 15000, 26 | serialReconnectTime: 15000, 27 | debugMaxLength: 10000000, 28 | 29 | // Blacklist the non-bluemix friendly nodes 30 | nodesExcludes:[ '66-mongodb.js','75-exec.js','35-arduino.js','36-rpi-gpio.js','25-serial.js','28-tail.js','50-file.js','31-tcpin.js','32-udp.js','23-watch.js' ], 31 | 32 | // Enable module reinstalls on start-up; this ensures modules installed 33 | // post-deploy are restored after a restage 34 | autoInstallModules: true, 35 | 36 | // Move the admin UI 37 | httpAdminRoot: '/red', 38 | 39 | // You can protect the user interface with a userid and password by using the following property 40 | // the password must be an md5 hash eg.. 5f4dcc3b5aa765d61d8327deb882cf99 ('password') 41 | //httpAdminAuth: {user:"user",pass:"5f4dcc3b5aa765d61d8327deb882cf99"}, 42 | 43 | // Serve up the welcome page 44 | httpStatic: path.join(__dirname,"public"), 45 | 46 | functionGlobalContext: { }, 47 | 48 | storageModule: require("./pgstorage"), 49 | 50 | httpNodeCors: { 51 | origin: "*", 52 | methods: "GET,PUT,POST,DELETE" 53 | }, 54 | 55 | // Disbled Credential Secret 56 | credentialSecret: false 57 | } 58 | 59 | if (process.env.NODE_RED_USERNAME && process.env.NODE_RED_PASSWORD) { 60 | settings.adminAuth = { 61 | type: "credentials", 62 | users: function(username) { 63 | if (process.env.NODE_RED_USERNAME == username) { 64 | return when.resolve({username:username,permissions:"*"}); 65 | } else { 66 | return when.resolve(null); 67 | } 68 | }, 69 | authenticate: function(username, password) { 70 | if (process.env.NODE_RED_USERNAME == username && 71 | process.env.NODE_RED_PASSWORD == password) { 72 | return when.resolve({username:username,permissions:"*"}); 73 | } else { 74 | return when.resolve(null); 75 | } 76 | } 77 | } 78 | } 79 | 80 | settings.pgAppname = 'nodered'; 81 | pgutil.initPG(); 82 | pgutil.createTable(); 83 | --------------------------------------------------------------------------------