@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 +1 -1
- package/README.md +2 -2
- package/nodes/portal-react.html +40 -2
- package/nodes/portal-react.js +37 -7
- package/package.json +2 -3
package/LICENSE
CHANGED
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 /
|
|
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
|
-
|
|
120
|
+
Apache-2.0
|
package/nodes/portal-react.html
CHANGED
|
@@ -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> →
|
|
663
|
-
<code>{ data, send }</code> | Components from
|
|
663
|
+
<code>{ data, send, user }</code> | Components from
|
|
664
664
|
<code>fc-portal-component</code> nodes auto-imported |
|
|
665
665
|
Must export <code><App /></code> |
|
|
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
|
+
• <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
|
+
• 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
|
package/nodes/portal-react.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|