node-red-contrib-room-tools 1.8.3 → 1.8.4
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/README.md +2 -0
- package/gateway.cjs +3 -3
- package/package.json +1 -1
- package/panel-editor-auth.js +4 -11
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ This package provides `ui-control-panel` for Node-RED and MUSE Automator.
|
|
|
6
6
|
|
|
7
7
|
The page author writes ordinary HTML/CSS/Vue and pastes it into the `ui-control-panel` Template. After configuring a Dashboard page, room ID, control login, and separate editor login, save and Deploy. Reopen the node and select **Open page binding editor**. Sign in with the editor account, right-click a supported control, choose its device points, and Apply.
|
|
8
8
|
|
|
9
|
+
Both logins work over HTTP on a trusted local network. HTTPS is recommended for other networks because HTTP transmits credentials without encryption. Session cookies are marked `Secure` when the connection uses HTTPS.
|
|
10
|
+
|
|
9
11
|
Bindings are stored in `visualBindings` on the node. The runtime reads that mapping to authenticate commands, route feedback, and generate a native bridge. The page author does not put device paths, `data-cp-*` attributes, or device-send code in the Template. Author-provided `data-cp-*` attributes are rejected.
|
|
10
12
|
|
|
11
13
|
Supported controls include native range, checkbox, semantic pressed button, select, text input, meter, progress, and simple text display. Give repeated controls distinct standard IDs. Simple top-level Vue `v-model` works; controls with existing send handlers or ambiguous dynamic behavior are rejected until a suitable adapter exists. See `CONTROL_PANEL_AI_GUIDE.md` for examples and limits.
|
package/gateway.cjs
CHANGED
|
@@ -292,7 +292,7 @@ function createGateway ({
|
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
const cookieName = 'room_panel_' + Buffer.from(room).toString('hex')
|
|
295
|
-
const cookie = (token, maxAge) => `${cookieName}=${token}; Path=${routePrefix}; HttpOnly; SameSite=Strict; Max-Age=${maxAge}${secureCookie ? '; Secure' : ''}`
|
|
295
|
+
const cookie = (token, maxAge, req) => `${cookieName}=${token}; Path=${routePrefix}; HttpOnly; SameSite=Strict; Max-Age=${maxAge}${secureCookie && req.socket.encrypted ? '; Secure' : ''}`
|
|
296
296
|
|
|
297
297
|
async function handler (req, res) {
|
|
298
298
|
const path = new URL(req.url, 'http://localhost').pathname
|
|
@@ -353,7 +353,7 @@ function createGateway ({
|
|
|
353
353
|
const csrf = crypto.randomBytes(24).toString('hex')
|
|
354
354
|
sessions.set(token, { csrf, expiry: now + sessionTtlMs })
|
|
355
355
|
reply(res, 200, { room, username, csrf }, {
|
|
356
|
-
'Set-Cookie': cookie(token, Math.ceil(sessionTtlMs / 1000))
|
|
356
|
+
'Set-Cookie': cookie(token, Math.ceil(sessionTtlMs / 1000), req)
|
|
357
357
|
})
|
|
358
358
|
if (boundOnly && typeof onBoundLogin === 'function') {
|
|
359
359
|
queueMicrotask(() => { if (!closed) onBoundLogin({ room, user: username }) })
|
|
@@ -405,7 +405,7 @@ function createGateway ({
|
|
|
405
405
|
if (path === routePrefix + '/logout' && req.method === 'POST') {
|
|
406
406
|
sessions.delete(token)
|
|
407
407
|
closeSubscribersForToken(token)
|
|
408
|
-
reply(res, 200, { ok: true }, { 'Set-Cookie': cookie('', 0) })
|
|
408
|
+
reply(res, 200, { ok: true }, { 'Set-Cookie': cookie('', 0, req) })
|
|
409
409
|
return true
|
|
410
410
|
}
|
|
411
411
|
if (path === routePrefix + '/bound-command' && req.method === 'POST') {
|
package/package.json
CHANGED
package/panel-editor-auth.js
CHANGED
|
@@ -30,8 +30,8 @@ function createPanelEditor ({ panelId, username, password, secureCookie, getConf
|
|
|
30
30
|
res.end(JSON.stringify(data))
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function cookie (value, age) {
|
|
34
|
-
return `${cookieName}=${value}; Path=${prefix}; HttpOnly; SameSite=Strict; Max-Age=${age}${secureCookie ? '; Secure' : ''}`
|
|
33
|
+
function cookie (value, age, req) {
|
|
34
|
+
return `${cookieName}=${value}; Path=${prefix}; HttpOnly; SameSite=Strict; Max-Age=${age}${secureCookie && req.socket.encrypted ? '; Secure' : ''}`
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function sameOrigin (req) {
|
|
@@ -44,12 +44,6 @@ function createPanelEditor ({ panelId, username, password, secureCookie, getConf
|
|
|
44
44
|
} catch (_) { return false }
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function localOrSecure (req) {
|
|
48
|
-
const address = String(req.socket.remoteAddress || '')
|
|
49
|
-
return ['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(address) ||
|
|
50
|
-
req.socket.encrypted === true
|
|
51
|
-
}
|
|
52
|
-
|
|
53
47
|
async function jsonBody (req) {
|
|
54
48
|
if (!String(req.headers['content-type'] || '').startsWith('application/json')) {
|
|
55
49
|
throw new Error('JSON body required')
|
|
@@ -71,7 +65,6 @@ function createPanelEditor ({ panelId, username, password, secureCookie, getConf
|
|
|
71
65
|
const path = new URL(req.url, 'http://localhost').pathname
|
|
72
66
|
if (!path.startsWith(prefix + '/')) return false
|
|
73
67
|
if (closed) { reply(res, 503, { error: 'Editor unavailable' }); return true }
|
|
74
|
-
if (!localOrSecure(req)) { reply(res, 403, { error: 'HTTPS required' }); return true }
|
|
75
68
|
if (req.method === 'POST' && !sameOrigin(req)) {
|
|
76
69
|
reply(res, 403, { error: 'Origin rejected' })
|
|
77
70
|
return true
|
|
@@ -103,7 +96,7 @@ function createPanelEditor ({ panelId, username, password, secureCookie, getConf
|
|
|
103
96
|
const token = crypto.randomBytes(32).toString('hex')
|
|
104
97
|
const csrf = crypto.randomBytes(24).toString('hex')
|
|
105
98
|
sessions.set(token, { csrf, expiry: now + ttl })
|
|
106
|
-
reply(res, 200, { csrf }, { 'Set-Cookie': cookie(token, ttl / 1000) })
|
|
99
|
+
reply(res, 200, { csrf }, { 'Set-Cookie': cookie(token, ttl / 1000, req) })
|
|
107
100
|
return true
|
|
108
101
|
}
|
|
109
102
|
const token = String(req.headers.cookie || '').split(';').map(part => part.trim())
|
|
@@ -118,7 +111,7 @@ function createPanelEditor ({ panelId, username, password, secureCookie, getConf
|
|
|
118
111
|
reply(res, 200, { csrf: session.csrf })
|
|
119
112
|
} else if (path === prefix + '/logout' && req.method === 'POST') {
|
|
120
113
|
sessions.delete(token)
|
|
121
|
-
reply(res, 200, { ok: true }, { 'Set-Cookie': cookie('', 0) })
|
|
114
|
+
reply(res, 200, { ok: true }, { 'Set-Cookie': cookie('', 0, req) })
|
|
122
115
|
} else if (path === prefix + '/config' && req.method === 'GET') {
|
|
123
116
|
reply(res, 200, await getConfig(req))
|
|
124
117
|
} else if (path === prefix + '/devices' && req.method === 'GET') {
|