@aaqu/fromcubes-portal-react 0.1.0-alpha.2 → 0.1.0-alpha.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/LICENSE CHANGED
@@ -175,7 +175,7 @@
175
175
 
176
176
  END OF TERMS AND CONDITIONS
177
177
 
178
- Copyright 2026 aaqu
178
+ Copyright 2026 Mazur Albert Fromcubes
179
179
 
180
180
  Licensed under the Apache License, Version 2.0 (the "License");
181
181
  you may not use this file except in compliance with the License.
package/README.md CHANGED
@@ -29,7 +29,7 @@ React portal node for Node-RED. Server-side JSX transpilation via esbuild. Tailw
29
29
 
30
30
  ```bash
31
31
  cd ~/.node-red
32
- npm install /path/to/fromcubes-portal-react
32
+ npm install @aaqu/fromcubes-portal-react@alpha
33
33
  # restart Node-RED
34
34
  ```
35
35
 
@@ -117,4 +117,4 @@ No Babel, no Sucrase client, no Vue, no Vuetify, no Socket.IO.
117
117
 
118
118
  ## License
119
119
 
120
- MIT
120
+ Apache-2.0
@@ -45,7 +45,7 @@
45
45
  var libContent = [
46
46
  "declare var React: any;",
47
47
  "declare var ReactDOM: any;",
48
- "declare function useNodeRed(): { data: any; send: (payload: any, topic?: string) => void };",
48
+ "declare function useNodeRed(): { data: any; send: (payload: any, topic?: string) => void; user: { userId?: string; userName?: string; username?: string; email?: string; role?: string; groups?: any[] } | null };",
49
49
  ].join("\n");
50
50
  console.log(PREFIX, "addExtraLib globals.d.ts");
51
51
  jsDef.addExtraLib(libContent, "file:///globals.d.ts");
@@ -660,7 +660,7 @@
660
660
  <div class="form-row" style="margin-bottom:0;">
661
661
  <div style="font-size:11px;opacity:.6;margin-bottom:4px;">
662
662
  <code>useNodeRed()</code> &rarr;
663
- <code>{ data, send }</code> &nbsp;|&nbsp; Components from
663
+ <code>{ data, send, user }</code> &nbsp;|&nbsp; Components from
664
664
  <code>fc-portal-component</code> nodes auto-imported &nbsp;|&nbsp;
665
665
  Must export <code>&lt;App /&gt;</code> &nbsp;|&nbsp;
666
666
  <strong>Transpiled server-side at deploy</strong>
@@ -711,6 +711,31 @@
711
711
  </div>
712
712
  </div>
713
713
  </div>
714
+ <!-- ── Tab: Auth ── -->
715
+ <div id="fc-tab-auth" class="fc-tab-pane" style="display:none;">
716
+ <div class="form-row">
717
+ <label style="width:auto;">
718
+ <input
719
+ type="checkbox"
720
+ id="node-input-portalAuth"
721
+ style="width:auto;margin:0 8px 0 0;vertical-align:middle;"
722
+ />
723
+ Enable Portal Auth headers
724
+ </label>
725
+ </div>
726
+ <div class="form-row" style="padding-left:4px;">
727
+ <div style="font-size:11px;opacity:.6;line-height:1.5;">
728
+ Read <code>X-Portal-*</code> headers set by Nginx proxy and expose user data
729
+ via <code>useNodeRed()</code>.<br><br>
730
+ When enabled:<br>
731
+ &bull; <code>useNodeRed()</code> returns <code>{ data, send, user }</code> where
732
+ <code>user</code> contains <code>userId</code>, <code>userName</code>,
733
+ <code>username</code>, <code>email</code>, <code>role</code>, <code>groups</code><br>
734
+ &bull; Messages from WebSocket include <code>msg._client</code> with user data<br><br>
735
+ Requires <code>@aaqu/node-red-dashboard-2-portal-auth</code> Nginx setup.
736
+ </div>
737
+ </div>
738
+ </div>
714
739
  </div>
715
740
 
716
741
  <input type="hidden" id="node-input-componentCode" />
