frugal-iot-server 2.0.5 → 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALLATION.md +3 -3
- package/README.md +1 -1
- package/frugal-iot-server.js +41 -3
- package/lib/enrol.js +7 -0
- package/lib/retained.js +58 -0
- package/package.json +9 -8
- package/public/service-worker.js +1 -1
- package/scripts/install-pi.sh +4 -3
- package/public/index-embedded.html +0 -54
package/INSTALLATION.md
CHANGED
|
@@ -308,9 +308,9 @@ node -v
|
|
|
308
308
|
|
|
309
309
|
What each is for:
|
|
310
310
|
|
|
311
|
-
* `nodejs` — the server needs **Node
|
|
312
|
-
provides 20.19.2, which is fine, on 32-bit as well as 64-bit. Older images shipped Node 18,
|
|
313
|
-
|
|
311
|
+
* `nodejs` — the server needs **Node 20.17 or later**. Current Raspberry Pi OS (Debian 13, trixie)
|
|
312
|
+
provides 20.19.2, which is fine, on 32-bit as well as 64-bit. Older images shipped Node 18, which
|
|
313
|
+
is too old. If `node -v` reports anything below 20.17, install a current version from
|
|
314
314
|
[NodeSource](https://github.com/nodesource/distributions) instead.
|
|
315
315
|
* `npm` — installs the server; it is a separate package from `nodejs` on Debian.
|
|
316
316
|
* `sqlite3` — the database the server keeps its accounts in.
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ thing in one go once you can ssh in.
|
|
|
8
8
|
|
|
9
9
|
On a unix box that is already running ...
|
|
10
10
|
|
|
11
|
-
Check you have node & npm installed `node -v` - version
|
|
11
|
+
Check you have node & npm installed `node -v` - version 20.17 or later.
|
|
12
12
|
If not then you'll need nodejs from [nodejs.org](https://nodejs.org) or your package manager.
|
|
13
13
|
You also need `sqlite3` and `zsh` for the setup commands below.
|
|
14
14
|
|
package/frugal-iot-server.js
CHANGED
|
@@ -110,9 +110,9 @@ import { createAPIRouter, createAPIErrorHandler } from './lib/api-routes.js';
|
|
|
110
110
|
import { buildConfigFor, hasPermissions } from './lib/config-for-user.js';
|
|
111
111
|
import { ensureSecrets, addEnrolmentSecret, removeEnrolmentSecret } from './lib/secrets.js';
|
|
112
112
|
import { syncUser, syncUserById, syncLoggers, syncNode, dropNode, syncBridge } from './lib/dynsec-server.js';
|
|
113
|
-
import { enrol, ENROL, enrolmentSecretsFor, makeRateLimiter, forgetNode,
|
|
113
|
+
import { enrol, ENROL, enrolmentSecretsFor, makeRateLimiter, forgetNode, formerProjects,
|
|
114
114
|
makeAttemptLog, setGrant, clearGrant, readGrants } from './lib/enrol.js';
|
|
115
|
-
import { deleteRetained } from './lib/retained.js';
|
|
115
|
+
import { deleteRetained, clearRetainedUnder } from './lib/retained.js';
|
|
116
116
|
import { replicaFor, bridgeForToken, noteBridgePull, startReplica } from './lib/replica.js';
|
|
117
117
|
import { addMissingColumns } from './lib/schema-columns.js';
|
|
118
118
|
import { createLoggerClient } from './lib/logger-client.js';
|
|
@@ -780,6 +780,38 @@ function addLoggedNodesToConfig() {
|
|
|
780
780
|
});
|
|
781
781
|
});
|
|
782
782
|
}
|
|
783
|
+
/*
|
|
784
|
+
* A node has enrolled in a new project, so its old address is retired: enrolment, broker account,
|
|
785
|
+
* retained messages, and what the logger and /config.json remember of it.
|
|
786
|
+
*
|
|
787
|
+
* Done here, once it has arrived, rather than when the move is asked for. A sleeping node only gets
|
|
788
|
+
* the move when it next wakes, and it needs its old credential to receive it - removed earlier, the
|
|
789
|
+
* node would be refused, enrol again in the OLD project, and never hear of the move.
|
|
790
|
+
*/
|
|
791
|
+
function retireFormerHome(org, project, nodeid, cb) {
|
|
792
|
+
waterfall([
|
|
793
|
+
(cb1) => forgetNode(db, org, project, nodeid, (err) => cb1(err)),
|
|
794
|
+
// Best effort, as for /node_reset - frugal-iot-diagnostic reports anything left behind.
|
|
795
|
+
(cb1) => dropNode(config, org, project, nodeid, () => cb1(null)),
|
|
796
|
+
(cb1) => clearRetainedUnder(config, org, `${org}/${project}/${nodeid}`, (err, result) => {
|
|
797
|
+
if (err) { console.error("Retained messages for", `${org}/${project}/${nodeid}`, "not cleared:", err.message); }
|
|
798
|
+
cb1(null, result);
|
|
799
|
+
}),
|
|
800
|
+
(result, cb1) => {
|
|
801
|
+
// After the retained messages, or the logger would hear them again and remember the node.
|
|
802
|
+
mqttLogger.forgetNode(org, project, nodeid);
|
|
803
|
+
const nodes = config.organizations?.[org]?.projects?.[project]?.nodes;
|
|
804
|
+
if (nodes) { delete nodes[nodeid]; }
|
|
805
|
+
console.log("Node", nodeid, "moved out of", `${org}/${project}`, "-",
|
|
806
|
+
result ? `${result.deleted} retained topic(s) cleared` : "retained topics left");
|
|
807
|
+
cb1(null);
|
|
808
|
+
},
|
|
809
|
+
], (err) => {
|
|
810
|
+
if (err) { console.error("Could not retire", `${org}/${project}/${nodeid}:`, err.message); }
|
|
811
|
+
cb(null);
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
783
815
|
// See lib/config-for-user.js - kept there so it can be tested without starting a server.
|
|
784
816
|
function unsafeCopyConfigFor(user) {
|
|
785
817
|
return buildConfigFor(config, user);
|
|
@@ -906,7 +938,13 @@ mqttLogger.readYamlConfig('.', (err, configobj) => {
|
|
|
906
938
|
req.body, (err, cred) => {
|
|
907
939
|
if (!err) {
|
|
908
940
|
console.log("Enrolled", cred.username, req.body.lora ? "(LoRa-capable)" : "");
|
|
909
|
-
|
|
941
|
+
res.status(200).json(cred);
|
|
942
|
+
const { org, project, nodeid } = req.body;
|
|
943
|
+
formerProjects(db, org, project, nodeid, (ferr, olds) => {
|
|
944
|
+
if (ferr) { return console.error("Could not look for", nodeid, "elsewhere:", ferr.message); }
|
|
945
|
+
each(olds, (old, cb) => retireFormerHome(org, old, nodeid, cb), () => {});
|
|
946
|
+
});
|
|
947
|
+
return;
|
|
910
948
|
}
|
|
911
949
|
const status = {
|
|
912
950
|
[ENROL.BAD_REQUEST]: 400,
|
package/lib/enrol.js
CHANGED
|
@@ -389,3 +389,10 @@ export function forgetNode(db, org, project, nodeid, cb) {
|
|
|
389
389
|
db.run('DELETE FROM nodes WHERE org = ? AND project = ? AND nodeid = ?',
|
|
390
390
|
[org, project, nodeid], function (err) { cb(err, this && this.changes); });
|
|
391
391
|
}
|
|
392
|
+
|
|
393
|
+
// Where else this node is enrolled. A node id is one device, so any other project is somewhere it
|
|
394
|
+
// used to be - see retireFormerHome in frugal-iot-server.js.
|
|
395
|
+
export function formerProjects(db, org, project, nodeid, cb) {
|
|
396
|
+
db.all('SELECT project FROM nodes WHERE org = ? AND nodeid = ? AND project != ?',
|
|
397
|
+
[org, nodeid, project], (err, rows) => cb(err, (rows || []).map((r) => r.project)));
|
|
398
|
+
}
|
package/lib/retained.js
CHANGED
|
@@ -108,3 +108,61 @@ function refusal(status, message) {
|
|
|
108
108
|
}
|
|
109
109
|
function err500(message) { return refusal(500, message); }
|
|
110
110
|
function refuse(cb, status, message) { return cb(refusal(status, message)); }
|
|
111
|
+
|
|
112
|
+
// Reading needs a different account from deleting: the admin account may publish anywhere in the
|
|
113
|
+
// organization but subscribe nowhere. The logger's reads the whole organization.
|
|
114
|
+
function readCredentialFor(config, org) {
|
|
115
|
+
const o = (config && config.organizations && config.organizations[org]) || {};
|
|
116
|
+
if (o.logger_userid) return { username: o.logger_userid, password: o.logger_password };
|
|
117
|
+
if (o.mqtt_password) return { username: org, password: o.mqtt_password };
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Retained topics arrive straight after the subscription is granted; this is how long to wait for
|
|
122
|
+
// the last of them, as frugal-iot-clearretained does.
|
|
123
|
+
const COLLECT_MS = 3000;
|
|
124
|
+
|
|
125
|
+
/*
|
|
126
|
+
* Delete every retained message under `prefix` - one node's whole subtree, when it has moved.
|
|
127
|
+
*
|
|
128
|
+
* Takes a prefix, not a pattern, so it cannot be widened to more than the caller named: the
|
|
129
|
+
* wildcard is added here.
|
|
130
|
+
*/
|
|
131
|
+
export function clearRetainedUnder(config, org, prefix, cb) {
|
|
132
|
+
if (!NAME.test(String(org || ''))) return refuse(cb, 400, 'Not an organization name');
|
|
133
|
+
if (!String(prefix || '').startsWith(`${org}/`) || /[+#]/.test(prefix) || prefix.split('/').length < 3) {
|
|
134
|
+
return refuse(cb, 400, `${prefix} is not a node within ${org}`);
|
|
135
|
+
}
|
|
136
|
+
const broker = config && config.mqtt && config.mqtt.broker;
|
|
137
|
+
if (!broker) return refuse(cb, 500, 'No mqtt.broker in the configuration');
|
|
138
|
+
const cred = readCredentialFor(config, org);
|
|
139
|
+
if (!cred) return refuse(cb, 500, `No broker credential to read ${org}`);
|
|
140
|
+
|
|
141
|
+
const client = mqtt.connect(broker, {
|
|
142
|
+
username: cred.username, password: cred.password,
|
|
143
|
+
connectTimeout: 5000, reconnectPeriod: 0, clean: true,
|
|
144
|
+
});
|
|
145
|
+
const topics = new Set();
|
|
146
|
+
let settled = false;
|
|
147
|
+
const finish = (err) => {
|
|
148
|
+
if (settled) return;
|
|
149
|
+
settled = true;
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
client.end(true, () => {
|
|
152
|
+
if (err) return cb(err);
|
|
153
|
+
if (!topics.size) return cb(null, { deleted: 0, failed: 0 });
|
|
154
|
+
deleteRetained(config, org, [...topics], cb);
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
const timer = setTimeout(() => finish(err500('The broker did not answer')), 30000);
|
|
158
|
+
client.on('error', (err) => finish(refusal(502, `Could not reach the broker: ${err.message}`)));
|
|
159
|
+
client.on('message', (topic, message, packet) => {
|
|
160
|
+
if (packet && packet.retain && message.length) topics.add(topic);
|
|
161
|
+
});
|
|
162
|
+
client.on('connect', () => {
|
|
163
|
+
client.subscribe(`${prefix}/#`, { qos: 0 }, (err) => {
|
|
164
|
+
if (err) return finish(refusal(502, `Could not subscribe to ${prefix}/#: ${err.message}`));
|
|
165
|
+
setTimeout(() => finish(null), COLLECT_MS);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
package/package.json
CHANGED
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
"cookie-parser": "^1.4.7",
|
|
13
13
|
"express": "^5.2.1",
|
|
14
14
|
"express-session": "^1.19.0",
|
|
15
|
-
"frugal-iot-client": "^2.0.
|
|
16
|
-
"frugal-iot-logger": "^2.0.
|
|
15
|
+
"frugal-iot-client": "^2.0.7",
|
|
16
|
+
"frugal-iot-logger": "^2.0.7",
|
|
17
17
|
"hash-wasm": "^4.12.0",
|
|
18
18
|
"js-yaml": "^4.2.0",
|
|
19
19
|
"morgan": "^1.11.0",
|
|
20
20
|
"mqtt": "^5.15.2",
|
|
21
21
|
"multer": "^2.1.1",
|
|
22
|
-
"nodemailer": "^
|
|
22
|
+
"nodemailer": "^10.1.2",
|
|
23
23
|
"passport": "^0.7.0",
|
|
24
24
|
"passport-local": "^1.0.0",
|
|
25
|
-
"sqlite3": "^
|
|
25
|
+
"sqlite3": "^6.0.1"
|
|
26
26
|
},
|
|
27
27
|
"bin": {
|
|
28
28
|
"frugal-iot-server": "frugal-iot-server.js",
|
|
@@ -63,10 +63,8 @@
|
|
|
63
63
|
"INSTALLATION.md"
|
|
64
64
|
],
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@types/jest": "^30.0.0",
|
|
67
|
-
"jest": "^30.4.2",
|
|
68
66
|
"supertest": "^7.2.2",
|
|
69
|
-
"vitest": "^1.
|
|
67
|
+
"vitest": "^4.1.12"
|
|
70
68
|
},
|
|
71
69
|
"homepage": "https://github.com/mitra42/frugal-iot-server",
|
|
72
70
|
"keywords": [
|
|
@@ -89,5 +87,8 @@
|
|
|
89
87
|
"check-files": "node scripts/check-published-files.js"
|
|
90
88
|
},
|
|
91
89
|
"type": "module",
|
|
92
|
-
"version": "2.0.
|
|
90
|
+
"version": "2.0.7",
|
|
91
|
+
"overrides": {
|
|
92
|
+
"uuid": "^11.1.1"
|
|
93
|
+
}
|
|
93
94
|
}
|
package/public/service-worker.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// it, an installed browser keeps serving the files it already cached - and since the list now covers
|
|
7
7
|
// the dashboard's own code, that means old JS behind freshly fetched HTML. Any edit to the list
|
|
8
8
|
// needs this bumped, or it does not reach anyone who already has the old one.
|
|
9
|
-
const CACHE_NAME = 'frugal-iot-cache-2.0.
|
|
9
|
+
const CACHE_NAME = 'frugal-iot-cache-2.0.7';
|
|
10
10
|
/*
|
|
11
11
|
* Everything the app needs, so that a phone on a poor or expensive link fetches it once.
|
|
12
12
|
*
|
package/scripts/install-pi.sh
CHANGED
|
@@ -571,10 +571,11 @@ PACKAGES=(nodejs npm sqlite3 zsh curl)
|
|
|
571
571
|
sudo_ env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${PACKAGES[@]}"
|
|
572
572
|
NODE_VERSION="$(node -v)"
|
|
573
573
|
info "node ${NODE_VERSION}, npm $(npm -v), sqlite3 $(sqlite3 --version | awk '{print $1}')"
|
|
574
|
-
# The server needs node
|
|
574
|
+
# The server needs node 20.17 or later (sqlite3 6 needs 20.17, nodemailer 10 needs 20)
|
|
575
575
|
NODE_MAJOR="$(echo "${NODE_VERSION#v}" | cut -d. -f1)"
|
|
576
|
-
|
|
577
|
-
|
|
576
|
+
NODE_MINOR="$(echo "${NODE_VERSION#v}" | cut -d. -f2)"
|
|
577
|
+
if (( NODE_MAJOR < 20 || (NODE_MAJOR == 20 && NODE_MINOR < 17) )); then
|
|
578
|
+
echo "node ${NODE_VERSION} is too old - the server needs 20.17 or later." >&2
|
|
578
579
|
echo "Install a current version from https://github.com/nodesource/distributions and run this again." >&2
|
|
579
580
|
exit 1
|
|
580
581
|
fi
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<title>Frugal IoT Demo</title>
|
|
6
|
-
<link href="./frugaliot.css" rel="stylesheet" type="text/css">
|
|
7
|
-
<!-- this maps files that webcomponents uses, -->
|
|
8
|
-
<script type="importmap">
|
|
9
|
-
{
|
|
10
|
-
"imports": {
|
|
11
|
-
"@kurkle/color": "https://frugaliot.naturalinnovation.org/node_modules/@kurkle/color/dist/color.esm.js",
|
|
12
|
-
"chart.js": "https://frugaliot.naturalinnovation.org/node_modules/chart.js/dist/Chart.js",
|
|
13
|
-
"dial-gauge": "/node_modules/dial-gauge/dial-gauge.js",
|
|
14
|
-
"luxon": "https://frugaliot.naturalinnovation.org/node_modules/luxon/src/luxon.js",
|
|
15
|
-
"chartjs-adapter-luxon": "https://frugaliot.naturalinnovation.org/node_modules/chartjs-adapter-luxon/dist/chartjs-adapter-luxon.esm.js",
|
|
16
|
-
"csv-parse": "https://frugaliot.naturalinnovation.org/node_modules/csv-parse/dist/esm/index.js"
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
</script>
|
|
20
|
-
<!-- load webcomponents.js either from local server, or from frugaliot.naturalinnovation.org -->
|
|
21
|
-
<script src="./webcomponents.js" type="module"></script>
|
|
22
|
-
<!--<script src="./webcomponents.js" type="module"></script>-->
|
|
23
|
-
</head>
|
|
24
|
-
<body>
|
|
25
|
-
<h1>This is an example of embedding a widget in a page</h1>
|
|
26
|
-
<div style="display:none">
|
|
27
|
-
<!-- every embedded page needs a mqtt-client, it sets up the MQTT connection to the broker -->
|
|
28
|
-
<mqtt-client server="wss://frugaliot.naturalinnovation.org/wss"></mqtt-client>
|
|
29
|
-
</div>
|
|
30
|
-
<div style="float:right;border:2px,solid,blue;width:50%">
|
|
31
|
-
<!-- this is a simple display widget - in this case a meter bar -->
|
|
32
|
-
<mqtt-bar max="45" min="0" topic="dev/lotus/esp8266-fb94bb/sht/temperature" label="Temperature" type="float" color="red"></mqtt-bar>
|
|
33
|
-
<!--<mqtt-bar max="45" min="0" topic="dev/lotus/esp32-70c376/soil" label="Soil" type="float" color="brown"></mqtt-bar>-->
|
|
34
|
-
</div>
|
|
35
|
-
<div style="float:left;border:2px,solid,blue;width:10%">
|
|
36
|
-
<!-- this is a simple control widget - in this case toggles an LED on a device at my office -->
|
|
37
|
-
<mqtt-toggle topic="dev/lotus/esp8266-fb94bb/ledbuiltin/on" label="LED" type="bool" color="blue"></mqtt-toggle>
|
|
38
|
-
</div>
|
|
39
|
-
<p>
|
|
40
|
-
This is a demo of a custom web page with a bar widget; a graph; and a control.
|
|
41
|
-
These all go to the same device which may or may not always be turned on!
|
|
42
|
-
</p>
|
|
43
|
-
<p>
|
|
44
|
-
<!-- And a pretty simple graph - in this case its displaying the same value from above, but it can
|
|
45
|
-
<!-- display any topic -->
|
|
46
|
-
<!-- Note - I think there is an issue with back button if loaded from a different server, will fix -->
|
|
47
|
-
<mqtt-graph>
|
|
48
|
-
<mqtt-graphdataset topic="dev/lotus/esp8266-fb94bb/sht/temperature" color="red" label="Office Temperature" min=0 max=50 type="float"></mqtt-graphdataset>
|
|
49
|
-
<mqtt-graphdataset topic="dev/lotus/esp8266-fb94bb/sht/humidity" color="blue" label="Office Humidity" min=0 max=100 type="float"></mqtt-graphdataset>
|
|
50
|
-
<mqtt-graphdataset topic="dev/lotus/esp8266-fb94bb/ledbuiltin/on" color="orange" label="LED" type="bool"></mqtt-graphdataset>
|
|
51
|
-
</mqtt-graph>
|
|
52
|
-
</p>
|
|
53
|
-
</body>
|
|
54
|
-
</html>
|