node-red-contrib-alarm-ultimate 1.0.1 → 1.0.2
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/CHANGELOG.MD +9 -0
- package/nodes/AlarmSystemUltimate.html +25 -2
- package/nodes/AlarmSystemUltimate.js +35 -4
- package/nodes/AlarmUltimateSiren.html +23 -1
- package/nodes/AlarmUltimateState.html +23 -1
- package/nodes/AlarmUltimateZone.html +23 -1
- package/package.json +1 -1
- package/tools/alarm-panel.html +22 -3
- package/tools/assets/alarm-vue-shell.js +1 -1
package/CHANGELOG.MD
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
> Stable release track from `0.3.0`.
|
|
4
4
|
> Previous pre-release history is preserved below.
|
|
5
5
|
|
|
6
|
+
## [1.0.2] - 2026-04-08
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **Tools + adminAuth (OAuth):** fixed `401/Unauthorized` when opening tools pages from the editor with protected Node-RED admin access.
|
|
11
|
+
- **OAuth token handling:** tools now support `access_token` propagation from editor links and keep the token while navigating between tool pages.
|
|
12
|
+
- **Bearer parsing conflict:** fixed `400 Bad Request` when both query `access_token` and `Authorization` header were present by normalizing auth before permission checks.
|
|
13
|
+
- **Control Panel auth fallback:** improved token lookup to support Node-RED token storage keys with `httpAdminRoot` suffix.
|
|
14
|
+
|
|
6
15
|
## [1.0.1] - 2026-04-07
|
|
7
16
|
|
|
8
17
|
### Changed
|
|
@@ -267,13 +267,35 @@
|
|
|
267
267
|
zoneAxProMatchRow.toggle(adapter === "axpro");
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
function getEditorAccessToken() {
|
|
271
|
+
try {
|
|
272
|
+
const tokens =
|
|
273
|
+
RED && RED.settings && typeof RED.settings.get === "function"
|
|
274
|
+
? RED.settings.get("auth-tokens")
|
|
275
|
+
: null;
|
|
276
|
+
return tokens && typeof tokens.access_token === "string"
|
|
277
|
+
? tokens.access_token.trim()
|
|
278
|
+
: "";
|
|
279
|
+
} catch (_err) {
|
|
280
|
+
return "";
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function withAccessToken(url) {
|
|
285
|
+
const token = getEditorAccessToken();
|
|
286
|
+
if (!token) return url;
|
|
287
|
+
const sep = url.includes("?") ? "&" : "?";
|
|
288
|
+
return `${url}${sep}access_token=${encodeURIComponent(token)}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
270
291
|
function openZonesManager() {
|
|
271
292
|
const httpAdminRoot = (RED.settings && RED.settings.httpAdminRoot) || "/";
|
|
272
293
|
const root = httpAdminRoot.endsWith("/") ? httpAdminRoot : `${httpAdminRoot}/`;
|
|
273
294
|
const currentName = String($("#node-input-name").val() || "").trim();
|
|
274
295
|
const namePart = currentName ? `&name=${encodeURIComponent(currentName)}` : "";
|
|
275
296
|
const idPart = nodeId ? `?id=${encodeURIComponent(nodeId)}${namePart}` : "";
|
|
276
|
-
|
|
297
|
+
const targetUrl = `${root}alarm-ultimate/alarm-json-mapper${idPart}`;
|
|
298
|
+
window.open(withAccessToken(targetUrl), "_blank");
|
|
277
299
|
}
|
|
278
300
|
|
|
279
301
|
$("#node-input-zones-panel").on("click", function (evt) {
|
|
@@ -281,8 +303,9 @@
|
|
|
281
303
|
const httpAdminRoot = (RED.settings && RED.settings.httpAdminRoot) || "/";
|
|
282
304
|
const root = httpAdminRoot.endsWith("/") ? httpAdminRoot : `${httpAdminRoot}/`;
|
|
283
305
|
const idPart = nodeId ? `?id=${encodeURIComponent(nodeId)}` : "";
|
|
306
|
+
const targetUrl = `${root}alarm-ultimate/alarm-panel${idPart}`;
|
|
284
307
|
window.open(
|
|
285
|
-
|
|
308
|
+
withAccessToken(targetUrl),
|
|
286
309
|
"_blank",
|
|
287
310
|
"noopener,noreferrer",
|
|
288
311
|
);
|
|
@@ -86,6 +86,37 @@ module.exports = function (RED) {
|
|
|
86
86
|
? RED.auth.needsPermission('AlarmSystemUltimate.write')
|
|
87
87
|
: (req, res, next) => next();
|
|
88
88
|
|
|
89
|
+
function applyAccessTokenFromQuery(req) {
|
|
90
|
+
const token =
|
|
91
|
+
req &&
|
|
92
|
+
req.query &&
|
|
93
|
+
typeof req.query.access_token === 'string' &&
|
|
94
|
+
req.query.access_token.trim().length > 0
|
|
95
|
+
? req.query.access_token.trim()
|
|
96
|
+
: '';
|
|
97
|
+
if (!token) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!req.headers || typeof req.headers !== 'object') {
|
|
101
|
+
req.headers = {};
|
|
102
|
+
}
|
|
103
|
+
const authHeader =
|
|
104
|
+
typeof req.headers.authorization === 'string' ? req.headers.authorization.trim() : '';
|
|
105
|
+
if (!authHeader && token) {
|
|
106
|
+
req.headers.authorization = `Bearer ${token}`;
|
|
107
|
+
}
|
|
108
|
+
// passport-http-bearer returns 400 if token is present in both header and query.
|
|
109
|
+
// Normalize to header-only for this request.
|
|
110
|
+
if (req.query && Object.prototype.hasOwnProperty.call(req.query, 'access_token')) {
|
|
111
|
+
delete req.query.access_token;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function needsReadWithQueryToken(req, res, next) {
|
|
116
|
+
applyAccessTokenFromQuery(req);
|
|
117
|
+
return needsRead(req, res, next);
|
|
118
|
+
}
|
|
119
|
+
|
|
89
120
|
function sendToolFile(res, filename) {
|
|
90
121
|
const filePath = path.join(__dirname, '..', 'tools', filename);
|
|
91
122
|
res.set('Cache-Control', 'no-store, max-age=0');
|
|
@@ -113,19 +144,19 @@ module.exports = function (RED) {
|
|
|
113
144
|
});
|
|
114
145
|
}
|
|
115
146
|
|
|
116
|
-
RED.httpAdmin.get('/alarm-ultimate/alarm-json-mapper',
|
|
147
|
+
RED.httpAdmin.get('/alarm-ultimate/alarm-json-mapper', needsReadWithQueryToken, (req, res) => {
|
|
117
148
|
sendToolFile(res, 'alarm-json-mapper.html');
|
|
118
149
|
});
|
|
119
150
|
|
|
120
|
-
RED.httpAdmin.get('/alarm-ultimate/alarm-panel',
|
|
151
|
+
RED.httpAdmin.get('/alarm-ultimate/alarm-panel', needsReadWithQueryToken, (req, res) => {
|
|
121
152
|
sendToolFile(res, 'alarm-panel.html');
|
|
122
153
|
});
|
|
123
154
|
|
|
124
|
-
RED.httpAdmin.get('/alarm-ultimate/alarm-settings',
|
|
155
|
+
RED.httpAdmin.get('/alarm-ultimate/alarm-settings', needsReadWithQueryToken, (req, res) => {
|
|
125
156
|
sendToolFile(res, 'alarm-settings.html');
|
|
126
157
|
});
|
|
127
158
|
|
|
128
|
-
RED.httpAdmin.get('/alarm-ultimate/alarm-tools/assets/:file',
|
|
159
|
+
RED.httpAdmin.get('/alarm-ultimate/alarm-tools/assets/:file', (req, res) => {
|
|
129
160
|
sendToolAssetFile(res, req.params.file);
|
|
130
161
|
});
|
|
131
162
|
|
|
@@ -31,12 +31,34 @@
|
|
|
31
31
|
const url = `${root}alarm-ultimate/alarm/nodes`;
|
|
32
32
|
const panelButton = $("#node-input-alarm-panel");
|
|
33
33
|
|
|
34
|
+
function getEditorAccessToken() {
|
|
35
|
+
try {
|
|
36
|
+
const tokens =
|
|
37
|
+
RED && RED.settings && typeof RED.settings.get === "function"
|
|
38
|
+
? RED.settings.get("auth-tokens")
|
|
39
|
+
: null;
|
|
40
|
+
return tokens && typeof tokens.access_token === "string"
|
|
41
|
+
? tokens.access_token.trim()
|
|
42
|
+
: "";
|
|
43
|
+
} catch (_err) {
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function withAccessToken(urlValue) {
|
|
49
|
+
const token = getEditorAccessToken();
|
|
50
|
+
if (!token) return urlValue;
|
|
51
|
+
const sep = urlValue.includes("?") ? "&" : "?";
|
|
52
|
+
return `${urlValue}${sep}access_token=${encodeURIComponent(token)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
34
55
|
panelButton.off("click").on("click", (evt) => {
|
|
35
56
|
evt.preventDefault();
|
|
36
57
|
const alarmId = alarmSelect.val() || this.alarmId || "";
|
|
37
58
|
const idPart = alarmId ? `?id=${encodeURIComponent(alarmId)}` : "";
|
|
59
|
+
const targetUrl = `${root}alarm-ultimate/alarm-panel${idPart}`;
|
|
38
60
|
window.open(
|
|
39
|
-
|
|
61
|
+
withAccessToken(targetUrl),
|
|
40
62
|
"_blank",
|
|
41
63
|
"noopener,noreferrer",
|
|
42
64
|
);
|
|
@@ -40,6 +40,27 @@
|
|
|
40
40
|
const topicRow = $("#au-row-topic");
|
|
41
41
|
const initRow = $("#au-row-outputInitialState");
|
|
42
42
|
|
|
43
|
+
function getEditorAccessToken() {
|
|
44
|
+
try {
|
|
45
|
+
const tokens =
|
|
46
|
+
RED && RED.settings && typeof RED.settings.get === "function"
|
|
47
|
+
? RED.settings.get("auth-tokens")
|
|
48
|
+
: null;
|
|
49
|
+
return tokens && typeof tokens.access_token === "string"
|
|
50
|
+
? tokens.access_token.trim()
|
|
51
|
+
: "";
|
|
52
|
+
} catch (_err) {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function withAccessToken(urlValue) {
|
|
58
|
+
const token = getEditorAccessToken();
|
|
59
|
+
if (!token) return urlValue;
|
|
60
|
+
const sep = urlValue.includes("?") ? "&" : "?";
|
|
61
|
+
return `${urlValue}${sep}access_token=${encodeURIComponent(token)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
43
64
|
function refreshVisibility() {
|
|
44
65
|
const mode = String(ioSelect.val() || "out");
|
|
45
66
|
const isOut = mode === "out";
|
|
@@ -51,8 +72,9 @@
|
|
|
51
72
|
evt.preventDefault();
|
|
52
73
|
const alarmId = alarmSelect.val() || this.alarmId || "";
|
|
53
74
|
const idPart = alarmId ? `?id=${encodeURIComponent(alarmId)}` : "";
|
|
75
|
+
const targetUrl = `${root}alarm-ultimate/alarm-panel${idPart}`;
|
|
54
76
|
window.open(
|
|
55
|
-
|
|
77
|
+
withAccessToken(targetUrl),
|
|
56
78
|
"_blank",
|
|
57
79
|
"noopener,noreferrer",
|
|
58
80
|
);
|
|
@@ -38,6 +38,27 @@
|
|
|
38
38
|
const root = httpAdminRoot.endsWith("/") ? httpAdminRoot : `${httpAdminRoot}/`;
|
|
39
39
|
const panelButton = $("#node-input-alarm-panel");
|
|
40
40
|
|
|
41
|
+
function getEditorAccessToken() {
|
|
42
|
+
try {
|
|
43
|
+
const tokens =
|
|
44
|
+
RED && RED.settings && typeof RED.settings.get === "function"
|
|
45
|
+
? RED.settings.get("auth-tokens")
|
|
46
|
+
: null;
|
|
47
|
+
return tokens && typeof tokens.access_token === "string"
|
|
48
|
+
? tokens.access_token.trim()
|
|
49
|
+
: "";
|
|
50
|
+
} catch (_err) {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function withAccessToken(url) {
|
|
56
|
+
const token = getEditorAccessToken();
|
|
57
|
+
if (!token) return url;
|
|
58
|
+
const sep = url.includes("?") ? "&" : "?";
|
|
59
|
+
return `${url}${sep}access_token=${encodeURIComponent(token)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
41
62
|
function apiUrl(path) {
|
|
42
63
|
return `${root}${path.replace(/^\//, "")}`;
|
|
43
64
|
}
|
|
@@ -46,8 +67,9 @@
|
|
|
46
67
|
evt.preventDefault();
|
|
47
68
|
const alarmId = alarmSelect.val() || this.alarmId || "";
|
|
48
69
|
const idPart = alarmId ? `?id=${encodeURIComponent(alarmId)}` : "";
|
|
70
|
+
const targetUrl = `${root}alarm-ultimate/alarm-panel${idPart}`;
|
|
49
71
|
window.open(
|
|
50
|
-
|
|
72
|
+
withAccessToken(targetUrl),
|
|
51
73
|
"_blank",
|
|
52
74
|
"noopener,noreferrer",
|
|
53
75
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-alarm-ultimate",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Alarm System node for Node-RED. Integrates also with Home Assistant, MQTT, KNX-Ultimate. Completed with web interface for fast configuration and control. With zone import wizard.",
|
|
5
5
|
"author": "Massimo Saccani (https://github.com/Supergiovane)",
|
|
6
6
|
"license": "MIT",
|
package/tools/alarm-panel.html
CHANGED
|
@@ -450,9 +450,28 @@
|
|
|
450
450
|
|
|
451
451
|
function authHeaders() {
|
|
452
452
|
try {
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
453
|
+
const tokenFromQuery =
|
|
454
|
+
params && typeof params.get === "function" ? String(params.get("access_token") || "").trim() : "";
|
|
455
|
+
if (tokenFromQuery) {
|
|
456
|
+
return { Authorization: `Bearer ${tokenFromQuery}` };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const root = httpAdminRoot();
|
|
460
|
+
const rootNoSlash = root.endsWith("/") ? root.slice(0, -1) : root;
|
|
461
|
+
const suffix = rootNoSlash ? rootNoSlash.replace(/\//g, "-") : "";
|
|
462
|
+
const candidates = [`auth-tokens${suffix}`, "auth-tokens"];
|
|
463
|
+
let tokens = null;
|
|
464
|
+
|
|
465
|
+
for (const key of candidates) {
|
|
466
|
+
const raw = localStorage.getItem(key);
|
|
467
|
+
if (!raw) continue;
|
|
468
|
+
const parsed = JSON.parse(raw);
|
|
469
|
+
if (parsed && parsed.access_token) {
|
|
470
|
+
tokens = parsed;
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
456
475
|
if (!tokens || !tokens.access_token) return {};
|
|
457
476
|
return { Authorization: `Bearer ${tokens.access_token}` };
|
|
458
477
|
} catch (err) {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
function computeTargetUrl(root, page, params, sourceParams) {
|
|
28
28
|
const target = new URLSearchParams();
|
|
29
|
-
const copyKeys = ['id', 'name', 'embed'];
|
|
29
|
+
const copyKeys = ['id', 'name', 'embed', 'access_token'];
|
|
30
30
|
for (const key of copyKeys) {
|
|
31
31
|
const value = asText(params.get(key));
|
|
32
32
|
if (value) target.set(key, value);
|