@@ -758,6 +783,7 @@
758
783
  pageTitle: { value: "Portal" },
759
784
  componentCode: { value: STARTER },
760
785
  customHead: { value: "" },
786
+ portalAuth: { value: false },
761
787
  },
762
788
  inputs: 1,
763
789
  outputs: 1,
@@ -798,6 +824,7 @@
798
824
  fcTabs.addTab({ id: "fc-tab-jsx", label: "JSX" });
799
825
  fcTabs.addTab({ id: "fc-tab-props", label: "Properties" });
800
826
  fcTabs.addTab({ id: "fc-tab-head", label: "Head HTML" });
827
+ fcTabs.addTab({ id: "fc-tab-auth", label: "Auth" });
801
828
  fcTabs.activateTab("fc-tab-jsx");
802
829
 
803
830
  // URL hint
@@ -1048,6 +1075,17 @@ const { data, send } = useNodeRed();
1048
1075
  by their component name.
1049
1076
  </p>
1050
1077
 
1078
+ <h3>Portal Auth</h3>
1079
+ <p>
1080
+ When enabled in the Auth tab, reads <code>X-Portal-*</code> headers set by
1081
+ an Nginx proxy (via <code>@aaqu/node-red-dashboard-2-portal-auth</code>) and
1082
+ exposes user data:
1083
+ </p>
1084
+ <ul>
1085
+ <li><code>useNodeRed()</code> returns <code>{ data, send, user }</code></li>
1086
+ <li>Messages from WebSocket include <code>msg._client</code> with user info</li>
1087
+ </ul>
1088
+
1051
1089
  <h3>Custom Head HTML</h3>
1052
1090
  <p>
1053
1091
  Inject CDN links, fonts, or extra stylesheets into
@@ -24,6 +24,7 @@ const reactHash = crypto
24
24
  module.exports = function (RED) {
25
25
  // ── Admin root prefix (for correct URLs when httpAdminRoot is set) ──
26
26
  const adminRoot = (RED.settings.httpAdminRoot || "/").replace(/\/$/, "");
27
+ const nodeRoot = (RED.settings.httpNodeRoot || "/").replace(/\/$/, "");
27
28
 
28
29
  // ── Shared state ──────────────────────────────────────────────
29
30
  // Component registry: populated by fc-portal-component canvas nodes at deploy time
@@ -133,6 +134,23 @@ module.exports = function (RED) {
133
134
  );
134
135
  }
135
136
 
