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 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
- window.open(`${root}alarm-ultimate/alarm-json-mapper${idPart}`, "_blank");
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
- `${root}alarm-ultimate/alarm-panel${idPart}`,
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', needsRead, (req, res) => {
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', needsRead, (req, res) => {
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', needsRead, (req, res) => {
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', needsRead, (req, res) => {
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
- `${root}alarm-ultimate/alarm-panel${idPart}`,
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
- `${root}alarm-ultimate/alarm-panel${idPart}`,
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
- `${root}alarm-ultimate/alarm-panel${idPart}`,
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.1",
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",
@@ -450,9 +450,28 @@
450
450
 
451
451
  function authHeaders() {
452
452
  try {
453
- const raw = localStorage.getItem("auth-tokens");
454
- if (!raw) return {};
455
- const tokens = JSON.parse(raw);
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);