frugal-iot-server 0.3.7 → 0.3.8
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 +299 -0
- package/README.md +94 -19
- package/config.d/schema/devices.yaml +63 -0
- package/config.d/schema/modules.yaml +8 -37
- package/config.d/schema/topics.yaml +39 -6
- package/extras/mosquitto-bridge.conf.example +130 -0
- package/frugal-iot-server.js +50 -2
- package/lib/housekeeping.js +10 -4
- package/package.json +10 -5
- package/public/index.html +8 -0
- package/public/service-worker.js +4 -3
- package/scripts/addbridge-pi.zsh +164 -0
- package/scripts/addbridge-prod.zsh +215 -0
- package/scripts/check-published-files.js +0 -0
- package/scripts/check-schema.js +126 -15
- package/scripts/clearretained.zsh +153 -0
- package/scripts/copy-schema-to-examples.zsh +9 -5
- package/scripts/diagnostic.zsh +47 -0
- package/scripts/install-pi.sh +646 -0
- package/scripts/not-published.txt +27 -0
- package/scripts/prerelease.zsh +91 -17
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
# name Capitalized English (and add translation below in 'languages')
|
|
10
10
|
# max For gauges, slider
|
|
11
11
|
# min For gauges, slider
|
|
12
|
+
# width Total characters of the rendered number, including any sign and the decimal point and
|
|
13
|
+
# excluding units - the same meaning the node gives it. Decimals are derived, not declared:
|
|
14
|
+
# intWidth = max(len(int(min)), len(int(max))) including a '-', then decimals = width-intWidth-1.
|
|
15
|
+
# Deriving from min/max rather than from the value keeps the decimal count steady as the value
|
|
16
|
+
# moves. Only float and exponential topics need it; an int has no decimals to decide.
|
|
12
17
|
# color can be a name or a #RRGGBB
|
|
13
18
|
# display One of bar,gauge,text,slider,inputbox
|
|
14
19
|
# slot if this topic fits into a specific slot in the enclosing module (currently just frugal_iot module)
|
|
@@ -21,7 +26,8 @@
|
|
|
21
26
|
# no setting at all, any float, int or bool with "rw: r" is recorded, which is easy to be
|
|
22
27
|
# surprised by. scripts/check-schema.js warns about a topic that does not say.
|
|
23
28
|
# duplicates when to bother recording a reading: significantvalue is how much it has to have moved
|
|
24
|
-
# since the last one recorded
|
|
29
|
+
# since the last one recorded - either an absolute amount e.g. 0.5, or as a percentage of the last
|
|
30
|
+
# reading e.g. "2%" - significantdate how long (in milliseconds) since that one, and a
|
|
25
31
|
# reading that meets neither is dropped. Every topic with "log: true" wants a rule here - without
|
|
26
32
|
# one, every reading is written to the SD card as it arrives, which wears the card out.
|
|
27
33
|
# Note battery gets special cased
|
|
@@ -30,9 +36,11 @@ altitude:
|
|
|
30
36
|
leaf: altitude
|
|
31
37
|
name: Altitude
|
|
32
38
|
type: float
|
|
39
|
+
units: m
|
|
33
40
|
display: text
|
|
34
41
|
min: -500
|
|
35
42
|
max: 9000
|
|
43
|
+
width: 6
|
|
36
44
|
color: black
|
|
37
45
|
log: true
|
|
38
46
|
duplicates:
|
|
@@ -111,6 +119,7 @@ bus:
|
|
|
111
119
|
display: text
|
|
112
120
|
min: 0
|
|
113
121
|
max: 32
|
|
122
|
+
width: 5
|
|
114
123
|
color: green
|
|
115
124
|
rw: r
|
|
116
125
|
log: true
|
|
@@ -142,6 +151,7 @@ controlfloat:
|
|
|
142
151
|
#[leaf, name] should be overridden
|
|
143
152
|
min: 0
|
|
144
153
|
max: 100
|
|
154
|
+
width: 5
|
|
145
155
|
type: float
|
|
146
156
|
log: false
|
|
147
157
|
display: text
|
|
@@ -198,6 +208,7 @@ course:
|
|
|
198
208
|
display: text
|
|
199
209
|
min: 0
|
|
200
210
|
max: 360
|
|
211
|
+
width: 5
|
|
201
212
|
color: black
|
|
202
213
|
log: true
|
|
203
214
|
duplicates:
|
|
@@ -213,6 +224,7 @@ current:
|
|
|
213
224
|
display: text
|
|
214
225
|
min: -3200
|
|
215
226
|
max: 3200
|
|
227
|
+
width: 7
|
|
216
228
|
color: red
|
|
217
229
|
rw: r
|
|
218
230
|
log: true
|
|
@@ -237,6 +249,7 @@ do:
|
|
|
237
249
|
display: bar
|
|
238
250
|
min: 0
|
|
239
251
|
max: 20
|
|
252
|
+
width: 5
|
|
240
253
|
color: blue
|
|
241
254
|
rw: r
|
|
242
255
|
log: true
|
|
@@ -267,6 +280,7 @@ gas:
|
|
|
267
280
|
display: text
|
|
268
281
|
min: 0
|
|
269
282
|
max: 500
|
|
283
|
+
width: 5
|
|
270
284
|
color: green
|
|
271
285
|
rw: r
|
|
272
286
|
log: true
|
|
@@ -282,6 +296,7 @@ hdop:
|
|
|
282
296
|
display: text
|
|
283
297
|
min: 0
|
|
284
298
|
max: 50
|
|
299
|
+
width: 5
|
|
285
300
|
color: black
|
|
286
301
|
log: false
|
|
287
302
|
duplicates:
|
|
@@ -296,6 +311,7 @@ humidity:
|
|
|
296
311
|
display: bar
|
|
297
312
|
min: 0
|
|
298
313
|
max: 100
|
|
314
|
+
width: 5
|
|
299
315
|
color: blue
|
|
300
316
|
log: true
|
|
301
317
|
duplicates:
|
|
@@ -342,6 +358,7 @@ latitude:
|
|
|
342
358
|
display: text
|
|
343
359
|
min: -90
|
|
344
360
|
max: +90
|
|
361
|
+
width: 9
|
|
345
362
|
color: black
|
|
346
363
|
log: true
|
|
347
364
|
duplicates:
|
|
@@ -357,6 +374,7 @@ load:
|
|
|
357
374
|
display: text
|
|
358
375
|
min: 0
|
|
359
376
|
max: 32
|
|
377
|
+
width: 5
|
|
360
378
|
color: green
|
|
361
379
|
rw: r
|
|
362
380
|
log: true
|
|
@@ -372,6 +390,7 @@ loadcell:
|
|
|
372
390
|
display: text
|
|
373
391
|
min: 0
|
|
374
392
|
max: 65000
|
|
393
|
+
width: 7
|
|
375
394
|
color: yellow
|
|
376
395
|
log: true
|
|
377
396
|
duplicates:
|
|
@@ -387,6 +406,7 @@ longitude:
|
|
|
387
406
|
display: text
|
|
388
407
|
min: -180
|
|
389
408
|
max: +180
|
|
409
|
+
width: 10
|
|
390
410
|
color: black
|
|
391
411
|
log: true
|
|
392
412
|
duplicates:
|
|
@@ -402,12 +422,13 @@ lux:
|
|
|
402
422
|
display: bar
|
|
403
423
|
min: 0
|
|
404
424
|
max: 65000
|
|
425
|
+
width: 5
|
|
405
426
|
color: yellow
|
|
406
427
|
log: true
|
|
407
428
|
rw: r
|
|
408
429
|
duplicates:
|
|
409
430
|
significantdate: 900000
|
|
410
|
-
significantvalue:
|
|
431
|
+
significantvalue: 5%
|
|
411
432
|
graphable: true
|
|
412
433
|
units: lx
|
|
413
434
|
manual:
|
|
@@ -442,6 +463,7 @@ mm:
|
|
|
442
463
|
display: text
|
|
443
464
|
min: 0
|
|
444
465
|
max: 500
|
|
466
|
+
width: 5
|
|
445
467
|
color: blue
|
|
446
468
|
rw: r
|
|
447
469
|
log: true
|
|
@@ -499,6 +521,7 @@ power:
|
|
|
499
521
|
display: text
|
|
500
522
|
min: 0
|
|
501
523
|
max: 100000
|
|
524
|
+
width: 8
|
|
502
525
|
color: orange
|
|
503
526
|
rw: r
|
|
504
527
|
log: true
|
|
@@ -512,8 +535,10 @@ pressure:
|
|
|
512
535
|
name: Pressure
|
|
513
536
|
type: float
|
|
514
537
|
display: bar
|
|
515
|
-
min:
|
|
516
|
-
max:
|
|
538
|
+
min: 300
|
|
539
|
+
max: 1100
|
|
540
|
+
width: 6
|
|
541
|
+
units: hPa
|
|
517
542
|
color: blue
|
|
518
543
|
rw: r
|
|
519
544
|
log: true
|
|
@@ -528,6 +553,7 @@ rain:
|
|
|
528
553
|
display: text
|
|
529
554
|
min: 0
|
|
530
555
|
max: 500
|
|
556
|
+
width: 5
|
|
531
557
|
color: blue
|
|
532
558
|
rw: r
|
|
533
559
|
log: true
|
|
@@ -543,6 +569,7 @@ satellites:
|
|
|
543
569
|
display: text
|
|
544
570
|
min: 0
|
|
545
571
|
max: 32
|
|
572
|
+
width: 2
|
|
546
573
|
color: black
|
|
547
574
|
log: false
|
|
548
575
|
duplicates:
|
|
@@ -550,7 +577,7 @@ satellites:
|
|
|
550
577
|
significantvalue: 1
|
|
551
578
|
rw: r
|
|
552
579
|
graphable: false
|
|
553
|
-
units:
|
|
580
|
+
units: count
|
|
554
581
|
select:
|
|
555
582
|
leaf: select
|
|
556
583
|
name: Select
|
|
@@ -569,6 +596,7 @@ shunt:
|
|
|
569
596
|
display: text
|
|
570
597
|
min: -320
|
|
571
598
|
max: 320
|
|
599
|
+
width: 6
|
|
572
600
|
color: blue
|
|
573
601
|
rw: r
|
|
574
602
|
log: true
|
|
@@ -584,6 +612,7 @@ speed:
|
|
|
584
612
|
display: text
|
|
585
613
|
min: 0
|
|
586
614
|
max: 999
|
|
615
|
+
width: 5
|
|
587
616
|
color: black
|
|
588
617
|
log: true
|
|
589
618
|
duplicates:
|
|
@@ -607,7 +636,7 @@ soil:
|
|
|
607
636
|
significantvalue: 1
|
|
608
637
|
graphable: true
|
|
609
638
|
calibrate: true
|
|
610
|
-
units: "%
|
|
639
|
+
units: "%"
|
|
611
640
|
temperature:
|
|
612
641
|
leaf: temperature
|
|
613
642
|
name: Temperature
|
|
@@ -616,6 +645,7 @@ temperature:
|
|
|
616
645
|
rw: r
|
|
617
646
|
min: 0
|
|
618
647
|
max: 50
|
|
648
|
+
width: 4
|
|
619
649
|
color: red
|
|
620
650
|
wireable: false
|
|
621
651
|
log: true
|
|
@@ -629,6 +659,7 @@ timeoff:
|
|
|
629
659
|
name: Time Off (s)
|
|
630
660
|
min: 0
|
|
631
661
|
max: 3600
|
|
662
|
+
width: 4
|
|
632
663
|
type: float
|
|
633
664
|
log: false
|
|
634
665
|
display: text
|
|
@@ -641,6 +672,7 @@ timeon:
|
|
|
641
672
|
name: Time On (s)
|
|
642
673
|
min: 0
|
|
643
674
|
max: 3600
|
|
675
|
+
width: 4
|
|
644
676
|
type: float
|
|
645
677
|
log: false
|
|
646
678
|
display: text
|
|
@@ -670,6 +702,7 @@ ultrasonic:
|
|
|
670
702
|
display: bar
|
|
671
703
|
min: 0
|
|
672
704
|
max: 7500
|
|
705
|
+
width: 4
|
|
673
706
|
color: blue
|
|
674
707
|
rw: r
|
|
675
708
|
log: true
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Bridging this Pi's broker to a production Frugal IoT server
|
|
2
|
+
#
|
|
3
|
+
# Optional. Without this file the Pi is self-contained: nodes publish to the broker here, the
|
|
4
|
+
# logger records the readings here, and the dashboard here shows them. Adding it also relays
|
|
5
|
+
# everything to a production server, so the same nodes appear on a dashboard elsewhere and can be
|
|
6
|
+
# controlled from it, while the Pi carries on working on its own whenever the link is down.
|
|
7
|
+
#
|
|
8
|
+
# This is a Mosquitto "bridge" - the broker's own feature for connecting to another broker. The
|
|
9
|
+
# server, the logger and the nodes know nothing about it and need no configuration change.
|
|
10
|
+
#
|
|
11
|
+
# To use it:
|
|
12
|
+
# sudo cp extras/mosquitto-bridge.conf.example /etc/mosquitto/conf.d/frugal-iot-bridge.conf
|
|
13
|
+
# sudo nano /etc/mosquitto/conf.d/frugal-iot-bridge.conf # replace every <...> below
|
|
14
|
+
# sudo systemctl restart mosquitto
|
|
15
|
+
#
|
|
16
|
+
# It has to be a restart, not a reload: Mosquitto does not pick up bridges on a reload signal.
|
|
17
|
+
# That briefly disconnects every node, which they recover from on their own.
|
|
18
|
+
#
|
|
19
|
+
# The file goes in /etc/mosquitto/conf.d/ rather than in this installation, because it holds a
|
|
20
|
+
# password for the production broker. Everything under config.d/ here is served to any logged-in
|
|
21
|
+
# browser by /config.json, so a credential put there would not stay private.
|
|
22
|
+
|
|
23
|
+
# ============ The connection ============
|
|
24
|
+
|
|
25
|
+
# Names the bridge. Anything, but it appears in the log, so make it recognisable.
|
|
26
|
+
connection to-prod
|
|
27
|
+
|
|
28
|
+
# The production broker's plain-MQTT-over-TLS address. Not the wss:// URL the browsers and the
|
|
29
|
+
# server use - a Mosquitto bridge speaks MQTT or MQTT-over-TLS and cannot use WebSockets, so
|
|
30
|
+
# production needs a listener on 8883 for this. Port 1883 would work on a trusted LAN, but sends
|
|
31
|
+
# the password below in clear, so over the internet use 8883 and keep the TLS lines further down.
|
|
32
|
+
address <prod-host>:8883
|
|
33
|
+
|
|
34
|
+
# The account this Pi uses on the production broker. Give each Pi its own, rather than sharing the
|
|
35
|
+
# organization's account: the organization password is handed to every browser that logs in, so it
|
|
36
|
+
# is not a secret, and a per-Pi account can be restricted to just this site's topics and revoked
|
|
37
|
+
# on its own.
|
|
38
|
+
remote_username <bridge-account>
|
|
39
|
+
remote_password <bridge-password>
|
|
40
|
+
|
|
41
|
+
# The client id this Pi presents to production, and the name production uses when it reports the
|
|
42
|
+
# state of this bridge (see "notifications" below). One per Pi, or two Pis will disconnect each
|
|
43
|
+
# other in a loop as each takes over the other's session.
|
|
44
|
+
remote_clientid <bridge-account>
|
|
45
|
+
|
|
46
|
+
# Verify production's certificate against the system CA store, which is what a Let's Encrypt
|
|
47
|
+
# certificate on production needs - no certificate has to be copied onto this Pi. Delete both
|
|
48
|
+
# lines, and use port 1883 above, only on a network you trust end to end.
|
|
49
|
+
bridge_cafile /etc/ssl/certs/ca-certificates.crt
|
|
50
|
+
bridge_insecure false
|
|
51
|
+
|
|
52
|
+
# How long to wait before retrying, in seconds, doubling from the first number up to the second.
|
|
53
|
+
# A Pi on a weak or intermittent link keeps retrying for as long as it takes; the cap stops a
|
|
54
|
+
# flapping connection from hammering production.
|
|
55
|
+
restart_timeout 5 60
|
|
56
|
+
|
|
57
|
+
# Keep production's copy of our subscriptions across a dropped connection. This is the default,
|
|
58
|
+
# and it matters: with "true" instead, Mosquitto re-subscribes on every reconnect and production
|
|
59
|
+
# re-sends every retained message each time, which on a flaky link means a burst of duplicated
|
|
60
|
+
# readings after every blip. Nodes publish nearly everything retained, so that burst is large.
|
|
61
|
+
cleansession false
|
|
62
|
+
|
|
63
|
+
# Publish a retained "1" or "0" to $SYS/broker/connection/<remote_clientid>/state on both brokers
|
|
64
|
+
# whenever the link comes up or goes down. This is how "npx --no frugal-iot-diagnostic" reports
|
|
65
|
+
# whether the bridge is connected, and it also lets production see which sites are reachable.
|
|
66
|
+
# The default is true; it is written out here because it is useful rather than incidental.
|
|
67
|
+
notifications true
|
|
68
|
+
|
|
69
|
+
# Uncomment if production refuses the bridge's connection or logs a denied publish to $SYS. Once
|
|
70
|
+
# production has an acl_file, writing to $SYS may not be permitted, and this stops trying: the
|
|
71
|
+
# state is then published only to the broker here, which is all the diagnostic script reads. What
|
|
72
|
+
# is lost is production's view of whether this site is reachable.
|
|
73
|
+
#notifications_local_only true
|
|
74
|
+
|
|
75
|
+
# Not set, and worth knowing why: local_username / local_password. The broker here requires
|
|
76
|
+
# authentication, but a bridge's local side is internal to the broker and does not go through it.
|
|
77
|
+
# Verified on mosquitto 2.0.21 - an anonymous publish is refused while the bridge still forwards.
|
|
78
|
+
|
|
79
|
+
# ============ What gets relayed ============
|
|
80
|
+
#
|
|
81
|
+
# Replace <org> in all four lines with the organization id - the first element of every topic, and
|
|
82
|
+
# the name of the file in config.d/organizations/. One set of four lines per organization on this
|
|
83
|
+
# Pi. To relay only some projects, replace "<org>/" with "<org>/<project>/" and add "+/" where a
|
|
84
|
+
# level is now missing, or repeat the block per project.
|
|
85
|
+
#
|
|
86
|
+
# Frugal IoT topics come in five shapes, and each line below picks out one or two of them. "+"
|
|
87
|
+
# matches exactly one level, so counting levels is what separates them:
|
|
88
|
+
#
|
|
89
|
+
# <org>/<project> the "quick discover" announcement
|
|
90
|
+
# <org>/<project>/<node>/<module>/<field> a reading
|
|
91
|
+
# <org>/<project>/<node>/<module>/<field>/<parameter> a reading's min, max, colour, wiring
|
|
92
|
+
# <org>/<project>/<node>/set/<module>/<field> a control
|
|
93
|
+
# <org>/<project>/<node>/set/<module>/<field>/<parameter>
|
|
94
|
+
#
|
|
95
|
+
# Readings, at five levels. These are the only topics the logger writes to disk, so this is the
|
|
96
|
+
# one line where a topic must not be matched twice - each extra copy would be another row in the
|
|
97
|
+
# CSV files at the far end.
|
|
98
|
+
#
|
|
99
|
+
# The "0" is the QoS, and it is doing real work. At 0, anything published while production is
|
|
100
|
+
# unreachable is simply dropped: production's history has a gap for the outage and this Pi's is
|
|
101
|
+
# complete. At 1 the broker would instead queue it all up and deliver it on reconnect, where
|
|
102
|
+
# production stamps each reading with the time it arrived - so an outage would come back as a
|
|
103
|
+
# whole day's readings all claiming to have happened at the moment the link returned, which is
|
|
104
|
+
# worse than a gap because it looks like real data. Leave this at 0.
|
|
105
|
+
topic <org>/+/+/+/+ out 0
|
|
106
|
+
|
|
107
|
+
# The quick discover announcement, so production learns which nodes live here. Sent periodically
|
|
108
|
+
# and not retained, so nothing accumulates while the link is down.
|
|
109
|
+
topic <org>/+ out 0
|
|
110
|
+
|
|
111
|
+
# Parameters - the min, max, colour and wiring a node announces once at startup. The dashboard
|
|
112
|
+
# needs them to draw a node properly, and the logger never records them.
|
|
113
|
+
topic <org>/+/+/+/+/+ out 0
|
|
114
|
+
|
|
115
|
+
# Controls, in both directions and at QoS 1. Both directions, because a control can be set from
|
|
116
|
+
# either dashboard and both brokers need to know. QoS 1 because here queueing is exactly what is
|
|
117
|
+
# wanted: a control set on production while this Pi is offline is held and delivered when it comes
|
|
118
|
+
# back, which is the same reason nodes publish these retained.
|
|
119
|
+
#
|
|
120
|
+
# At six levels this overlaps the parameter line above, so some controls reach production twice.
|
|
121
|
+
# That is deliberate - it is the harmless place to put the overlap, since a repeated control
|
|
122
|
+
# carries the same value and the logger discards anything with "set" in it. There is no way to
|
|
123
|
+
# write "six levels but not a control", as MQTT patterns cannot exclude.
|
|
124
|
+
topic <org>/+/+/set/# both 1
|
|
125
|
+
|
|
126
|
+
# Loop protection is Mosquitto's "try_private", on by default, which tells production that this is
|
|
127
|
+
# a bridge and not an ordinary client so that it never echoes our own messages back at us. Both
|
|
128
|
+
# ends must be Mosquitto for that to hold - it was measured working between 2.0.21 and itself.
|
|
129
|
+
# TODO revisit if production ever moves to a different broker, or if a third broker is ever
|
|
130
|
+
# bridged into the same topics, as neither case is covered by that protection.
|
package/frugal-iot-server.js
CHANGED
|
@@ -255,6 +255,13 @@ function send_people_list(req, res) {
|
|
|
255
255
|
});
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
// Sessions older than this hold that user's permissions from before the change - see the middleware
|
|
259
|
+
// after passport.authenticate('session')
|
|
260
|
+
const permissionsChangedAt = new Map(); // user id -> when it last changed
|
|
261
|
+
function notePermissionsChanged(id) {
|
|
262
|
+
permissionsChangedAt.set(Number(id), Date.now());
|
|
263
|
+
}
|
|
264
|
+
|
|
258
265
|
function add_permission(id, capability, org, cb) {
|
|
259
266
|
if ((id === undefined)
|
|
260
267
|
|| (capability === undefined) || (capability.length < 2)
|
|
@@ -267,6 +274,7 @@ function add_permission(id, capability, org, cb) {
|
|
|
267
274
|
(cb) => db.get('SELECT COUNT(id) FROM permissions WHERE id = ? AND capability = ? AND org = ?', [id, capability, org], cb),
|
|
268
275
|
(n_perms, cb) => { if (n_perms["COUNT(id)"] != 0) { cb(new Error("Duplicate permission")); } else { cb(null); }},
|
|
269
276
|
(cb) => db.run(sqlAddPermission, [id, capability, org], cb),
|
|
277
|
+
(cb) => { notePermissionsChanged(id); cb(null); },
|
|
270
278
|
], cb);
|
|
271
279
|
}
|
|
272
280
|
}
|
|
@@ -278,6 +286,7 @@ function permissions_delete(id, capability, org, cb) {
|
|
|
278
286
|
} else {
|
|
279
287
|
waterfall([
|
|
280
288
|
(cb) => db.get('DELETE FROM permissions WHERE id = ? AND capability = ? AND org = ?', [id,capability,org], cb),
|
|
289
|
+
(cb) => { notePermissionsChanged(id); cb(null); },
|
|
281
290
|
], cb);
|
|
282
291
|
}
|
|
283
292
|
}
|
|
@@ -806,6 +815,7 @@ mqttLogger.readYamlConfig('.', (err, configobj) => {
|
|
|
806
815
|
email: user.email,
|
|
807
816
|
phone: user.phone,
|
|
808
817
|
permissions: user.permissions,
|
|
818
|
+
loginAt: Date.now(), // So a permission change can tell which sessions predate it
|
|
809
819
|
});
|
|
810
820
|
});
|
|
811
821
|
});
|
|
@@ -822,6 +832,30 @@ mqttLogger.readYamlConfig('.', (err, configobj) => {
|
|
|
822
832
|
// Check if have a session, and if so store in req.user, uses function defined in deserializeUser above
|
|
823
833
|
app.use(passport.authenticate('session')); // Add user to req.user
|
|
824
834
|
|
|
835
|
+
// End any session that predates a change to that user's permissions.
|
|
836
|
+
//
|
|
837
|
+
// The session carries the permissions read at login, and can_READ, can_ADMIN and the rest
|
|
838
|
+
// read them from there - so without this a change takes effect only when the user next logs
|
|
839
|
+
// in. Granting is merely confusing; revoking is worse, because someone whose ADMIN has been
|
|
840
|
+
// taken away keeps it until their session ends.
|
|
841
|
+
//
|
|
842
|
+
// Rather than re-reading the table on every request to catch something this rare, note when
|
|
843
|
+
// a user's permissions changed and end the sessions older than that. They log in again,
|
|
844
|
+
// which is a small price for a change that happens a handful of times.
|
|
845
|
+
//
|
|
846
|
+
// In memory on purpose: express-session's default store keeps the sessions in this process
|
|
847
|
+
// too, so the two are lost together on a restart and neither can outlive the other.
|
|
848
|
+
app.use((req, res, next) => {
|
|
849
|
+
const changedAt = req.user && permissionsChangedAt.get(Number(req.user.id));
|
|
850
|
+
if (changedAt && (!req.user.loginAt || (changedAt > req.user.loginAt))) {
|
|
851
|
+
console.log("Permissions changed for", req.user.id, "- ending the session made before it");
|
|
852
|
+
// Carry on unauthenticated: a page redirects to login, /config.json answers 401 and the
|
|
853
|
+
// client redirects itself
|
|
854
|
+
return req.logout((err) => (err ? next(err) : next()));
|
|
855
|
+
}
|
|
856
|
+
next();
|
|
857
|
+
});
|
|
858
|
+
|
|
825
859
|
// ===== API Integration: Farm IoT Interoperability Standard =====
|
|
826
860
|
// Its tables are in frugal-iot-createdb.sql, already run by execSqlStart above, so there is
|
|
827
861
|
// nothing to initialize here (lib/database.js still exports initializeSchema for the tests).
|
|
@@ -864,11 +898,15 @@ mqttLogger.readYamlConfig('.', (err, configobj) => {
|
|
|
864
898
|
(req,res,next) => {
|
|
865
899
|
console.log("Trying to login with redirect to",req.body.url);
|
|
866
900
|
// This is ugly, but I cannot see how to pass the URL to passport.authenticate options
|
|
901
|
+
// Default the destination rather than trusting every caller to supply one: with no
|
|
902
|
+
// successRedirect, passport calls next() on success and the request 404s as
|
|
903
|
+
// "Cannot POST /login" - a confusing way to report a missing form field.
|
|
904
|
+
const returnTo = req.body.url || '/dashboard';
|
|
867
905
|
passport.authenticate('local', {
|
|
868
906
|
session: true,
|
|
869
907
|
//failWithError: true,
|
|
870
|
-
failureRedirect: `${loginUrl}?register=false&message=Incorrect+username+or+password&url=${
|
|
871
|
-
successRedirect:
|
|
908
|
+
failureRedirect: `${loginUrl}?register=false&message=Incorrect+username+or+password&url=${returnTo}`,
|
|
909
|
+
successRedirect: returnTo,
|
|
872
910
|
// In failure case will also be messages in the session which need clearing out TODO-N89
|
|
873
911
|
//failureRedirect: `${loginUrl}?register=false&message=Incorrect+username+or+password&url=${req.body.url}`,
|
|
874
912
|
})(req, res, next);
|
|
@@ -1021,6 +1059,16 @@ mqttLogger.readYamlConfig('.', (err, configobj) => {
|
|
|
1021
1059
|
},
|
|
1022
1060
|
send_projects_list,
|
|
1023
1061
|
);
|
|
1062
|
+
// Log out and go back to the login page. There was no way to do this at all before, which
|
|
1063
|
+
// matters more now that a permissions change ends the session and people log in again.
|
|
1064
|
+
app.get('/logout', (req, res, next) => {
|
|
1065
|
+
// With a url to come back to: without one, POST /login has no successRedirect, so on a
|
|
1066
|
+
// *successful* login passport calls next(), nothing else handles POST /login, and express
|
|
1067
|
+
// answers "Cannot POST /login" - which looks like a broken login rather than a missing
|
|
1068
|
+
// parameter.
|
|
1069
|
+
req.logout((err) => (err ? next(err) : res.redirect(`${loginUrl}?register=false&url=/dashboard`)));
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1024
1072
|
// /dashboard is served statically, to logged in users //TODO-N89 restrict orgs to those have permissions for (maybe handled via /config.org )
|
|
1025
1073
|
const routerDashboard = express.Router();
|
|
1026
1074
|
app.use('/dashboard', routerDashboard);
|
package/lib/housekeeping.js
CHANGED
|
@@ -19,7 +19,12 @@
|
|
|
19
19
|
* appending to them.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { createReadStream, createWriteStream, readdir, rename, stat,
|
|
22
|
+
import { createReadStream, createWriteStream, readdir, rename, stat, unlink, readFile, writeFile } from 'fs';
|
|
23
|
+
// statfs arrived in node 18.15, and this package says it needs node 18. Taken off the namespace
|
|
24
|
+
// rather than imported by name, because a named import of something a builtin does not have fails
|
|
25
|
+
// when the module loads - which would stop the server outright on an 18.x older than that, instead
|
|
26
|
+
// of just doing without the one check that needs it (see freePercent below).
|
|
27
|
+
import fs from 'fs';
|
|
23
28
|
import { createGzip, gunzip, gzip } from 'zlib';
|
|
24
29
|
import path from 'path';
|
|
25
30
|
import { each, eachSeries, waterfall } from 'async';
|
|
@@ -147,8 +152,8 @@ function deleteFiles(files, callback) {
|
|
|
147
152
|
* How much of the filesystem holding "dir" is free, as a percentage, or null if it cannot be told.
|
|
148
153
|
*/
|
|
149
154
|
function freePercent(dir, callback) {
|
|
150
|
-
if (typeof statfs !== 'function') { callback(null, null); return; } //
|
|
151
|
-
statfs(dir, (err, st) => {
|
|
155
|
+
if (typeof fs.statfs !== 'function') { callback(null, null); return; } // node older than 18.15
|
|
156
|
+
fs.statfs(dir, (err, st) => {
|
|
152
157
|
if (err || !st || !st.blocks) { callback(null, null); return; }
|
|
153
158
|
callback(null, (st.bavail / st.blocks) * 100);
|
|
154
159
|
});
|
|
@@ -189,7 +194,8 @@ export function housekeep(datadir, config, callback) {
|
|
|
189
194
|
// Delete oldest days until there is enough room. The shortfall is worked out in bytes so
|
|
190
195
|
// that the number of days removed matches what is actually needed.
|
|
191
196
|
console.log(`Housekeeping: ${free.toFixed(1)}% of the disk free, below the ${freeWanted}% wanted - removing the oldest readings`);
|
|
192
|
-
|
|
197
|
+
// Reached only when freePercent returned a number, so statfs exists on this node
|
|
198
|
+
fs.statfs(datadir, (err3, st) => {
|
|
193
199
|
let shortfall = (err3 || !st) ? 0 : ((freeWanted / 100) * st.blocks - st.bavail) * st.bsize;
|
|
194
200
|
let toRemove = [];
|
|
195
201
|
let planned = 0;
|
package/package.json
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"cookie-parser": "^1.4.7",
|
|
13
13
|
"express": "^5.2.1",
|
|
14
14
|
"express-session": "^1.19.0",
|
|
15
|
-
"frugal-iot-client": "^1.3.
|
|
16
|
-
"frugal-iot-logger": "^1.1.
|
|
15
|
+
"frugal-iot-client": "^1.3.14",
|
|
16
|
+
"frugal-iot-logger": "^1.1.20",
|
|
17
17
|
"hash-wasm": "^4.12.0",
|
|
18
18
|
"js-yaml": "^4.2.0",
|
|
19
19
|
"morgan": "^1.11.0",
|
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
"frugal-iot-init": "scripts/init.zsh",
|
|
28
28
|
"frugal-iot-addorganization": "scripts/addorganization.zsh",
|
|
29
29
|
"frugal-iot-setpassword": "scripts/setpassword.zsh",
|
|
30
|
-
"frugal-iot-diagnostic": "scripts/diagnostic.zsh"
|
|
30
|
+
"frugal-iot-diagnostic": "scripts/diagnostic.zsh",
|
|
31
|
+
"frugal-iot-clearretained": "scripts/clearretained.zsh",
|
|
32
|
+
"frugal-iot-addbridge-prod": "scripts/addbridge-prod.zsh",
|
|
33
|
+
"frugal-iot-addbridge-pi": "scripts/addbridge-pi.zsh"
|
|
31
34
|
},
|
|
32
35
|
"description": "Frugal IoT client and server",
|
|
33
36
|
"//files": "An allowlist, not an ignore list - anything not named here is never published, so a stray file (a database backup, a credential) cannot leak by default. Check with: npm pack --dry-run",
|
|
@@ -44,6 +47,7 @@
|
|
|
44
47
|
"private/",
|
|
45
48
|
"scripts/",
|
|
46
49
|
"extras/mosquitto.conf",
|
|
50
|
+
"extras/mosquitto-bridge.conf.example",
|
|
47
51
|
"extras/frugaliot.service",
|
|
48
52
|
"API.md",
|
|
49
53
|
"INSTALLATION.md"
|
|
@@ -68,8 +72,9 @@
|
|
|
68
72
|
"test:watch": "vitest",
|
|
69
73
|
"test:coverage": "vitest run --coverage",
|
|
70
74
|
"check-schema": "node scripts/check-schema.js",
|
|
71
|
-
"prerelease": "zsh scripts/prerelease.zsh"
|
|
75
|
+
"prerelease": "zsh scripts/prerelease.zsh",
|
|
76
|
+
"check-files": "node scripts/check-published-files.js"
|
|
72
77
|
},
|
|
73
78
|
"type": "module",
|
|
74
|
-
"version": "0.3.
|
|
79
|
+
"version": "0.3.8"
|
|
75
80
|
}
|
package/public/index.html
CHANGED
|
@@ -167,6 +167,14 @@
|
|
|
167
167
|
<div class="nav-section">
|
|
168
168
|
<h2>Other examples</h2>
|
|
169
169
|
<div class="box-grid">
|
|
170
|
+
<!-- The display the Dashboard used before the cards. Kept reachable while people get used
|
|
171
|
+
to the new one; it will go once nothing needs it. -->
|
|
172
|
+
<div class="nav-box">
|
|
173
|
+
<div class="nav-box-background" style="background-image: url('/images/icon-dashboard.png')"></div>
|
|
174
|
+
<div class="nav-box-content">
|
|
175
|
+
<a href="/dashboard/index-old.html">Previous dashboard</a>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
170
178
|
<div class="nav-box">
|
|
171
179
|
<div class="nav-box-background" style="background-image: url('/images/icon-dashboard.png')"></div>
|
|
172
180
|
<div class="nav-box-content">
|
package/public/service-worker.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
1
|
+
// Changing this version is what makes an already installed PWA fetch the app again instead of
|
|
2
|
+
// serving what it cached. It follows the version of frugal-iot-client this release installs, and
|
|
3
|
+
// "npm run prerelease" sets it - so there is normally no reason to edit it by hand.
|
|
4
|
+
const CACHE_NAME = 'frugal-iot-cache-1.3.14';
|
|
4
5
|
const urlsToCache = [
|
|
5
6
|
'/',
|
|
6
7
|
'/index.html',
|