4bnode 4.1.1 → 4.1.3
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/add-security.js +0 -18
- package/app-1/.4bnode/.passkey +1 -0
- package/app-1/.4bnode/assets/fonts/NotoSans-Bold.woff2 +0 -0
- package/app-1/.4bnode/assets/fonts/NotoSans-Light.woff2 +0 -0
- package/app-1/.4bnode/assets/fonts/NotoSans-Medium.woff2 +0 -0
- package/app-1/.4bnode/assets/fonts/NotoSans-Regular.woff2 +0 -0
- package/app-1/.4bnode/assets/fonts/NotoSans-SemiBold.woff2 +0 -0
- package/app-1/.4bnode/assets/images/mongo.png +0 -0
- package/app-1/.4bnode/assets/js/App.js +139 -0
- package/app-1/.4bnode/assets/js/Root.js +40 -0
- package/app-1/.4bnode/assets/js/components/Modal.js +52 -0
- package/app-1/.4bnode/assets/js/components/Sidebar.js +73 -0
- package/app-1/.4bnode/assets/js/components/ToastContainer.js +15 -0
- package/app-1/.4bnode/assets/js/context.js +7 -0
- package/app-1/.4bnode/assets/js/lib.js +113 -0
- package/app-1/.4bnode/assets/js/main.js +5 -0
- package/app-1/.4bnode/assets/js/pages/AiBuilderPage.js +268 -0
- package/app-1/.4bnode/assets/js/pages/ApiTesterPage.js +1879 -0
- package/app-1/.4bnode/assets/js/pages/DashboardPage.js +435 -0
- package/app-1/.4bnode/assets/js/pages/DatabasePage.js +300 -0
- package/app-1/.4bnode/assets/js/pages/DocsPage.js +150 -0
- package/app-1/.4bnode/assets/js/pages/EnvironmentPage.js +103 -0
- package/app-1/.4bnode/assets/js/pages/LogsPage.js +205 -0
- package/app-1/.4bnode/assets/js/pages/MailPage.js +134 -0
- package/app-1/.4bnode/assets/js/pages/RealtimePage.js +186 -0
- package/app-1/.4bnode/assets/js/pages/RoutesPage.js +1436 -0
- package/app-1/.4bnode/assets/js/pages/SchemasPage.js +838 -0
- package/app-1/.4bnode/assets/js/pages/SecurityPage.js +74 -0
- package/app-1/.4bnode/assets/js/pages/SerialPortPage.js +170 -0
- package/app-1/.4bnode/assets/js/screens/PasskeyScreen.js +105 -0
- package/app-1/.4bnode/codegen.js +1249 -0
- package/app-1/.4bnode/dev-api.js +3621 -0
- package/app-1/.4bnode/ui.html +1578 -0
- package/app-1/README.md +47 -0
- package/app-1/ecosystem.config.cjs +25 -0
- package/app-1/index.js +145 -0
- package/app-1/package.json +35 -0
- package/app-1/public/index.html +292 -0
- package/app-1/src/db.js +13 -0
- package/app-1/src/docs.js +84 -0
- package/app-1/src/middleware/errorHandler.js +15 -0
- package/app-1/src/middleware/roles.js +14 -0
- package/app-1/src/middleware/validate.js +20 -0
- package/app-1/src/models/login.js +9 -0
- package/app-1/src/openapi.json +62 -0
- package/app-1/src/routes/products.js +26 -0
- package/app-1/src/validators/products.js +7 -0
- package/app-1/src/validators/user.js +7 -0
- package/index.js +2 -17
- package/lib/codegen.js +69 -43
- package/lib/codegen.test.js +21 -2
- package/package.json +1 -1
- package/skeleton/.4bnode/assets/js/App.js +0 -2
- package/skeleton/.4bnode/assets/js/components/Sidebar.js +2 -3
- package/skeleton/.4bnode/assets/js/lib.js +0 -3
- package/skeleton/.4bnode/assets/js/pages/DocsPage.js +29 -21
- package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +12 -0
- package/skeleton/.4bnode/codegen.js +69 -43
- package/skeleton/.4bnode/dev-api.js +173 -87
- package/skeleton/_gitignore +0 -1
- package/skeleton/ecosystem.config.cjs +25 -0
- package/skeleton/index.js +4 -8
- package/skeleton/.4bnode/assets/js/pages/ApiKeysPage.js +0 -164
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// PM2 process config for production.
|
|
2
|
+
//
|
|
3
|
+
// pm2 start ecosystem.config.cjs # start (NODE_ENV=production)
|
|
4
|
+
// pm2 save # remember across reboots
|
|
5
|
+
// pm2 startup # enable boot startup (run the printed command)
|
|
6
|
+
// pm2 logs / pm2 restart / pm2 status
|
|
7
|
+
//
|
|
8
|
+
// NODE_ENV=production keeps the /_dev dashboard OFF and security on. Other config
|
|
9
|
+
// (PORT, MONGO_URI, secrets) comes from your .env. CommonJS (.cjs) on purpose —
|
|
10
|
+
// the app itself is an ES module.
|
|
11
|
+
const pkg = require("./package.json");
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
apps: [
|
|
15
|
+
{
|
|
16
|
+
name: pkg.name || "app",
|
|
17
|
+
script: "index.js",
|
|
18
|
+
instances: 1,
|
|
19
|
+
autorestart: true,
|
|
20
|
+
env: {
|
|
21
|
+
NODE_ENV: "production",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
};
|
package/skeleton/index.js
CHANGED
|
@@ -8,12 +8,15 @@ import helmet from "helmet";
|
|
|
8
8
|
import rateLimit from "express-rate-limit";
|
|
9
9
|
import { createInterface } from "readline";
|
|
10
10
|
import { notFound, errorHandler } from "./src/middleware/errorHandler.js";
|
|
11
|
-
import apiKey from "./src/middleware/apiKey.js";
|
|
12
11
|
|
|
13
12
|
// ── Environment ─────────────────────────────────────
|
|
14
13
|
dotenv.config();
|
|
15
14
|
|
|
16
15
|
const app = express();
|
|
16
|
+
// Behind reverse proxies (e.g. nginx → load balancer). Lets Express read the real
|
|
17
|
+
// client IP from X-Forwarded-For so rate limiting and req.ip are correct. The number
|
|
18
|
+
// is how many proxy hops to trust — adjust to your infrastructure.
|
|
19
|
+
app.set("trust proxy", 2);
|
|
17
20
|
const port = process.env.PORT || 3000;
|
|
18
21
|
const __dirname = path.resolve();
|
|
19
22
|
|
|
@@ -54,13 +57,6 @@ const apiLimiter = rateLimit({
|
|
|
54
57
|
});
|
|
55
58
|
app.use(apiLimiter);
|
|
56
59
|
|
|
57
|
-
// ── API key gate ─────────────────────────────────────
|
|
58
|
-
// Every /api/* request must present a valid key in the `x-api-key` header.
|
|
59
|
-
// Manage keys in the dashboard (API Keys). Set API_KEY_REQUIRED=off to disable.
|
|
60
|
-
if (process.env.API_KEY_REQUIRED !== "off") {
|
|
61
|
-
app.use("/api", apiKey);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
60
|
// ── Static Files ────────────────────────────────────
|
|
65
61
|
app.use(express.static(path.join(__dirname, "public")));
|
|
66
62
|
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
2
|
-
import { LuKeyRound, LuPlus, LuTrash2, LuCopy, LuEye, LuEyeOff, LuPencil, LuCheck, LuX } from "react-icons/lu";
|
|
3
|
-
import { html, api, copyToClipboard } from "../lib.js";
|
|
4
|
-
import { useApp } from "../context.js";
|
|
5
|
-
|
|
6
|
-
export function ApiKeysPage() {
|
|
7
|
-
const { toast, showConfirm } = useApp();
|
|
8
|
-
const [keys, setKeys] = useState(null);
|
|
9
|
-
const [label, setLabel] = useState("");
|
|
10
|
-
const [busy, setBusy] = useState(false);
|
|
11
|
-
const [revealed, setRevealed] = useState({});
|
|
12
|
-
const [editId, setEditId] = useState(null);
|
|
13
|
-
const [editLabel, setEditLabel] = useState("");
|
|
14
|
-
|
|
15
|
-
async function load() {
|
|
16
|
-
try {
|
|
17
|
-
const r = await api("/security/api-keys");
|
|
18
|
-
setKeys(r.keys || []);
|
|
19
|
-
} catch (err) {
|
|
20
|
-
toast(err.message, "error");
|
|
21
|
-
setKeys([]);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
useEffect(() => { load(); }, []);
|
|
25
|
-
|
|
26
|
-
async function generate() {
|
|
27
|
-
setBusy(true);
|
|
28
|
-
try {
|
|
29
|
-
const r = await api("/security/api-keys", { method: "POST", body: { label: label.trim() } });
|
|
30
|
-
setLabel("");
|
|
31
|
-
setRevealed((s) => ({ ...s, [r.key.id]: true }));
|
|
32
|
-
toast("API key generated", "success");
|
|
33
|
-
await load();
|
|
34
|
-
} catch (err) {
|
|
35
|
-
toast(err.message, "error");
|
|
36
|
-
}
|
|
37
|
-
setBusy(false);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function revoke(k) {
|
|
41
|
-
const ok = await showConfirm(`Revoke API key "${k.label}"? Any client using it will stop working immediately.`, { danger: true, confirmLabel: "Revoke" });
|
|
42
|
-
if (!ok) return;
|
|
43
|
-
try {
|
|
44
|
-
await api("/security/api-keys/" + k.id, { method: "DELETE" });
|
|
45
|
-
toast("API key revoked", "success");
|
|
46
|
-
await load();
|
|
47
|
-
} catch (err) {
|
|
48
|
-
toast(err.message, "error");
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function startRename(k) {
|
|
53
|
-
setEditId(k.id);
|
|
54
|
-
setEditLabel(k.label);
|
|
55
|
-
}
|
|
56
|
-
function cancelRename() {
|
|
57
|
-
setEditId(null);
|
|
58
|
-
setEditLabel("");
|
|
59
|
-
}
|
|
60
|
-
async function saveRename(k) {
|
|
61
|
-
const next = editLabel.trim();
|
|
62
|
-
if (!next || next === k.label) { cancelRename(); return; }
|
|
63
|
-
try {
|
|
64
|
-
await api("/security/api-keys/" + k.id, { method: "PATCH", body: { label: next } });
|
|
65
|
-
toast("API key renamed", "success");
|
|
66
|
-
cancelRename();
|
|
67
|
-
await load();
|
|
68
|
-
} catch (err) {
|
|
69
|
-
toast(err.message, "error");
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function copy(k) {
|
|
74
|
-
copyToClipboard(k.key);
|
|
75
|
-
toast("Key copied to clipboard", "success");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function mask(key) {
|
|
79
|
-
if (key.length <= 12) return key;
|
|
80
|
-
return key.slice(0, 7) + "…" + key.slice(-4);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function fmtDate(iso) {
|
|
84
|
-
if (!iso) return "";
|
|
85
|
-
try { return new Date(iso).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }); }
|
|
86
|
-
catch { return ""; }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return html`<div>
|
|
90
|
-
<div className="flex items-center gap-2 mb-1">
|
|
91
|
-
<${LuKeyRound} size=${20} />
|
|
92
|
-
<div className="card-title" style=${{ margin: 0 }}>API Keys</div>
|
|
93
|
-
</div>
|
|
94
|
-
<div className="card-subtitle mb-4">
|
|
95
|
-
Named keys for machine/service access. A caller sends one in the
|
|
96
|
-
<span style=${{ fontFamily: "monospace" }}> x-api-key</span> header.
|
|
97
|
-
Protect a route with <span style=${{ fontFamily: "monospace" }}>apiKey</span> middleware:
|
|
98
|
-
<span style=${{ fontFamily: "monospace" }}>app.use('/api/private', apiKey)</span>.
|
|
99
|
-
Keys live in <span style=${{ fontFamily: "monospace" }}>src/api-keys.json</span> and take effect immediately.
|
|
100
|
-
</div>
|
|
101
|
-
|
|
102
|
-
<div className="card">
|
|
103
|
-
<div style=${{ display: "flex", gap: "8px", marginBottom: "16px", flexWrap: "wrap" }}>
|
|
104
|
-
<input className="form-input" placeholder="Key name (e.g. Mobile app, Partner X)"
|
|
105
|
-
value=${label} maxLength=${60}
|
|
106
|
-
onInput=${(e) => setLabel(e.target.value)}
|
|
107
|
-
onKeyDown=${(e) => { if (e.key === "Enter" && !busy) generate(); }}
|
|
108
|
-
style=${{ flex: 1, minWidth: "220px" }} />
|
|
109
|
-
<button className="btn btn-primary" onClick=${generate} disabled=${busy}>
|
|
110
|
-
${busy ? html`<span className="spinner" />` : html`<${LuPlus} size=${15} />`} Generate key
|
|
111
|
-
</button>
|
|
112
|
-
</div>
|
|
113
|
-
|
|
114
|
-
${keys === null
|
|
115
|
-
? html`<div className="text-muted text-sm">Loading…</div>`
|
|
116
|
-
: keys.length === 0
|
|
117
|
-
? html`<div className="text-muted text-sm">No keys yet — generate one above.</div>`
|
|
118
|
-
: html`<div style=${{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
|
119
|
-
${keys.map((k) => html`<div key=${k.id}
|
|
120
|
-
style=${{ display: "flex", alignItems: "center", gap: "10px", padding: "10px 12px",
|
|
121
|
-
border: "1px solid var(--border)", borderRadius: "10px", background: "var(--bg-surface)" }}>
|
|
122
|
-
<div style=${{ flex: 1, minWidth: 0 }}>
|
|
123
|
-
${editId === k.id
|
|
124
|
-
? html`<input className="form-input" autoFocus value=${editLabel} maxLength=${60}
|
|
125
|
-
onInput=${(e) => setEditLabel(e.target.value)}
|
|
126
|
-
onKeyDown=${(e) => { if (e.key === "Enter") saveRename(k); if (e.key === "Escape") cancelRename(); }}
|
|
127
|
-
style=${{ fontSize: "13px", padding: "4px 8px", marginBottom: "2px", maxWidth: "260px" }} />`
|
|
128
|
-
: html`<div style=${{ fontWeight: 600, fontSize: "13px" }}>${k.label}</div>`}
|
|
129
|
-
<div style=${{ fontFamily: "monospace", fontSize: "12px", color: "var(--text-muted)" }}>
|
|
130
|
-
${revealed[k.id] ? k.key : mask(k.key)}
|
|
131
|
-
${k.createdAt && html`<span style=${{ marginLeft: "8px" }}>· ${fmtDate(k.createdAt)}</span>`}
|
|
132
|
-
</div>
|
|
133
|
-
</div>
|
|
134
|
-
${editId === k.id
|
|
135
|
-
? html`<${React.Fragment}>
|
|
136
|
-
<button className="btn btn-sm btn-ghost" title="Save" onClick=${() => saveRename(k)}
|
|
137
|
-
style=${{ color: "var(--success, #22c55e)" }}>
|
|
138
|
-
<${LuCheck} size=${15} />
|
|
139
|
-
</button>
|
|
140
|
-
<button className="btn btn-sm btn-ghost" title="Cancel" onClick=${cancelRename}>
|
|
141
|
-
<${LuX} size=${15} />
|
|
142
|
-
</button>
|
|
143
|
-
<//>`
|
|
144
|
-
: html`<${React.Fragment}>
|
|
145
|
-
<button className="btn btn-sm btn-ghost" title="Rename" onClick=${() => startRename(k)}>
|
|
146
|
-
<${LuPencil} size=${15} />
|
|
147
|
-
</button>
|
|
148
|
-
<button className="btn btn-sm btn-ghost" title=${revealed[k.id] ? "Hide" : "Reveal"}
|
|
149
|
-
onClick=${() => setRevealed((s) => ({ ...s, [k.id]: !s[k.id] }))}>
|
|
150
|
-
${revealed[k.id] ? html`<${LuEyeOff} size=${15} />` : html`<${LuEye} size=${15} />`}
|
|
151
|
-
</button>
|
|
152
|
-
<button className="btn btn-sm btn-ghost" title="Copy" onClick=${() => copy(k)}>
|
|
153
|
-
<${LuCopy} size=${15} />
|
|
154
|
-
</button>
|
|
155
|
-
<button className="btn btn-sm btn-ghost" title="Revoke" onClick=${() => revoke(k)}
|
|
156
|
-
style=${{ color: "var(--danger, #ef4444)" }}>
|
|
157
|
-
<${LuTrash2} size=${15} />
|
|
158
|
-
</button>
|
|
159
|
-
<//>`}
|
|
160
|
-
</div>`)}
|
|
161
|
-
</div>`}
|
|
162
|
-
</div>
|
|
163
|
-
</div>`;
|
|
164
|
-
}
|