137
+ function extractPortalUser(headers) {
138
+ const user = {};
139
+ if (headers["x-portal-user-id"]) user.userId = headers["x-portal-user-id"];
140
+ if (headers["x-portal-user-name"]) user.userName = headers["x-portal-user-name"];
141
+ if (headers["x-portal-user-username"]) user.username = headers["x-portal-user-username"];
142
+ if (headers["x-portal-user-email"]) user.email = headers["x-portal-user-email"];
143
+ if (headers["x-portal-user-role"]) user.role = headers["x-portal-user-role"];
144
+ if (headers["x-portal-user-groups"]) {
145
+ try {
146
+ user.groups = JSON.parse(headers["x-portal-user-groups"]);
147
+ } catch (_) {
148
+ user.groups = headers["x-portal-user-groups"];
149
+ }
150
+ }
151
+ return Object.keys(user).length > 0 ? user : null;
152
+ }
153
+
136
154
  function removeRoute(router, path) {
137
155
  if (!router || !router.stack) return;
138
156
  router.stack = router.stack.filter(
@@ -195,6 +213,7 @@ module.exports = function (RED) {
195
213
  const componentCode = config.componentCode || "";
196
214
  const pageTitle = config.pageTitle || "Portal";
197
215
  const customHead = config.customHead || "";
216
+ const portalAuth = config.portalAuth === true;
198
217
 
199
218
  // State
200
219
  const clients = new Set();
@@ -202,7 +221,7 @@ module.exports = function (RED) {
202
221
  let wsServer = null;
203
222
  let isClosing = false;
204
223
 
205
- const wsPath = endpoint + "/_ws";
224
+ const wsPath = nodeRoot + endpoint + "/_ws";
206
225
 
207
226
  // ── Rebuild: transpile JSX + update page state ────────────
208
227
 
@@ -237,7 +256,8 @@ module.exports = function (RED) {
237
256
  const send = React.useCallback((payload, topic) => {
238
257
  window.__NR.send(payload, topic);
239
258
  }, []);
240
- return { data, send };
259
+ const user = window.__NR._user || null;
260
+ return { data, send, user };
241
261
  }`,
242
262
  "",
243
263
  "// ── Library components ──",
@@ -277,6 +297,7 @@ module.exports = function (RED) {
277
297
  pageTitle,
278
298
  wsPath,
279
299
  customHead,
300
+ portalAuth,
280
301
  };
281
302
  }
282
303
 
@@ -304,6 +325,7 @@ module.exports = function (RED) {
304
325
  return;
305
326
  }
306
327
  const cssHash = await state.cssHashReady;
328
+ const user = state.portalAuth ? extractPortalUser(_req.headers) : null;
307
329
  res
308
330
  .type("text/html")
309
331
  .send(
@@ -313,6 +335,7 @@ module.exports = function (RED) {
313
335
  state.wsPath,
314
336
  state.customHead,
315
337
  cssHash,
338
+ user,
316
339
  ),
317
340
  );
318
341
  });
@@ -350,11 +373,14 @@ module.exports = function (RED) {
350
373
  RED.server.on("upgrade", onUpgrade);
351
374
  upgradeHandlers[nodeId] = onUpgrade;
352
375
 
353
- wsServer.on("connection", (ws) => {
376
+ wsServer.on("connection", (ws, request) => {
354
377
  if (isClosing) {
355
378
  ws.close();
356
379
  return;
357
380
  }
381
+ if (portalAuth) {
382
+ ws._portalUser = extractPortalUser(request.headers);
383
+ }
358
384
  clients.add(ws);
359
385
  updateStatus();
360
386
 
@@ -367,10 +393,14 @@ module.exports = function (RED) {
367
393
  try {
368
394
  const msg = JSON.parse(raw.toString());
369
395
  if (msg.type === "output") {
370
- node.send({
396
+ const out = {
371
397
  payload: msg.payload,
372
398
  topic: msg.topic || "",
373
- });
399
+ };
400
+ if (portalAuth && ws._portalUser) {
401
+ out._client = ws._portalUser;
402
+ }
403
+ node.send(out);
374
404
  }
375
405
  } catch (e) {
376
406
  node.warn("Bad WS message: " + e.message);
@@ -533,7 +563,7 @@ module.exports = function (RED) {
533
563
 
534
564
  // ── Page builders ─────────────────────────────────────────────
535
565
 
536
- function buildPage(title, transpiledJs, wsPath, customHead, cssHash) {
566
+ function buildPage(title, transpiledJs, wsPath, customHead, cssHash, user) {
537
567
  return `<!DOCTYPE html>
538
568
  <html lang="en">
539
569
  <head>
@@ -560,7 +590,7 @@ body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a0a;color:#e0
560
590
  <div id="__cs" class="err">disconnected</div>
561
591
  <script>
562
592
  window.__NR={
563
- _ws:null,_listeners:new Set(),_lastData:null,_retries:0,_wasConnected:false,
593
+ _ws:null,_listeners:new Set(),_lastData:null,_retries:0,_wasConnected:false,_user:${user ? escScript(JSON.stringify(user)) : 'null'},
564
594
  connect(){
565
595
  const p=location.protocol==='https:'?'wss:':'ws:';
566
596
  const ws=new WebSocket(p+'//'+location.host+'${wsPath}');
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "@aaqu/fromcubes-portal-react",
3
- "version": "0.1.0-alpha.2",
4
- "description": "Fromcubes Portal - React for Node-RED",
3
+ "version": "0.1.0-alpha.4",
4
+ "description": "Fromcubes Portal - React for Node-RED with Tailwind CSS and auto complete",
5
5
  "keywords": [
6
6
  "node-red",
7
- "react",
8
7
  "dashboard",
9
8
  "portal",
10
9
  "jsx",