4bnode 4.1.4 → 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.
- package/add-api.js +5 -3
- package/add-docs.js +18 -5
- package/add-email.js +118 -28
- package/add-login.js +35 -16
- package/add-mongo-schema.js +13 -5
- package/lib/codegen.js +408 -104
- package/lib/codegen.test.js +122 -15
- package/lib/names.js +1 -1
- package/package.json +1 -1
- package/skeleton/.4bnode/assets/js/components/Sidebar.js +2 -2
- package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +54 -9
- package/skeleton/.4bnode/assets/js/pages/LogsPage.js +49 -16
- package/skeleton/.4bnode/assets/js/pages/MailPage.js +221 -71
- package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +51 -2
- package/skeleton/.4bnode/assets/js/pages/SecurityPage.js +1 -1
- package/skeleton/.4bnode/codegen.js +408 -104
- package/skeleton/.4bnode/dev-api.js +384 -30
- package/skeleton/.env +6 -0
- package/skeleton/.env.example +25 -0
- package/skeleton/_gitignore +1 -0
- package/skeleton/index.js +7 -2
- package/skeleton/src/middleware/apiKey.js +14 -1
- package/skeleton/src/middleware/errorHandler.js +7 -2
- package/app-1/.4bnode/.passkey +0 -1
- 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 +0 -139
- package/app-1/.4bnode/assets/js/Root.js +0 -40
- package/app-1/.4bnode/assets/js/components/Modal.js +0 -52
- package/app-1/.4bnode/assets/js/components/Sidebar.js +0 -73
- package/app-1/.4bnode/assets/js/components/ToastContainer.js +0 -15
- package/app-1/.4bnode/assets/js/context.js +0 -7
- package/app-1/.4bnode/assets/js/lib.js +0 -113
- package/app-1/.4bnode/assets/js/main.js +0 -5
- package/app-1/.4bnode/assets/js/pages/AiBuilderPage.js +0 -268
- package/app-1/.4bnode/assets/js/pages/ApiTesterPage.js +0 -1879
- package/app-1/.4bnode/assets/js/pages/DashboardPage.js +0 -435
- package/app-1/.4bnode/assets/js/pages/DatabasePage.js +0 -300
- package/app-1/.4bnode/assets/js/pages/DocsPage.js +0 -150
- package/app-1/.4bnode/assets/js/pages/EnvironmentPage.js +0 -103
- package/app-1/.4bnode/assets/js/pages/LogsPage.js +0 -205
- package/app-1/.4bnode/assets/js/pages/MailPage.js +0 -134
- package/app-1/.4bnode/assets/js/pages/RealtimePage.js +0 -186
- package/app-1/.4bnode/assets/js/pages/RoutesPage.js +0 -1436
- package/app-1/.4bnode/assets/js/pages/SchemasPage.js +0 -838
- package/app-1/.4bnode/assets/js/pages/SecurityPage.js +0 -74
- package/app-1/.4bnode/assets/js/pages/SerialPortPage.js +0 -170
- package/app-1/.4bnode/assets/js/screens/PasskeyScreen.js +0 -105
- package/app-1/.4bnode/codegen.js +0 -1249
- package/app-1/.4bnode/dev-api.js +0 -3621
- package/app-1/.4bnode/ui.html +0 -1578
- package/app-1/README.md +0 -47
- package/app-1/ecosystem.config.cjs +0 -25
- package/app-1/index.js +0 -145
- package/app-1/package.json +0 -35
- package/app-1/public/index.html +0 -292
- package/app-1/src/db.js +0 -13
- package/app-1/src/docs.js +0 -84
- package/app-1/src/middleware/errorHandler.js +0 -15
- package/app-1/src/middleware/roles.js +0 -14
- package/app-1/src/middleware/validate.js +0 -20
- package/app-1/src/models/login.js +0 -9
- package/app-1/src/openapi.json +0 -62
- package/app-1/src/routes/products.js +0 -26
- package/app-1/src/validators/products.js +0 -7
- package/app-1/src/validators/user.js +0 -7
package/skeleton/index.js
CHANGED
|
@@ -23,13 +23,15 @@ const __dirname = path.resolve();
|
|
|
23
23
|
// ── Middleware ───────────────────────────────────────
|
|
24
24
|
app.use(express.json({ limit: "50mb" }));
|
|
25
25
|
app.use(express.urlencoded({ extended: true, limit: "50mb" }));
|
|
26
|
-
app.use(cors({ origin: "*" }));
|
|
27
26
|
|
|
28
27
|
// ── Dev Dashboard (local development only) ───────────
|
|
29
28
|
// The dashboard at /_dev can write files and run shell commands — it is a
|
|
30
29
|
// remote-code-execution surface and must NEVER be reachable in production.
|
|
31
30
|
// In production it is not even imported. It additionally restricts itself to
|
|
32
|
-
// loopback requests
|
|
31
|
+
// loopback requests, validates Host/Origin (defeats DNS-rebinding), and requires
|
|
32
|
+
// a passkey for remote access (see .4bnode/dev-api.js). It is mounted BEFORE the
|
|
33
|
+
// permissive global CORS below so `Access-Control-Allow-Origin: *` never applies
|
|
34
|
+
// to the dashboard.
|
|
33
35
|
// • production (NODE_ENV=production): OFF — set _4BNODE_DASHBOARD=on to force.
|
|
34
36
|
// • non-production: ON — set _4BNODE_DASHBOARD=off to disable.
|
|
35
37
|
const dashboardEnabled =
|
|
@@ -42,6 +44,9 @@ if (dashboardEnabled) {
|
|
|
42
44
|
} catch {}
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
// ── CORS (applies to your API, not the /_dev dashboard above) ──
|
|
48
|
+
app.use(cors({ origin: "*" }));
|
|
49
|
+
|
|
45
50
|
// ── Security (built-in by default) ───────────────────
|
|
46
51
|
// Mounted AFTER the /_dev dashboard so its CDN/inline scripts aren't blocked by
|
|
47
52
|
// helmet's CSP and the dashboard isn't throttled by the rate limiter.
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// app.use('/api/private', apiKey);
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import path from 'path';
|
|
7
|
+
import crypto from 'crypto';
|
|
7
8
|
import { fileURLToPath } from 'url';
|
|
8
9
|
|
|
9
10
|
const KEYS_FILE = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'api-keys.json');
|
|
@@ -24,10 +25,22 @@ function loadKeys() {
|
|
|
24
25
|
return _cache.keys;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
// Constant-time membership test: hash both sides to a fixed length so neither the
|
|
29
|
+
// match result nor the key length leaks through string-comparison timing.
|
|
30
|
+
function keyMatches(provided, valid) {
|
|
31
|
+
const p = crypto.createHash('sha256').update(String(provided)).digest();
|
|
32
|
+
let ok = false;
|
|
33
|
+
for (const k of valid) {
|
|
34
|
+
const h = crypto.createHash('sha256').update(String(k)).digest();
|
|
35
|
+
if (crypto.timingSafeEqual(p, h)) ok = true; // no early return — keep timing flat
|
|
36
|
+
}
|
|
37
|
+
return ok;
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
const apiKey = (req, res, next) => {
|
|
28
41
|
const provided = req.header('x-api-key');
|
|
29
42
|
const valid = loadKeys();
|
|
30
|
-
if (!provided || !
|
|
43
|
+
if (!provided || !keyMatches(provided, valid)) {
|
|
31
44
|
return res.status(401).json({ message: 'Invalid or missing API key' });
|
|
32
45
|
}
|
|
33
46
|
next();
|
|
@@ -8,8 +8,13 @@ export function notFound(req, res, next) {
|
|
|
8
8
|
export function errorHandler(err, req, res, next) {
|
|
9
9
|
const status = err.status || err.statusCode || 500;
|
|
10
10
|
console.error(err);
|
|
11
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
12
|
+
// In production, only surface messages for client (4xx) errors or errors
|
|
13
|
+
// explicitly marked safe (err.expose). Never leak raw 5xx internals — a Mongo
|
|
14
|
+
// E11000 or driver message discloses schema/indexes and enables enumeration.
|
|
15
|
+
const showMessage = !isProd || err.expose === true || (status >= 400 && status < 500);
|
|
11
16
|
res.status(status).json({
|
|
12
|
-
message: err.message || 'Server error',
|
|
13
|
-
...(
|
|
17
|
+
message: showMessage ? (err.message || 'Server error') : 'Server error',
|
|
18
|
+
...(isProd ? {} : { stack: err.stack }),
|
|
14
19
|
});
|
|
15
20
|
}
|
package/app-1/.4bnode/.passkey
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
scrypt$af7f6048a1c565afcc6f06de246f8b18$673c63bd180dc82eb74265fba34699ad45d1ae27353595e5c1c492351c66aafbd80e18d712b1e6f40e2343e45137b96cb4ed376d0c4ac5b54e8ea9d0a2fd2f73
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useCallback } from "react";
|
|
2
|
-
import { motion, AnimatePresence } from "framer-motion";
|
|
3
|
-
import { html, api, PAGE_TITLES } from "./lib.js";
|
|
4
|
-
import { AppContext } from "./context.js";
|
|
5
|
-
import { Sidebar } from "./components/Sidebar.js";
|
|
6
|
-
import { ToastContainer } from "./components/ToastContainer.js";
|
|
7
|
-
import { ConfirmDialog } from "./components/Modal.js";
|
|
8
|
-
import { DashboardPage } from "./pages/DashboardPage.js";
|
|
9
|
-
import { DatabasePage } from "./pages/DatabasePage.js";
|
|
10
|
-
import { SchemasPage } from "./pages/SchemasPage.js";
|
|
11
|
-
import { RoutesPage } from "./pages/RoutesPage.js";
|
|
12
|
-
import { DocsPage } from "./pages/DocsPage.js";
|
|
13
|
-
import { AiBuilderPage } from "./pages/AiBuilderPage.js";
|
|
14
|
-
import { SecurityPage } from "./pages/SecurityPage.js";
|
|
15
|
-
import { ApiTesterPage } from "./pages/ApiTesterPage.js";
|
|
16
|
-
import { RealtimePage } from "./pages/RealtimePage.js";
|
|
17
|
-
import { SerialPortPage } from "./pages/SerialPortPage.js";
|
|
18
|
-
import { MailPage } from "./pages/MailPage.js";
|
|
19
|
-
import { LogsPage } from "./pages/LogsPage.js";
|
|
20
|
-
import { EnvironmentPage } from "./pages/EnvironmentPage.js";
|
|
21
|
-
|
|
22
|
-
function getHashPage() {
|
|
23
|
-
const hash = window.location.hash.replace("#/", "").replace("#", "");
|
|
24
|
-
return hash && PAGE_TITLES[hash] ? hash : "dashboard";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function App() {
|
|
28
|
-
const [page, setPageState] = useState(getHashPage);
|
|
29
|
-
const [data, setData] = useState({
|
|
30
|
-
models: [],
|
|
31
|
-
routes: [],
|
|
32
|
-
mongoConfigured: false,
|
|
33
|
-
routesDirExists: false,
|
|
34
|
-
});
|
|
35
|
-
const [status, setStatus] = useState({});
|
|
36
|
-
const [toasts, setToasts] = useState([]);
|
|
37
|
-
const [confirmState, setConfirmState] = useState(null);
|
|
38
|
-
|
|
39
|
-
const showConfirm = useCallback((message, opts = {}) => {
|
|
40
|
-
return new Promise((resolve) => {
|
|
41
|
-
setConfirmState({ message, ...opts, resolve });
|
|
42
|
-
});
|
|
43
|
-
}, []);
|
|
44
|
-
|
|
45
|
-
const setPage = useCallback((p) => {
|
|
46
|
-
window.location.hash = "#/" + p;
|
|
47
|
-
}, []);
|
|
48
|
-
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
function onHash() {
|
|
51
|
-
setPageState(getHashPage());
|
|
52
|
-
}
|
|
53
|
-
window.addEventListener("hashchange", onHash);
|
|
54
|
-
return () => window.removeEventListener("hashchange", onHash);
|
|
55
|
-
}, []);
|
|
56
|
-
|
|
57
|
-
const toast = useCallback((message, type = "info") => {
|
|
58
|
-
const id = Date.now();
|
|
59
|
-
setToasts((prev) => [...prev, { id, message, type }]);
|
|
60
|
-
setTimeout(
|
|
61
|
-
() => setToasts((prev) => prev.filter((t) => t.id !== id)),
|
|
62
|
-
3500,
|
|
63
|
-
);
|
|
64
|
-
}, []);
|
|
65
|
-
|
|
66
|
-
const refresh = useCallback(async () => {
|
|
67
|
-
// File-writing actions (create route, docs, security, ...) modify index.js,
|
|
68
|
-
// which triggers a nodemon restart. Since the dashboard is served by that same
|
|
69
|
-
// app, the refresh fetch fired right after can hit the restart window and fail,
|
|
70
|
-
// leaving the UI stale until a manual reload. Retry across the restart so new
|
|
71
|
-
// state shows up in real time.
|
|
72
|
-
let lastErr;
|
|
73
|
-
for (let attempt = 0; attempt < 8; attempt++) {
|
|
74
|
-
try {
|
|
75
|
-
const [dashData, statusData] = await Promise.all([
|
|
76
|
-
api("/dashboard"),
|
|
77
|
-
api("/status"),
|
|
78
|
-
]);
|
|
79
|
-
setData(dashData);
|
|
80
|
-
setStatus(statusData);
|
|
81
|
-
return;
|
|
82
|
-
} catch (err) {
|
|
83
|
-
lastErr = err;
|
|
84
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
throw lastErr;
|
|
88
|
-
}, []);
|
|
89
|
-
|
|
90
|
-
useEffect(() => {
|
|
91
|
-
refresh().catch(() => {});
|
|
92
|
-
}, []);
|
|
93
|
-
|
|
94
|
-
const pages = {
|
|
95
|
-
dashboard: html`<${DashboardPage} />`,
|
|
96
|
-
"ai-builder": html`<${AiBuilderPage} />`,
|
|
97
|
-
database: html`<${DatabasePage} />`,
|
|
98
|
-
schemas: html`<${SchemasPage} />`,
|
|
99
|
-
routes: html`<${RoutesPage} />`,
|
|
100
|
-
docs: html`<${DocsPage} />`,
|
|
101
|
-
security: html`<${SecurityPage} />`,
|
|
102
|
-
"api-tester": html`<${ApiTesterPage} />`,
|
|
103
|
-
realtime: html`<${RealtimePage} />`,
|
|
104
|
-
serialport: html`<${SerialPortPage} />`,
|
|
105
|
-
mail: html`<${MailPage} />`,
|
|
106
|
-
logs: html`<${LogsPage} />`,
|
|
107
|
-
env: html`<${EnvironmentPage} />`,
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
return html`<${AppContext.Provider}
|
|
111
|
-
value=${{ page, setPage, data, status, toasts, toast, refresh, showConfirm }}
|
|
112
|
-
>
|
|
113
|
-
<${Sidebar} />
|
|
114
|
-
<div className="main">
|
|
115
|
-
<div className="topbar">
|
|
116
|
-
<h2>${PAGE_TITLES[page] || "Dashboard"}</h2>
|
|
117
|
-
</div>
|
|
118
|
-
<div className="content">
|
|
119
|
-
<${AnimatePresence} mode="wait">
|
|
120
|
-
<${motion.div} key=${page}
|
|
121
|
-
initial=${{ opacity: 0, y: 8 }}
|
|
122
|
-
animate=${{ opacity: 1, y: 0 }}
|
|
123
|
-
exit=${{ opacity: 0, y: -8 }}
|
|
124
|
-
transition=${{ duration: 0.15 }}>
|
|
125
|
-
${pages[page] || pages.dashboard}
|
|
126
|
-
<//>
|
|
127
|
-
<//>
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
<${ToastContainer} />
|
|
131
|
-
${confirmState && html`<${ConfirmDialog}
|
|
132
|
-
message=${confirmState.message}
|
|
133
|
-
confirmLabel=${confirmState.confirmLabel}
|
|
134
|
-
danger=${confirmState.danger}
|
|
135
|
-
onConfirm=${() => { confirmState.resolve(true); setConfirmState(null); }}
|
|
136
|
-
onCancel=${() => { confirmState.resolve(false); setConfirmState(null); }}
|
|
137
|
-
/>`}
|
|
138
|
-
<//>`;
|
|
139
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
2
|
-
import { AnimatePresence } from "framer-motion";
|
|
3
|
-
import { motion } from "framer-motion";
|
|
4
|
-
import { html, API, getStoredPasskey } from "./lib.js";
|
|
5
|
-
import { App } from "./App.js";
|
|
6
|
-
import { PasskeyScreen } from "./screens/PasskeyScreen.js";
|
|
7
|
-
|
|
8
|
-
export function Root() {
|
|
9
|
-
const [authed, setAuthed] = useState(false);
|
|
10
|
-
const [checking, setChecking] = useState(true);
|
|
11
|
-
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
(async () => {
|
|
14
|
-
try {
|
|
15
|
-
const stored = getStoredPasskey();
|
|
16
|
-
const res = await fetch(API + '/auth', {
|
|
17
|
-
method: 'POST',
|
|
18
|
-
headers: { 'Content-Type': 'application/json' },
|
|
19
|
-
body: JSON.stringify({ passkey: stored }),
|
|
20
|
-
});
|
|
21
|
-
if (res.ok) setAuthed(true);
|
|
22
|
-
} catch {}
|
|
23
|
-
setChecking(false);
|
|
24
|
-
})();
|
|
25
|
-
}, []);
|
|
26
|
-
|
|
27
|
-
if (checking) return null;
|
|
28
|
-
|
|
29
|
-
return html`<${AnimatePresence} mode="wait">
|
|
30
|
-
${!authed
|
|
31
|
-
? html`<${PasskeyScreen} key="passkey" onSuccess=${() => setAuthed(true)} />`
|
|
32
|
-
: html`<${motion.div} key="app"
|
|
33
|
-
style=${{ display: 'flex', height: '100vh', width: '100%' }}
|
|
34
|
-
initial=${{ opacity: 0 }}
|
|
35
|
-
animate=${{ opacity: 1 }}
|
|
36
|
-
transition=${{ duration: 0.4 }}>
|
|
37
|
-
<${App} />
|
|
38
|
-
<//>`}
|
|
39
|
-
<//>`;
|
|
40
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { motion, AnimatePresence } from "framer-motion";
|
|
3
|
-
import { LuTrash2, LuZap } from "react-icons/lu";
|
|
4
|
-
import { html } from "../lib.js";
|
|
5
|
-
|
|
6
|
-
export function Modal({ title, children, onClose, actions }) {
|
|
7
|
-
return html`<${AnimatePresence}><${motion.div}
|
|
8
|
-
className="modal-overlay"
|
|
9
|
-
onClick=${(e) => e.target === e.currentTarget && onClose()}
|
|
10
|
-
initial=${{ opacity: 0 }} animate=${{ opacity: 1 }} exit=${{ opacity: 0 }}
|
|
11
|
-
transition=${{ duration: 0.15 }}
|
|
12
|
-
>
|
|
13
|
-
<${motion.div} className="modal"
|
|
14
|
-
initial=${{ opacity: 0, scale: 0.95, y: 10 }}
|
|
15
|
-
animate=${{ opacity: 1, scale: 1, y: 0 }}
|
|
16
|
-
exit=${{ opacity: 0, scale: 0.95, y: 10 }}
|
|
17
|
-
transition=${{ duration: 0.15 }}>
|
|
18
|
-
<div className="modal-title">${title}</div>
|
|
19
|
-
${children}
|
|
20
|
-
${actions && html`<div className="modal-actions">${actions}</div>`}
|
|
21
|
-
<//>
|
|
22
|
-
<//><//>`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function ConfirmDialog({ message, onConfirm, onCancel, confirmLabel, danger }) {
|
|
26
|
-
return html`<${AnimatePresence}><${motion.div}
|
|
27
|
-
className="modal-overlay"
|
|
28
|
-
onClick=${onCancel}
|
|
29
|
-
initial=${{ opacity: 0 }} animate=${{ opacity: 1 }} exit=${{ opacity: 0 }}
|
|
30
|
-
transition=${{ duration: 0.15 }}
|
|
31
|
-
>
|
|
32
|
-
<${motion.div} className="modal" onClick=${e => e.stopPropagation()}
|
|
33
|
-
style=${{ minWidth: '380px', maxWidth: '440px' }}
|
|
34
|
-
initial=${{ opacity: 0, scale: 0.95, y: 10 }}
|
|
35
|
-
animate=${{ opacity: 1, scale: 1, y: 0 }}
|
|
36
|
-
exit=${{ opacity: 0, scale: 0.95, y: 10 }}
|
|
37
|
-
transition=${{ duration: 0.15 }}>
|
|
38
|
-
<div className="flex items-center gap-3" style=${{ marginBottom: '16px' }}>
|
|
39
|
-
<div style=${{ width: '40px', height: '40px', borderRadius: '10px', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
|
40
|
-
background: danger ? 'var(--error-glow)' : 'var(--warning-glow)' }}>
|
|
41
|
-
<${danger ? LuTrash2 : LuZap} size=${18} color=${danger ? 'var(--error)' : 'var(--warning)'} />
|
|
42
|
-
</div>
|
|
43
|
-
<div style=${{ fontSize: '15px', fontWeight: 600 }}>Confirm Action</div>
|
|
44
|
-
</div>
|
|
45
|
-
<div style=${{ fontSize: '13px', color: 'var(--text-secondary)', lineHeight: '1.6', marginBottom: '20px' }}>${message}</div>
|
|
46
|
-
<div className="modal-actions">
|
|
47
|
-
<button className="btn btn-ghost" onClick=${onCancel}>Cancel</button>
|
|
48
|
-
<button className=${`btn ${danger ? 'btn-danger' : 'btn-primary'}`} onClick=${onConfirm}>${confirmLabel || 'Confirm'}</button>
|
|
49
|
-
</div>
|
|
50
|
-
<//>
|
|
51
|
-
<//><//>`;
|
|
52
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { html, Icons } from "../lib.js";
|
|
3
|
-
import { useApp } from "../context.js";
|
|
4
|
-
|
|
5
|
-
export function Sidebar() {
|
|
6
|
-
const { page, setPage, data } = useApp();
|
|
7
|
-
const dbOk = data.mongoConfigured;
|
|
8
|
-
|
|
9
|
-
const navItems = [
|
|
10
|
-
{
|
|
11
|
-
id: "dashboard",
|
|
12
|
-
label: "Dashboard",
|
|
13
|
-
icon: Icons.dashboard,
|
|
14
|
-
section: "Overview",
|
|
15
|
-
},
|
|
16
|
-
// AI Builder — temporarily hidden from the menu (re-enable by uncommenting)
|
|
17
|
-
// { id: "ai-builder", label: "AI Builder", icon: Icons.ai },
|
|
18
|
-
{
|
|
19
|
-
id: "database",
|
|
20
|
-
label: "Database",
|
|
21
|
-
icon: Icons.database,
|
|
22
|
-
section: "Setup",
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
id: "schemas",
|
|
26
|
-
label: "Schemas",
|
|
27
|
-
icon: Icons.schema,
|
|
28
|
-
locked: !dbOk,
|
|
29
|
-
lockMsg: "Requires database",
|
|
30
|
-
},
|
|
31
|
-
{ id: "routes", label: "API Routes", icon: Icons.route },
|
|
32
|
-
// { id: "docs", label: "API Docs", icon: Icons.docs }, // hidden for now
|
|
33
|
-
{ id: "api-tester", label: "API Tester", icon: Icons.apiTester },
|
|
34
|
-
{
|
|
35
|
-
id: "security",
|
|
36
|
-
label: "Security",
|
|
37
|
-
icon: Icons.shield,
|
|
38
|
-
section: "Features",
|
|
39
|
-
},
|
|
40
|
-
{ id: "realtime", label: "Real-time", icon: Icons.realtime },
|
|
41
|
-
{ id: "serialport", label: "SerialPort", icon: Icons.serial },
|
|
42
|
-
// { id: "mail", label: "Email", icon: Icons.mail }, // hidden for now
|
|
43
|
-
{ id: "logs", label: "Logs", icon: Icons.logs, section: "Settings" },
|
|
44
|
-
{ id: "env", label: "Environment", icon: Icons.env },
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
let currentSection = null;
|
|
48
|
-
return html`<div className="sidebar">
|
|
49
|
-
<div className="sidebar-brand">
|
|
50
|
-
<pre>${`██╗ ██╗██████╗ ███╗ ██╗ ██████╗ ██████╗ ███████╗\n██║ ██║██╔══██╗████╗ ██║██╔═══██╗██╔══██╗██╔════╝\n███████║██████╔╝██╔██╗ ██║██║ ██║██║ ██║█████╗\n╚════██║██╔══██╗██║╚██╗██║██║ ██║██║ ██║██╔══╝\n ██║██████╔╝██║ ╚████║╚██████╔╝██████╔╝███████╗\n ╚═╝╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝`}</pre>
|
|
51
|
-
</div>
|
|
52
|
-
${navItems.map((item) => {
|
|
53
|
-
const sectionHeader =
|
|
54
|
-
item.section && item.section !== currentSection
|
|
55
|
-
? ((currentSection = item.section),
|
|
56
|
-
html`<div className="sidebar-section">${item.section}</div>`)
|
|
57
|
-
: null;
|
|
58
|
-
return html`<${React.Fragment} key=${item.id}>
|
|
59
|
-
${sectionHeader}
|
|
60
|
-
<div
|
|
61
|
-
className=${`sidebar-item ${page === item.id ? "active" : ""} ${item.locked ? "locked" : ""}`}
|
|
62
|
-
onClick=${() => !item.locked && setPage(item.id)}
|
|
63
|
-
title=${item.locked ? item.lockMsg : item.label}
|
|
64
|
-
>
|
|
65
|
-
<span className="sidebar-icon">${item.icon}</span>
|
|
66
|
-
${item.label}
|
|
67
|
-
${item.locked &&
|
|
68
|
-
html`<span className="lock-icon">${Icons.lock}</span>`}
|
|
69
|
-
</div>
|
|
70
|
-
<//>`;
|
|
71
|
-
})}
|
|
72
|
-
</div>`;
|
|
73
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { html } from "../lib.js";
|
|
2
|
-
import { useApp } from "../context.js";
|
|
3
|
-
|
|
4
|
-
export function ToastContainer() {
|
|
5
|
-
const { toasts } = useApp();
|
|
6
|
-
if (toasts.length === 0) return null;
|
|
7
|
-
return html`<div className="toast-container">
|
|
8
|
-
${toasts.map(
|
|
9
|
-
(t) =>
|
|
10
|
-
html`<div key=${t.id} className=${`toast ${t.type}`}>
|
|
11
|
-
${t.message}
|
|
12
|
-
</div>`,
|
|
13
|
-
)}
|
|
14
|
-
</div>`;
|
|
15
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import htm from "htm";
|
|
3
|
-
import {
|
|
4
|
-
LuLayoutDashboard,
|
|
5
|
-
LuDatabase,
|
|
6
|
-
LuFileText,
|
|
7
|
-
LuRadio,
|
|
8
|
-
LuPlus,
|
|
9
|
-
LuTrash2,
|
|
10
|
-
LuCheck,
|
|
11
|
-
LuLock,
|
|
12
|
-
LuArrowRight,
|
|
13
|
-
LuPencil,
|
|
14
|
-
LuCable,
|
|
15
|
-
LuActivity,
|
|
16
|
-
LuZap,
|
|
17
|
-
LuShield,
|
|
18
|
-
LuBox,
|
|
19
|
-
LuFlaskConical,
|
|
20
|
-
LuSend,
|
|
21
|
-
LuSettings,
|
|
22
|
-
LuScrollText,
|
|
23
|
-
LuSparkles,
|
|
24
|
-
LuBookOpen,
|
|
25
|
-
LuMail,
|
|
26
|
-
} from "react-icons/lu";
|
|
27
|
-
|
|
28
|
-
export const html = htm.bind(React.createElement);
|
|
29
|
-
export const API = "/_dev/api";
|
|
30
|
-
|
|
31
|
-
export function getStoredPasskey() {
|
|
32
|
-
return sessionStorage.getItem("_dev_passkey") || "";
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function copyToClipboard(text) {
|
|
36
|
-
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
37
|
-
return navigator.clipboard.writeText(text);
|
|
38
|
-
}
|
|
39
|
-
const ta = document.createElement("textarea");
|
|
40
|
-
ta.value = text;
|
|
41
|
-
ta.style.cssText = "position:fixed;opacity:0";
|
|
42
|
-
document.body.appendChild(ta);
|
|
43
|
-
ta.select();
|
|
44
|
-
document.execCommand("copy");
|
|
45
|
-
document.body.removeChild(ta);
|
|
46
|
-
return Promise.resolve();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function api(path, options = {}) {
|
|
50
|
-
const passkey = getStoredPasskey();
|
|
51
|
-
const headers = { "Content-Type": "application/json" };
|
|
52
|
-
if (passkey) headers["x-passkey"] = passkey;
|
|
53
|
-
const res = await fetch(API + path, {
|
|
54
|
-
headers,
|
|
55
|
-
...options,
|
|
56
|
-
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
57
|
-
});
|
|
58
|
-
const text = await res.text();
|
|
59
|
-
let data;
|
|
60
|
-
try {
|
|
61
|
-
data = JSON.parse(text);
|
|
62
|
-
} catch {
|
|
63
|
-
if (res.ok) return { message: "Done" };
|
|
64
|
-
throw new Error("Server restarting, please wait...");
|
|
65
|
-
}
|
|
66
|
-
if (!res.ok) {
|
|
67
|
-
const e = new Error(data.error || data.message || "Request failed");
|
|
68
|
-
e.data = data; // expose extra fields (e.g. command output) to callers
|
|
69
|
-
throw e;
|
|
70
|
-
}
|
|
71
|
-
return data;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export const Icons = {
|
|
75
|
-
dashboard: html`<${LuLayoutDashboard} size=${16} />`,
|
|
76
|
-
database: html`<${LuDatabase} size=${16} />`,
|
|
77
|
-
schema: html`<${LuFileText} size=${16} />`,
|
|
78
|
-
route: html`<${LuRadio} size=${16} />`,
|
|
79
|
-
realtime: html`<${LuActivity} size=${16} />`,
|
|
80
|
-
env: html`<${LuSettings} size=${16} />`,
|
|
81
|
-
plus: html`<${LuPlus} size=${16} />`,
|
|
82
|
-
trash: html`<${LuTrash2} size=${16} />`,
|
|
83
|
-
check: html`<${LuCheck} size=${16} />`,
|
|
84
|
-
lock: html`<${LuLock} size=${12} />`,
|
|
85
|
-
arrow: html`<${LuArrowRight} size=${14} />`,
|
|
86
|
-
edit: html`<${LuPencil} size=${16} />`,
|
|
87
|
-
serial: html`<${LuCable} size=${16} />`,
|
|
88
|
-
zap: html`<${LuZap} size=${16} />`,
|
|
89
|
-
shield: html`<${LuShield} size=${16} />`,
|
|
90
|
-
box: html`<${LuBox} size=${16} />`,
|
|
91
|
-
apiTester: html`<${LuFlaskConical} size=${16} />`,
|
|
92
|
-
send: html`<${LuSend} size=${16} />`,
|
|
93
|
-
logs: html`<${LuScrollText} size=${16} />`,
|
|
94
|
-
ai: html`<${LuSparkles} size=${16} />`,
|
|
95
|
-
docs: html`<${LuBookOpen} size=${16} />`,
|
|
96
|
-
mail: html`<${LuMail} size=${16} />`,
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
export const PAGE_TITLES = {
|
|
100
|
-
dashboard: "Dashboard",
|
|
101
|
-
"ai-builder": "AI Builder",
|
|
102
|
-
database: "Database",
|
|
103
|
-
schemas: "Schemas",
|
|
104
|
-
routes: "API Routes",
|
|
105
|
-
docs: "API Docs",
|
|
106
|
-
security: "Security",
|
|
107
|
-
"api-tester": "API Tester",
|
|
108
|
-
realtime: "Real-time",
|
|
109
|
-
serialport: "SerialPort",
|
|
110
|
-
mail: "Email",
|
|
111
|
-
logs: "Logs",
|
|
112
|
-
env: "Environment",
|
|
113
|
-
};
|