4bnode 4.1.5 → 4.1.6

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.
Files changed (68) hide show
  1. package/add-api.js +5 -3
  2. package/add-email.js +118 -28
  3. package/add-login.js +35 -16
  4. package/add-mongo-schema.js +13 -5
  5. package/lib/codegen.js +191 -57
  6. package/lib/codegen.test.js +87 -15
  7. package/lib/names.js +1 -1
  8. package/package.json +1 -1
  9. package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +54 -9
  10. package/skeleton/.4bnode/assets/js/pages/LogsPage.js +49 -16
  11. package/skeleton/.4bnode/assets/js/pages/MailPage.js +110 -50
  12. package/skeleton/.4bnode/codegen.js +191 -57
  13. package/skeleton/.4bnode/dev-api.js +284 -56
  14. package/skeleton/.env.example +17 -3
  15. package/skeleton/_gitignore +1 -0
  16. package/skeleton/index.js +7 -2
  17. package/skeleton/src/middleware/apiKey.js +14 -1
  18. package/skeleton/src/middleware/errorHandler.js +7 -2
  19. package/app-1/.4bnode/.passkey +0 -1
  20. package/app-1/.4bnode/assets/fonts/NotoSans-Bold.woff2 +0 -0
  21. package/app-1/.4bnode/assets/fonts/NotoSans-Light.woff2 +0 -0
  22. package/app-1/.4bnode/assets/fonts/NotoSans-Medium.woff2 +0 -0
  23. package/app-1/.4bnode/assets/fonts/NotoSans-Regular.woff2 +0 -0
  24. package/app-1/.4bnode/assets/fonts/NotoSans-SemiBold.woff2 +0 -0
  25. package/app-1/.4bnode/assets/images/mongo.png +0 -0
  26. package/app-1/.4bnode/assets/js/App.js +0 -139
  27. package/app-1/.4bnode/assets/js/Root.js +0 -40
  28. package/app-1/.4bnode/assets/js/components/Modal.js +0 -52
  29. package/app-1/.4bnode/assets/js/components/Sidebar.js +0 -73
  30. package/app-1/.4bnode/assets/js/components/ToastContainer.js +0 -15
  31. package/app-1/.4bnode/assets/js/context.js +0 -7
  32. package/app-1/.4bnode/assets/js/lib.js +0 -113
  33. package/app-1/.4bnode/assets/js/main.js +0 -5
  34. package/app-1/.4bnode/assets/js/pages/AiBuilderPage.js +0 -268
  35. package/app-1/.4bnode/assets/js/pages/ApiTesterPage.js +0 -1879
  36. package/app-1/.4bnode/assets/js/pages/DashboardPage.js +0 -435
  37. package/app-1/.4bnode/assets/js/pages/DatabasePage.js +0 -300
  38. package/app-1/.4bnode/assets/js/pages/DocsPage.js +0 -150
  39. package/app-1/.4bnode/assets/js/pages/EnvironmentPage.js +0 -103
  40. package/app-1/.4bnode/assets/js/pages/LogsPage.js +0 -205
  41. package/app-1/.4bnode/assets/js/pages/MailPage.js +0 -224
  42. package/app-1/.4bnode/assets/js/pages/RealtimePage.js +0 -186
  43. package/app-1/.4bnode/assets/js/pages/RoutesPage.js +0 -1485
  44. package/app-1/.4bnode/assets/js/pages/SchemasPage.js +0 -838
  45. package/app-1/.4bnode/assets/js/pages/SecurityPage.js +0 -74
  46. package/app-1/.4bnode/assets/js/pages/SerialPortPage.js +0 -170
  47. package/app-1/.4bnode/assets/js/screens/PasskeyScreen.js +0 -105
  48. package/app-1/.4bnode/codegen.js +0 -1440
  49. package/app-1/.4bnode/dev-api.js +0 -3747
  50. package/app-1/.4bnode/ui.html +0 -1578
  51. package/app-1/.env.example +0 -11
  52. package/app-1/README.md +0 -47
  53. package/app-1/ecosystem.config.cjs +0 -25
  54. package/app-1/index.js +0 -188
  55. package/app-1/package.json +0 -37
  56. package/app-1/public/index.html +0 -292
  57. package/app-1/src/db.js +0 -13
  58. package/app-1/src/docs.js +0 -84
  59. package/app-1/src/middleware/apiKey.js +0 -36
  60. package/app-1/src/middleware/auth.js +0 -17
  61. package/app-1/src/middleware/errorHandler.js +0 -15
  62. package/app-1/src/middleware/roles.js +0 -14
  63. package/app-1/src/middleware/validate.js +0 -20
  64. package/app-1/src/models/user.js +0 -11
  65. package/app-1/src/openapi.json +0 -80
  66. package/app-1/src/routes/upload-resume.js +0 -21
  67. package/app-1/src/services/mailer.js +0 -52
  68. package/app-1/src/validators/upload-resume.js +0 -8
