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/app-1/src/docs.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import crypto from 'crypto';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
|
|
7
|
-
const router = express.Router();
|
|
8
|
-
const DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const SPEC = path.join(DIR, 'openapi.json');
|
|
10
|
-
const AUTH = path.join(DIR, 'docs-auth.json');
|
|
11
|
-
|
|
12
|
-
// Optional password protection. If src/docs-auth.json holds a password hash, the
|
|
13
|
-
// whole /docs surface requires a single password (NO username) via a small login
|
|
14
|
-
// page + cookie. Set/remove the password from the dashboard (API Docs). Read live
|
|
15
|
-
// (mtime-cached), so changes apply without a restart.
|
|
16
|
-
let _authCache = { mtimeMs: -1, hash: null };
|
|
17
|
-
function docsHash() {
|
|
18
|
-
try {
|
|
19
|
-
const { mtimeMs } = fs.statSync(AUTH);
|
|
20
|
-
if (mtimeMs !== _authCache.mtimeMs) {
|
|
21
|
-
const data = JSON.parse(fs.readFileSync(AUTH, 'utf8'));
|
|
22
|
-
_authCache = { mtimeMs, hash: (data && data.hash) || null };
|
|
23
|
-
}
|
|
24
|
-
} catch { _authCache = { mtimeMs: -1, hash: null }; }
|
|
25
|
-
return _authCache.hash;
|
|
26
|
-
}
|
|
27
|
-
function safeEq(a, b) {
|
|
28
|
-
try { const x = Buffer.from(String(a)), y = Buffer.from(String(b)); return x.length === y.length && crypto.timingSafeEqual(x, y); } catch { return false; }
|
|
29
|
-
}
|
|
30
|
-
function cookieToken(req) {
|
|
31
|
-
const m = (req.headers.cookie || '').match(/(?:^|; )docs_auth=([^;]+)/);
|
|
32
|
-
return m ? m[1] : null;
|
|
33
|
-
}
|
|
34
|
-
const LOGIN_HTML = "<!doctype html>\n<html lang=\"en\"><head>\n<meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>API Documentation</title>\n<style>\n *{box-sizing:border-box}\n body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#0f1117;color:#eceef4;font-family:\"Noto Sans\",-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif}\n form{background:#1a1d2b;border:1px solid rgba(255,255,255,0.08);border-radius:14px;padding:28px;width:340px;max-width:90vw;text-align:center}\n h1{font-size:18px;margin:0 0 6px;background:linear-gradient(135deg,#3daefd,#0079ff);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}\n p{color:#8b8fa3;font-size:13px;margin:0 0 18px}\n input{width:100%;background:#13161f;border:1px solid rgba(255,255,255,0.08);border-radius:9px;color:#eceef4;padding:11px 13px;font-size:14px;outline:none}\n input:focus{border-color:#0079ff}\n .err{color:#ef4444;font-size:12px;min-height:16px;margin:8px 0 0}\n button{width:100%;margin-top:14px;background:#0079ff;color:#fff;border:none;border-radius:9px;padding:11px;font-size:14px;font-weight:600;cursor:pointer}\n button:hover{filter:brightness(1.08)}\n</style></head>\n<body>\n<form method=\"post\" action=\"/docs/login\">\n <h1>API Documentation</h1>\n <p>Enter the password to view the docs.</p>\n <input type=\"password\" name=\"password\" placeholder=\"Password\" autofocus autocomplete=\"current-password\" />\n <div class=\"err\"><!--ERR--></div>\n <button type=\"submit\">Unlock</button>\n</form>\n</body></html>";
|
|
35
|
-
|
|
36
|
-
// Password submit — reachable without auth.
|
|
37
|
-
router.post('/login', (req, res) => {
|
|
38
|
-
const expected = docsHash();
|
|
39
|
-
if (!expected) return res.redirect('/docs');
|
|
40
|
-
const got = crypto.createHash('sha256').update(String((req.body && req.body.password) || '')).digest('hex');
|
|
41
|
-
if (safeEq(got, expected)) {
|
|
42
|
-
res.setHeader('Set-Cookie', 'docs_auth=' + expected + '; Path=/docs; HttpOnly; SameSite=Lax; Max-Age=86400');
|
|
43
|
-
return res.redirect('/docs');
|
|
44
|
-
}
|
|
45
|
-
return res.status(401).type('html').send(LOGIN_HTML.replace('<!--ERR-->', 'Incorrect password'));
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// Gate the rest of /docs behind the password (when one is set).
|
|
49
|
-
router.use((req, res, next) => {
|
|
50
|
-
const expected = docsHash();
|
|
51
|
-
if (!expected) return next(); // no password → open
|
|
52
|
-
if (safeEq(cookieToken(req), expected)) return next();
|
|
53
|
-
return res.status(401).type('html').send(LOGIN_HTML);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// Interactive API documentation. The page is fully self-contained (no external
|
|
57
|
-
// CDN); the spec is generated by 4bnode and served from this app.
|
|
58
|
-
router.get('/openapi.json', (req, res) => {
|
|
59
|
-
res.set('Cache-Control', 'no-store');
|
|
60
|
-
if (!fs.existsSync(SPEC)) return res.status(404).json({ error: 'API spec not generated yet.' });
|
|
61
|
-
res.sendFile(SPEC);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
const DOCS_PAGE = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>API Documentation</title>\n<style>\n * { box-sizing: border-box; }\n :root {\n --bg:#0f1117; --surface:#11141e; --card:#1a1d2b; --card-hover:#21253a; --input:#13161f;\n --border:rgba(255,255,255,0.08); --text:#eceef4; --muted:#8b8fa3; --faint:#626880;\n --accent:#0079ff; --accent-light:#3daefd; --accent-glow:rgba(0,121,255,0.14);\n }\n * { scrollbar-width: thin; }\n html, body { margin:0; padding:0; height:100%; }\n body { background:var(--bg); color:var(--text); line-height:1.5; height:100vh;\n display:flex; flex-direction:column; overflow:hidden;\n font-family:\"Noto Sans\",-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif; }\n .topbar { flex:none; display:flex; align-items:center; gap:14px;\n background:var(--surface); border-bottom:1px solid var(--border); padding:14px 24px; }\n .topbar h1 { font-size:17px; font-weight:700; margin:0;\n background:linear-gradient(135deg,#3daefd,#0079ff); -webkit-background-clip:text;\n background-clip:text; -webkit-text-fill-color:transparent; }\n .pill-ver { font-size:12px; color:var(--accent-light); background:rgba(0,121,255,0.12);\n border:1px solid rgba(0,121,255,0.25); padding:3px 9px; border-radius:999px; }\n .layout { flex:1; display:flex; min-height:0; }\n .sidebar { width:330px; flex:none; border-right:1px solid var(--border);\n overflow-y:auto; padding:14px 12px; }\n .search { width:100%; background:var(--input); border:1px solid var(--border);\n color:var(--text); border-radius:9px; padding:8px 12px; font-size:13px; outline:none; margin-bottom:12px; }\n .search:focus { border-color:var(--accent); }\n .group-title { font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.6px;\n color:var(--faint); margin:14px 6px 6px; }\n .nav-item { display:flex; align-items:center; gap:9px; padding:8px 10px; border-radius:8px;\n cursor:pointer; border:1px solid transparent; }\n .nav-item:hover { background:var(--card-hover); }\n .nav-item.active { background:var(--accent-glow); border-color:rgba(0,121,255,0.35); }\n .nav-method { font-size:9.5px; font-weight:700; letter-spacing:0.4px; text-transform:uppercase;\n color:#fff; padding:3px 0; border-radius:5px; width:46px; flex:none; text-align:center; }\n .nav-path { font-family:ui-monospace,Menlo,monospace; font-size:12.5px; color:var(--text);\n overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n .main { flex:1; overflow-y:auto; padding:30px 40px; }\n .main-empty { color:var(--faint); display:flex; height:100%; align-items:center; justify-content:center; }\n .detail-head { display:flex; align-items:center; gap:14px; margin-bottom:6px; }\n .method-lg { font-size:13px; font-weight:700; letter-spacing:0.5px; text-transform:uppercase;\n color:#fff; padding:6px 12px; border-radius:8px; }\n .path-lg { font-family:ui-monospace,Menlo,monospace; font-size:18px; color:var(--text); word-break:break-all; }\n .detail-sum { color:var(--muted); margin:0 0 8px; }\n .lock { color:var(--faint); font-size:12px; margin-left:8px; white-space:nowrap; }\n .sec { font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.5px;\n color:var(--faint); margin:22px 0 8px; }\n .row { display:flex; gap:10px; align-items:center; padding:8px 0;\n border-bottom:1px solid var(--border); font-size:13px; }\n .row:last-child { border-bottom:none; }\n .fname { font-family:ui-monospace,Menlo,monospace; color:var(--text); min-width:150px; }\n .ftype { color:var(--accent-light); font-size:12px; }\n .req { color:#ef4444; font-size:11px; font-weight:700; }\n .opt { color:var(--faint); font-size:11px; }\n .resp-code { font-family:ui-monospace,Menlo,monospace; color:#22c55e; font-weight:700; }\n input.f, textarea.f { width:100%; max-width:540px; background:var(--input); border:1px solid var(--border);\n color:var(--text); border-radius:8px; padding:8px 10px; font-size:13px; outline:none;\n font-family:ui-monospace,Menlo,monospace; }\n input.f:focus, textarea.f:focus { border-color:var(--accent); }\n textarea.f { min-height:110px; resize:vertical; }\n .try-field { margin-bottom:10px; }\n .try-label { font-size:12px; color:var(--muted); margin-bottom:4px; display:block; }\n .btn { background:var(--accent); color:#fff; border:none; border-radius:8px; padding:9px 18px;\n font-size:13px; font-weight:600; cursor:pointer; }\n .btn:hover { filter:brightness(1.08); }\n .btn:disabled { opacity:.5; cursor:not-allowed; }\n pre.out { background:#0a0c12; border:1px solid var(--border); border-radius:8px; padding:12px;\n font-size:12.5px; color:#d5d8e3; overflow:auto; max-height:360px; max-width:540px; white-space:pre-wrap;\n word-break:break-word; margin-top:10px; }\n .out-status { font-family:ui-monospace,Menlo,monospace; font-size:12px; margin-top:10px; }\n ::-webkit-scrollbar { width:9px; height:9px; }\n ::-webkit-scrollbar-thumb { background:rgba(255,255,255,0.12); border-radius:6px; }\n</style>\n</head>\n<body>\n<div class=\"topbar\">\n <h1 id=\"apiTitle\">API</h1>\n <span class=\"pill-ver\" id=\"apiVersion\">v1.0.0</span>\n</div>\n<div class=\"layout\">\n <aside class=\"sidebar\">\n <input class=\"search\" id=\"search\" placeholder=\"Filter endpoints...\" />\n <div id=\"nav\"></div>\n </aside>\n <main class=\"main\" id=\"main\"><div class=\"main-empty\">Select an endpoint to view its details.</div></main>\n</div>\n<script>\n(function(){\n var SPEC_URL='/docs/openapi.json';\n var COLORS={get:'#0079ff',post:'#22c55e',put:'#f59e0b',patch:'#a855f7',delete:'#ef4444',head:'#64748b',options:'#64748b'};\n var uid=0; var ITEMS=[];\n function esc(s){return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\"/g,'"');}\n function typeName(sc){if(!sc)return'';if(sc.format==='binary')return'file';if(sc.$ref)return sc.$ref.split('/').pop();return sc.type||'string';}\n function paramsOf(op,where){return (op.parameters||[]).filter(function(p){return p.in===where;});}\n function bodyOf(op){if(!op.requestBody)return null;var c=op.requestBody.content||{};var mime=Object.keys(c)[0];if(!mime)return null;return {mime:mime,schema:(c[mime]||{}).schema||{}};}\n function sampleVal(sc){var t=typeName(sc);if(t==='integer'||t==='number')return 0;if(t==='boolean')return false;return'';}\n function bodySkeleton(schema){var o={};var p=(schema&&schema.properties)||{};Object.keys(p).forEach(function(k){o[k]=sampleVal(p[k]);});return JSON.stringify(o,null,2);}\n function groupKey(p){return (p.split('/').filter(Boolean)[0]||'general').replace(/[{}]/g,'');}\n function collect(spec){var out=[];var paths=spec.paths||{};Object.keys(paths).forEach(function(p){var item=paths[p];Object.keys(item).forEach(function(m){out.push({path:p,method:m,op:item[m]});});});return out;}\n\n function detailsHtml(e,id){\n var html='';\n var pp=paramsOf(e.op,'path'),qp=paramsOf(e.op,'query'),body=bodyOf(e.op);\n if(pp.length||qp.length){\n html+='<div class=\"sec\">Parameters</div>';\n pp.concat(qp).forEach(function(p){\n html+='<div class=\"row\"><span class=\"fname\">'+esc(p.name)+'</span><span class=\"ftype\">'+esc(typeName(p.schema)||'string')+' · '+esc(p.in)+'</span>'+(p.required?'<span class=\"req\">required</span>':'<span class=\"opt\">optional</span>')+'</div>';\n });\n }\n if(body){\n html+='<div class=\"sec\">Request body <span class=\"opt\">('+esc(body.mime)+')</span></div>';\n var props=(body.schema&&body.schema.properties)||{};var reqd=(body.schema&&body.schema.required)||[];\n Object.keys(props).forEach(function(k){\n html+='<div class=\"row\"><span class=\"fname\">'+esc(k)+'</span><span class=\"ftype\">'+esc(typeName(props[k]))+'</span>'+(reqd.indexOf(k)>=0?'<span class=\"req\">required</span>':'<span class=\"opt\">optional</span>')+'</div>';\n });\n }\n html+='<div class=\"sec\">Responses</div>';\n var resp=e.op.responses||{};\n Object.keys(resp).forEach(function(code){\n html+='<div class=\"row\"><span class=\"resp-code\">'+esc(code)+'</span><span class=\"detail-sum\" style=\"margin:0\">'+esc((resp[code]||{}).description||'')+'</span></div>';\n });\n html+='<div class=\"sec\">Try it</div>';\n pp.forEach(function(p){html+='<div class=\"try-field\"><label class=\"try-label\">'+esc(p.name)+' (path)</label><input class=\"f\" data-kind=\"path\" data-name=\"'+esc(p.name)+'\" /></div>';});\n qp.forEach(function(p){html+='<div class=\"try-field\"><label class=\"try-label\">'+esc(p.name)+' (query)</label><input class=\"f\" data-kind=\"query\" data-name=\"'+esc(p.name)+'\" /></div>';});\n if(e.op.security){html+='<div class=\"try-field\"><label class=\"try-label\">Authorization (Bearer token)</label><input class=\"f\" id=\"'+id+'_auth\" placeholder=\"token\" /></div>';}\n if(body){html+='<div class=\"try-field\"><label class=\"try-label\">Body (JSON)</label><textarea class=\"f\" id=\"'+id+'_body\">'+esc(bodySkeleton(body.schema))+'</textarea></div>';}\n html+='<button class=\"btn\" id=\"'+id+'_send\">Send request</button>';\n html+='<div class=\"out-status\" id=\"'+id+'_status\"></div>';\n html+='<pre class=\"out\" id=\"'+id+'_out\" style=\"display:none\"></pre>';\n return html;\n }\n\n function wireTry(e,id,root){\n var btn=root.querySelector('#'+id+'_send');if(!btn)return;\n // Auto-grow the body textarea to fit its content (capped, then scrolls).\n var bodyTa=root.querySelector('#'+id+'_body');\n if(bodyTa){\n var autosize=function(){ bodyTa.style.height='auto'; bodyTa.style.height=Math.min(bodyTa.scrollHeight+2,600)+'px'; };\n bodyTa.addEventListener('input',autosize);\n autosize();\n }\n btn.addEventListener('click',function(){\n var url=e.path;\n root.querySelectorAll('[data-kind=path]').forEach(function(inp){url=url.replace('{'+inp.getAttribute('data-name')+'}',encodeURIComponent(inp.value||''));});\n var qs=[];\n root.querySelectorAll('[data-kind=query]').forEach(function(inp){if(inp.value)qs.push(encodeURIComponent(inp.getAttribute('data-name'))+'='+encodeURIComponent(inp.value));});\n if(qs.length)url+=(url.indexOf('?')>=0?'&':'?')+qs.join('&');\n var opts={method:e.method.toUpperCase(),headers:{}};\n var auth=root.querySelector('#'+id+'_auth');if(auth&&auth.value)opts.headers['Authorization']='Bearer '+auth.value;\n var bodyEl=root.querySelector('#'+id+'_body');\n if(bodyEl&&['post','put','patch'].indexOf(e.method)>=0){opts.headers['Content-Type']='application/json';opts.body=bodyEl.value;}\n var statusEl=root.querySelector('#'+id+'_status');var outEl=root.querySelector('#'+id+'_out');\n btn.disabled=true;statusEl.textContent='Sending...';var t0=Date.now();\n fetch(url,opts).then(function(r){var ct=r.headers.get('content-type')||'';return r.text().then(function(txt){return {r:r,txt:txt,ct:ct};});}).then(function(d){\n var color=d.r.ok?'#22c55e':'#ef4444';\n statusEl.innerHTML='<span style=\"color:'+color+'\">'+d.r.status+' '+esc(d.r.statusText)+'</span> · '+(Date.now()-t0)+'ms';\n var out=d.txt;if(d.ct.indexOf('application/json')>=0){try{out=JSON.stringify(JSON.parse(d.txt),null,2);}catch(err){}}\n outEl.style.display='block';outEl.textContent=out;\n }).catch(function(err){statusEl.innerHTML='<span style=\"color:#ef4444\">Request failed</span>';outEl.style.display='block';outEl.textContent=String(err);}).then(function(){btn.disabled=false;});\n });\n }\n\n function showDetail(e,navEl){\n document.querySelectorAll('.nav-item').forEach(function(n){n.classList.remove('active');});\n if(navEl)navEl.classList.add('active');\n var id='ep'+(uid++);\n var main=document.getElementById('main');\n var color=COLORS[e.method]||'#64748b';\n var lock=e.op.security?'<span class=\"lock\">🔒 requires auth</span>':'';\n var html='<div class=\"detail-head\"><span class=\"method-lg\" style=\"background:'+color+'\">'+esc(e.method)+'</span><span class=\"path-lg\">'+esc(e.path)+'</span></div>';\n html+='<div class=\"detail-sum\">'+esc(e.op.summary||'')+lock+'</div>';\n html+=detailsHtml(e,id);\n main.innerHTML=html;main.scrollTop=0;\n wireTry(e,id,main);\n }\n\n function render(spec){\n document.getElementById('apiTitle').textContent=(spec.info&&spec.info.title)||'API';\n document.getElementById('apiVersion').textContent='v'+((spec.info&&spec.info.version)||'1.0.0');\n ITEMS=collect(spec);\n var nav=document.getElementById('nav');nav.innerHTML='';\n if(!ITEMS.length){\n nav.innerHTML='<div class=\"group-title\">No endpoints</div>';\n document.getElementById('main').innerHTML='<div class=\"main-empty\">No endpoints in the spec yet. Add routes, then regenerate the docs from the dashboard (API Docs).</div>';\n return;\n }\n var groups={};ITEMS.forEach(function(e){var k=groupKey(e.path);(groups[k]=groups[k]||[]).push(e);});\n var first=null;\n Object.keys(groups).sort().forEach(function(k){\n var gt=document.createElement('div');gt.className='group-title';gt.textContent=k;nav.appendChild(gt);\n groups[k].forEach(function(e){\n var color=COLORS[e.method]||'#64748b';\n var item=document.createElement('div');item.className='nav-item';\n item.setAttribute('data-search',(e.method+' '+e.path+' '+(e.op.summary||'')).toLowerCase());\n item.innerHTML='<span class=\"nav-method\" style=\"background:'+color+'\">'+esc(e.method)+'</span><span class=\"nav-path\">'+esc(e.path)+'</span>';\n item.addEventListener('click',function(){showDetail(e,item);});\n nav.appendChild(item);\n if(!first)first={e:e,item:item};\n });\n });\n if(first)showDetail(first.e,first.item);\n }\n\n document.getElementById('search').addEventListener('input',function(ev){\n var q=ev.target.value.toLowerCase();\n document.querySelectorAll('.nav-item').forEach(function(c){c.style.display=c.getAttribute('data-search').indexOf(q)>=0?'':'none';});\n });\n\n // The server inlines the spec below (so a protected page needs no second\n // authenticated request — avoids cross-browser cookie quirks on fetch). If it\n // isn't inlined (e.g. the page is served statically), fall back to fetching.\n var SPEC_DATA=/*__SPEC__*/null;\n if(SPEC_DATA&&typeof SPEC_DATA==='object'){\n render(SPEC_DATA);\n } else {\n fetch(SPEC_URL,{credentials:'same-origin'}).then(function(r){\n if(!r.ok){throw new Error('the server returned HTTP '+r.status+'. Regenerate the docs from the dashboard (API Docs).');}\n return r.json();\n }).then(render).catch(function(err){\n document.getElementById('main').innerHTML='<div class=\"main-empty\">Could not load the API spec — '+esc(String((err&&err.message)||err))+'</div>';\n });\n }\n})();\n</script>\n</body>\n</html>";
|
|
65
|
-
|
|
66
|
-
router.get('/', (req, res) => {
|
|
67
|
-
// Inline the spec into the page so a password-protected page doesn't need a
|
|
68
|
-
// second authenticated request (some browsers won't attach the docs cookie to
|
|
69
|
-
// the fetch). Falls back to fetching /openapi.json if no spec is inlined.
|
|
70
|
-
let spec = 'null';
|
|
71
|
-
try {
|
|
72
|
-
if (fs.existsSync(SPEC)) {
|
|
73
|
-
const raw = fs.readFileSync(SPEC, 'utf8').trim();
|
|
74
|
-
// Escape '<' so a spec value containing '</script>' can't break out of the
|
|
75
|
-
// inline <script>. The '\u003c' sequence still evaluates to '<' in JS.
|
|
76
|
-
if (raw) spec = raw.split('<').join(String.fromCharCode(92) + 'u003c');
|
|
77
|
-
}
|
|
78
|
-
} catch {}
|
|
79
|
-
// Never cache the docs page — keeps it from going stale after a regenerate.
|
|
80
|
-
res.set('Cache-Control', 'no-store');
|
|
81
|
-
res.type('html').send(DOCS_PAGE.replace('/*__SPEC__*/null', () => spec));
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
export default router;
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// Central error handling. Register AFTER all routes:
|
|
2
|
-
// app.use(notFound);
|
|
3
|
-
// app.use(errorHandler);
|
|
4
|
-
export function notFound(req, res, next) {
|
|
5
|
-
res.status(404).json({ message: `Not found - ${req.originalUrl}` });
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function errorHandler(err, req, res, next) {
|
|
9
|
-
const status = err.status || err.statusCode || 500;
|
|
10
|
-
console.error(err);
|
|
11
|
-
res.status(status).json({
|
|
12
|
-
message: err.message || 'Server error',
|
|
13
|
-
...(process.env.NODE_ENV === 'production' ? {} : { stack: err.stack }),
|
|
14
|
-
});
|
|
15
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
// Role-based access control. Use AFTER the JWT auth middleware so req.user is set:
|
|
2
|
-
// router.get('/admin', auth, roles('admin'), handler);
|
|
3
|
-
// Requires the login token payload to include a `role` (e.g. jwt.sign({ id, role })).
|
|
4
|
-
const roles = (...allowed) => (req, res, next) => {
|
|
5
|
-
if (!req.user) {
|
|
6
|
-
return res.status(401).json({ message: 'Not authenticated' });
|
|
7
|
-
}
|
|
8
|
-
if (allowed.length && !allowed.includes(req.user.role)) {
|
|
9
|
-
return res.status(403).json({ message: 'Forbidden: insufficient role' });
|
|
10
|
-
}
|
|
11
|
-
next();
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export default roles;
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { ZodError } from 'zod';
|
|
2
|
-
|
|
3
|
-
// Validate part of the request against a Zod schema. Replaces req[source] with the
|
|
4
|
-
// parsed/coerced data on success. Use as middleware:
|
|
5
|
-
// router.post('/', validate(userSchema), handler); // validates req.body
|
|
6
|
-
// router.get('/', validate(querySchema, 'query'), handler); // validates req.query
|
|
7
|
-
const validate = (schema, source = 'body') => (req, res, next) => {
|
|
8
|
-
const result = schema.safeParse(req[source]);
|
|
9
|
-
if (!result.success) {
|
|
10
|
-
const errors = (result.error instanceof ZodError ? result.error.issues : []).map((i) => ({
|
|
11
|
-
path: i.path.join('.'),
|
|
12
|
-
message: i.message,
|
|
13
|
-
}));
|
|
14
|
-
return res.status(400).json({ message: 'Validation failed', errors });
|
|
15
|
-
}
|
|
16
|
-
req[source] = result.data;
|
|
17
|
-
next();
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export default validate;
|
package/app-1/src/openapi.json
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"openapi": "3.0.0",
|
|
3
|
-
"info": {
|
|
4
|
-
"title": "app-1",
|
|
5
|
-
"version": "1.0.0"
|
|
6
|
-
},
|
|
7
|
-
"servers": [
|
|
8
|
-
{
|
|
9
|
-
"url": "/"
|
|
10
|
-
}
|
|
11
|
-
],
|
|
12
|
-
"paths": {
|
|
13
|
-
"/api/products": {
|
|
14
|
-
"post": {
|
|
15
|
-
"summary": "POST /api/products",
|
|
16
|
-
"responses": {
|
|
17
|
-
"200": {
|
|
18
|
-
"description": "Success"
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
"requestBody": {
|
|
22
|
-
"content": {
|
|
23
|
-
"application/json": {
|
|
24
|
-
"schema": {
|
|
25
|
-
"type": "object",
|
|
26
|
-
"properties": {
|
|
27
|
-
"first_name": {
|
|
28
|
-
"type": "string"
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
"security": [
|
|
36
|
-
{
|
|
37
|
-
"bearerAuth": []
|
|
38
|
-
}
|
|
39
|
-
]
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
"components": {
|
|
44
|
-
"securitySchemes": {
|
|
45
|
-
"bearerAuth": {
|
|
46
|
-
"type": "http",
|
|
47
|
-
"scheme": "bearer",
|
|
48
|
-
"bearerFormat": "JWT"
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
"schemas": {
|
|
52
|
-
"Login": {
|
|
53
|
-
"type": "object",
|
|
54
|
-
"properties": {
|
|
55
|
-
"first_name": {
|
|
56
|
-
"type": "string"
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { productsSchema } from '../validators/products.js';
|
|
2
|
-
import validate from '../middleware/validate.js';
|
|
3
|
-
import express from 'express';
|
|
4
|
-
import jwt from 'jsonwebtoken';
|
|
5
|
-
import Login from '../models/login.js';
|
|
6
|
-
|
|
7
|
-
const router = express.Router();
|
|
8
|
-
|
|
9
|
-
// Register
|
|
10
|
-
router.post('/', validate(productsSchema), async (req, res) => {
|
|
11
|
-
try {
|
|
12
|
-
const { first_name } = req.body;
|
|
13
|
-
const existing = await Login.findOne({ first_name });
|
|
14
|
-
if (existing) return res.status(400).json({ message: 'Login already exists' });
|
|
15
|
-
|
|
16
|
-
const item = new Login({ first_name });
|
|
17
|
-
const saved = await item.save();
|
|
18
|
-
|
|
19
|
-
const token = jwt.sign({ id: saved._id }, process.env.JWT_SECRET, { expiresIn: '7d' });
|
|
20
|
-
res.status(201).json({ token, login: { id: saved._id, first_name: saved.first_name } });
|
|
21
|
-
} catch (err) {
|
|
22
|
-
res.status(400).json({ message: err.message });
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
export default router;
|