@@ -1,74 +0,0 @@
1
- import React, { useState, useEffect } from "react";
2
- import { LuCheck, LuGauge } from "react-icons/lu";
3
- import { html, api } from "../lib.js";
4
- import { useApp } from "../context.js";
5
-
6
- export function SecurityPage() {
7
- const { toast } = useApp();
8
-
9
- const [rl, setRl] = useState(null);
10
- const [savingRl, setSavingRl] = useState(false);
11
-
12
- useEffect(() => {
13
- api("/security/rate-limit")
14
- .then((r) => setRl({ windowMinutes: r.windowMinutes, max: r.max }))
15
- .catch(() => setRl({ windowMinutes: 15, max: 500 }));
16
- }, []);
17
-
18
- async function saveRl() {
19
- setSavingRl(true);
20
- try {
21
- await api("/security/rate-limit", {
22
- method: "POST",
23
- body: { windowMinutes: Number(rl.windowMinutes), max: Number(rl.max) },
24
- });
25
- toast("Rate limit saved — restart the app to apply.", "success");
26
- } catch (err) {
27
- toast(err.message, "error");
28
- }
29
- setSavingRl(false);
30
- }
31
-
32
- return html`<div>
33
- <div className="mb-4">
34
- <div className="card-title">Security</div>
35
- <div className="card-subtitle">
36
- <b>Security is built in</b> — Helmet, rate limiting, CORS, error handling, RBAC, and Zod
37
- validation ship with every app. Validation is generated per route from each route's own
38
- fields when you create it. Adjust rate limiting below.
39
- </div>
40
- </div>
41
-
42
- <div className="card">
43
- <div className="flex items-center gap-2 mb-1">
44
- <${LuGauge} size=${16} />
45
- <div className="card-title" style=${{ margin: 0 }}>Rate limiting</div>
46
- </div>
47
- <div className="card-subtitle mb-4">
48
- Throttle requests per IP. Applies to your API routes (the <span style=${{ fontFamily: "monospace" }}>/_dev</span> dashboard is exempt).
49
- </div>
50
- ${rl === null
51
- ? html`<div className="text-muted text-sm">Loading…</div>`
52
- : html`<div style=${{ display: "flex", gap: "16px", alignItems: "flex-end", flexWrap: "wrap" }}>
53
- <div className="form-group" style=${{ marginBottom: 0 }}>
54
- <label className="form-label">Window (minutes)</label>
55
- <input className="form-input" type="number" min="1" value=${rl.windowMinutes}
56
- onInput=${(e) => setRl((s) => ({ ...s, windowMinutes: e.target.value }))}
57
- style=${{ width: "140px" }} />
58
- </div>
59
- <div className="form-group" style=${{ marginBottom: 0 }}>
60
- <label className="form-label">Max requests / window</label>
61
- <input className="form-input" type="number" min="1" value=${rl.max}
62
- onInput=${(e) => setRl((s) => ({ ...s, max: e.target.value }))}
63
- style=${{ width: "180px" }} />
64
- </div>
65
- <button className="btn btn-primary" onClick=${saveRl} disabled=${savingRl}>
66
- ${savingRl ? html`<span className="spinner" />` : html`<${LuCheck} size=${15} />`} Save
67
- </button>
68
- </div>
69
- <div className="text-sm text-muted" style=${{ marginTop: "10px" }}>
70
- Restart the app for changes to take effect (values are read from <span style=${{ fontFamily: "monospace" }}>.env</span> at startup).
71
- </div>`}
72
- </div>
73
- </div>`;
74
- }
@@ -1,170 +0,0 @@
1
- import React, { useState, useEffect } from "react";
2
- import { LuCable } from "react-icons/lu";
3
- import { html, api, Icons } from "../lib.js";
4
- import { useApp } from "../context.js";
5
-
6
- export function SerialPortPage() {
7
- const { toast, refresh, status, showConfirm } = useApp();
8
- const [ports, setPorts] = useState([]);
9
- const [selected, setSelected] = useState(null);
10
- const [showAdd, setShowAdd] = useState(false);
11
- const [portName, setPortName] = useState("serialPort");
12
- const [portPath, setPortPath] = useState("/dev/ttyUSB0");
13
- const [baudRate, setBaudRate] = useState("9600");
14
- const [loading, setLoading] = useState(false);
15
-
16
- useEffect(() => {
17
- api("/serialports").then(setPorts).catch(() => {});
18
- }, [status]);
19
-
20
- async function handleAdd() {
21
- setLoading(true);
22
- try {
23
- const result = await api("/add-serialport", {
24
- method: "POST",
25
- body: { name: portName, portPath, baudRate: parseInt(baudRate) },
26
- });
27
- toast(result.message, "success");
28
- setShowAdd(false);
29
- setPortName("serialPort" + ((ports?.length || 0) + 1));
30
- setPortPath("/dev/ttyUSB0");
31
- setBaudRate("9600");
32
- refresh();
33
- api("/serialports").then(setPorts).catch(() => {});
34
- } catch (err) { toast(err.message, "error"); }
35
- setLoading(false);
36
- }
37
-
38
- async function handleDelete(name) {
39
- const ok = await showConfirm('Remove serial port "' + name + '"? This will remove its code from index.js.', { danger: true, confirmLabel: 'Remove' });
40
- if (!ok) return;
41
- try {
42
- await api("/serialports/" + name, { method: "DELETE" });
43
- toast('Serial port "' + name + '" removed', "success");
44
- setSelected(null);
45
- refresh();
46
- api("/serialports").then(setPorts).catch(() => {});
47
- } catch (err) { toast(err.message, "error"); }
48
- }
49
-
50
- const selPort = ports.find(p => p.name === selected);
51
- const hasNoPorts = !ports || ports.length === 0;
52
-
53
- if (hasNoPorts && !showAdd) {
54
- return html`<div>
55
- <div className="flex items-center justify-between mb-4">
56
- <div>
57
- <div className="card-title">SerialPort</div>
58
- <div className="card-subtitle">0 connections</div>
59
- </div>
60
- <button className="btn btn-primary" onClick=${() => setShowAdd(true)}>
61
- <span className="sidebar-icon">${Icons.plus}</span> Add Port
62
- </button>
63
- </div>
64
- <div className="empty-state">
65
- <div className="empty-icon"><${LuCable} size=${36} /></div>
66
- <h3>No serial ports</h3>
67
- <p>Add a serial port connection to communicate with hardware devices</p>
68
- <button className="btn btn-primary" onClick=${() => setShowAdd(true)}>Add Port</button>
69
- </div>
70
- </div>`;
71
- }
72
-
73
- return html`<div>
74
- <div className="flex items-center justify-between mb-4">
75
- <div>
76
- <div className="card-title">SerialPort</div>
77
- <div className="card-subtitle">${ports.length} connection${ports.length !== 1 ? 's' : ''}</div>
78
- </div>
79
- <button className=${`btn ${showAdd ? 'btn-ghost' : 'btn-primary'}`}
80
- onClick=${() => {
81
- if (showAdd) { setShowAdd(false); if (ports.length) setSelected(ports[0].name); }
82
- else { setShowAdd(true); setSelected(null); setPortName("serialPort" + (ports.length + 1)); }
83
- }}>
84
- ${showAdd ? 'Cancel' : html`<span className="sidebar-icon">${Icons.plus}</span> Add Port`}
85
- </button>
86
- </div>
87
-
88
- <div className="routes-layout">
89
- <div className="card" style=${{ padding: 0, overflow: 'hidden' }}>
90
- <div style=${{ padding: '14px 16px 10px', borderBottom: '1px solid var(--border)' }}>
91
- <span style=${{ fontWeight: 600, fontSize: '12px', textTransform: 'uppercase', letterSpacing: '0.5px', color: 'var(--text-muted)' }}>Connections</span>
92
- </div>
93
- <div className="routes-list">
94
- ${ports.map(p => html`<div key=${p.name}
95
- className=${`route-item ${selected === p.name && !showAdd ? 'active' : ''}`}
96
- onClick=${() => { setSelected(p.name); setShowAdd(false); }}>
97
- <div style=${{ display: 'flex', flexDirection: 'column', gap: '4px', flex: 1, minWidth: 0 }}>
98
- <span className="ri-name">${p.name}</span>
99
- <span className="ri-file">${p.path} @ ${p.baudRate}</span>
100
- </div>
101
- <span className="badge badge-success" style=${{ fontSize: '10px' }}>Active</span>
102
- </div>`)}
103
- <div className=${`route-item ${showAdd ? 'active' : ''}`}
104
- onClick=${() => { setShowAdd(true); setSelected(null); setPortName("serialPort" + (ports.length + 1)); }}
105
- style=${{ borderTop: ports.length ? '1px solid var(--border)' : 'none' }}>
106
- <span className="sidebar-icon" style=${{ color: 'var(--accent-light)' }}>${Icons.plus}</span>
107
- <span className="ri-name" style=${{ color: 'var(--accent-light)' }}>Add Port</span>
108
- </div>
109
- </div>
110
- </div>
111
-
112
- <div className="routes-detail">
113
- ${showAdd && html`<div className="card">
114
- <div className="card-title mb-2">Add Serial Port</div>
115
- <div className="card-subtitle mb-4">Configure a new serial port connection</div>
116
- <div className="form-group">
117
- <label className="form-label">Variable Name</label>
118
- <input className="form-input" value=${portName} onInput=${e => setPortName(e.target.value)} placeholder="serialPort1" />
119
- </div>
120
- <div className="grid-2">
121
- <div className="form-group">
122
- <label className="form-label">Port Path</label>
123
- <input className="form-input" value=${portPath} onInput=${e => setPortPath(e.target.value)} placeholder="/dev/ttyUSB0" />
124
- </div>
125
- <div className="form-group">
126
- <label className="form-label">Baud Rate</label>
127
- <select className="form-select" value=${baudRate} onChange=${e => setBaudRate(e.target.value)}>
128
- ${['9600', '14400', '19200', '38400', '57600', '115200'].map(r =>
129
- html`<option key=${r} value=${r}>${r}</option>`
130
- )}
131
- </select>
132
- </div>
133
- </div>
134
- <button className="btn btn-primary" onClick=${handleAdd} disabled=${loading}>
135
- ${loading ? html`<span className="spinner" />` : null}
136
- ${loading ? 'Adding...' : 'Add Port'}
137
- </button>
138
- </div>`}
139
-
140
- ${!showAdd && selPort && html`<div>
141
- <div className="card mb-4">
142
- <div className="flex items-center justify-between mb-3">
143
- <div>
144
- <div style=${{ fontWeight: 600, fontSize: '16px', marginBottom: '2px' }}>${selPort.name}</div>
145
- <div className="text-sm text-muted" style=${{ fontFamily: "'SF Mono', monospace" }}>${selPort.path} @ ${selPort.baudRate} baud</div>
146
- </div>
147
- <button className="btn btn-danger btn-sm btn-icon" onClick=${() => handleDelete(selPort.name)}>
148
- <span className="sidebar-icon">${Icons.trash}</span>
149
- </button>
150
- </div>
151
- <div style=${{ fontSize: '11px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px', color: 'var(--text-muted)', marginBottom: '8px' }}>Configuration</div>
152
- <div style=${{ display: 'grid', gridTemplateColumns: '100px 1fr', gap: '6px 12px', fontSize: '13px' }}>
153
- <span className="text-muted">Variable</span><span>${selPort.name}</span>
154
- <span className="text-muted">Path</span><span style=${{ fontFamily: 'monospace' }}>${selPort.path}</span>
155
- <span className="text-muted">Baud Rate</span><span>${selPort.baudRate}</span>
156
- <span className="text-muted">Parser</span><span>ReadlineParser (\\r\\n)</span>
157
- </div>
158
- </div>
159
- </div>`}
160
-
161
- ${!showAdd && !selPort && html`<div className="card">
162
- <div className="empty-state" style=${{ padding: '40px' }}>
163
- <p>Select a port to view details</p>
164
- </div>
165
- </div>`}
166
- </div>
167
- </div>
168
- </div>`;
169
- }
170
-
@@ -1,105 +0,0 @@
1
- import React, { useState, useEffect, useRef } from "react";
2
- import { motion } from "framer-motion";
3
- import { LuLock, LuEye, LuEyeOff } from "react-icons/lu";
4
- import { html, API } from "../lib.js";
5
-
6
- export function PasskeyScreen({ onSuccess }) {
7
- const [passkey, setPasskey] = useState("");
8
- const [show, setShow] = useState(false);
9
- const [error, setError] = useState("");
10
- const [loading, setLoading] = useState(false);
11
- const inputRef = useRef(null);
12
-
13
- const handleSubmit = async () => {
14
- if (loading) return;
15
- if (!passkey) {
16
- setError("Enter your passkey");
17
- return;
18
- }
19
- setLoading(true);
20
- setError("");
21
- try {
22
- const res = await fetch(API + "/auth", {
23
- method: "POST",
24
- headers: { "Content-Type": "application/json" },
25
- body: JSON.stringify({ passkey }),
26
- });
27
- const data = await res.json().catch(() => ({}));
28
- if (!res.ok) {
29
- setError(data.error || "Invalid passkey");
30
- setPasskey("");
31
- inputRef.current?.focus();
32
- } else {
33
- sessionStorage.setItem("_dev_passkey", passkey);
34
- onSuccess();
35
- }
36
- } catch {
37
- setError("Connection failed");
38
- } finally {
39
- setLoading(false);
40
- }
41
- };
42
-
43
- const handleKeyDown = (e) => {
44
- if (e.key === "Enter") handleSubmit();
45
- };
46
-
47
- useEffect(() => {
48
- inputRef.current?.focus();
49
- }, []);
50
-
51
- return html`<${motion.div}
52
- className="passkey-screen"
53
- initial=${{ opacity: 0 }}
54
- animate=${{ opacity: 1 }}
55
- exit=${{ opacity: 0 }}
56
- transition=${{ duration: 0.3 }}
57
- >
58
- <div className="splash-glow" />
59
- <${motion.div}
60
- className="passkey-card"
61
- initial=${{ opacity: 0, scale: 0.95, y: 20 }}
62
- animate=${{ opacity: 1, scale: 1, y: 0 }}
63
- transition=${{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
64
- >
65
- <${LuLock} size=${28} color="#0072ff" />
66
- <h2>Dashboard Passkey</h2>
67
- <p>Enter your passkey to continue</p>
68
- <div className="passkey-field" style=${{ position: "relative", width: "100%" }}>
69
- <input
70
- ref=${inputRef}
71
- type=${show ? "text" : "password"}
72
- autoComplete="current-password"
73
- className=${error ? "error" : ""}
74
- value=${passkey}
75
- placeholder="Passkey"
76
- style=${{ width: "100%", paddingRight: "40px" }}
77
- onInput=${(e) => {
78
- setPasskey(e.target.value);
79
- setError("");
80
- }}
81
- onKeyDown=${handleKeyDown}
82
- />
83
- <span
84
- onClick=${() => setShow((s) => !s)}
85
- title=${show ? "Hide" : "Show"}
86
- style=${{
87
- position: "absolute",
88
- right: "12px",
89
- top: "50%",
90
- transform: "translateY(-50%)",
91
- cursor: "pointer",
92
- color: "var(--text-muted, #888)",
93
- display: "flex",
94
- }}
95
- >
96
- ${show ? html`<${LuEyeOff} size=${18} />` : html`<${LuEye} size=${18} />`}
97
- </span>
98
- </div>
99
- <div className="passkey-error">${error}</div>
100
- <button className="passkey-btn" onClick=${handleSubmit} disabled=${loading}>
101
- ${loading ? "Verifying..." : "Unlock Dashboard"}
102
- </button>
103
- <//>
104
- <//>`;
105
- }