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,1485 +0,0 @@
1
- import React, { useState, useEffect, useRef } from "react";
2
- import { LuPlug } from "react-icons/lu";
3
- import { html, api, Icons } from "../lib.js";
4
- import { useApp } from "../context.js";
5
-
6
- export function RoutesPage() {
7
- const { data, toast, refresh, showConfirm } = useApp();
8
- const [showCreate, setShowCreate] = useState(false);
9
- const [selectedRoute, setSelectedRoute] = useState(null);
10
- const [routeContents, setRouteContents] = useState({});
11
- const [loadingRoute, setLoadingRoute] = useState(null);
12
- const [showAddMethod, setShowAddMethod] = useState(false);
13
- const [amRouteType, setAmRouteType] = useState(null);
14
- const [amMethods, setAmMethods] = useState([]);
15
- const [amModelName, setAmModelName] = useState("");
16
- const [amLoginField, setAmLoginField] = useState("");
17
- const [amPasswordField, setAmPasswordField] = useState("");
18
- const [amRegisterFields, setAmRegisterFields] = useState([]);
19
- const [amSelectedFields, setAmSelectedFields] = useState([]);
20
- const [amCustomFields, setAmCustomFields] = useState([]);
21
- const [amNewFieldName, setAmNewFieldName] = useState("");
22
- const [amNewFieldType, setAmNewFieldType] = useState("String");
23
- const [amLoading, setAmLoading] = useState(false);
24
-
25
- // Auto-select first route
26
- useEffect(() => {
27
- if (data.routes?.length > 0 && !selectedRoute && !showCreate) {
28
- selectRoute(data.routes[0]);
29
- }
30
- }, [data.routes]);
31
-
32
- async function selectRoute(filename) {
33
- setShowCreate(false);
34
- setSelectedRoute(filename);
35
- if (!routeContents[filename]) {
36
- setLoadingRoute(filename);
37
- try {
38
- const result = await api("/routes/" + filename + "/content");
39
- setRouteContents((prev) => ({ ...prev, [filename]: result }));
40
- } catch (err) {
41
- toast(err.message, "error");
42
- }
43
- setLoadingRoute(null);
44
- }
45
- }
46
-
47
- function parseEndpoints(content) {
48
- if (!content) return [];
49
- const endpoints = [];
50
- const regex =
51
- /router\.(get|post|put|patch|delete)\(\s*['"`](\/[^'"`]*?)['"`]/gi;
52
- let m;
53
- while ((m = regex.exec(content)) !== null) {
54
- endpoints.push({ method: m[1].toUpperCase(), path: m[2] });
55
- }
56
- return endpoints;
57
- }
58
-
59
- function getMethodsForFile(filename) {
60
- const content = routeContents[filename]?.content;
61
- if (!content) return [];
62
- const methods = [];
63
- const regex = /router\.(get|post|put|patch|delete)\(/gi;
64
- let m;
65
- while ((m = regex.exec(content)) !== null) {
66
- const method = m[1].toUpperCase();
67
- if (!methods.includes(method)) methods.push(method);
68
- }
69
- return methods;
70
- }
71
-
72
- const selName = selectedRoute?.replace(".js", "") || "";
73
- const selContent = routeContents[selectedRoute]?.content;
74
- const selEndpoints = parseEndpoints(selContent);
75
- const hasAuth = selContent?.includes("import auth") || false;
76
- const hasModel = selContent?.match(
77
- /import\s+(\w+)\s+from\s+'\.\.\/models/,
78
- );
79
-
80
- // When no routes yet, show empty + create in a simpler layout
81
- if (data.routes?.length === 0 && !showCreate) {
82
- return html`<div>
83
- <div className="flex items-center justify-between mb-4">
84
- <div>
85
- <div className="card-title">API Routes</div>
86
- <div className="card-subtitle">0 route files</div>
87
- </div>
88
- <button
89
- className="btn btn-primary"
90
- onClick=${() => setShowCreate(true)}
91
- >
92
- <span className="sidebar-icon">${Icons.plus}</span> New Route
93
- </button>
94
- </div>
95
- <div className="empty-state">
96
- <div className="empty-icon"><${LuPlug} size=${36} /></div>
97
- <h3>No routes yet</h3>
98
- <p>Create your first API endpoint to get started</p>
99
- <button
100
- className="btn btn-primary"
101
- onClick=${() => setShowCreate(true)}
102
- >
103
- Create Route
104
- </button>
105
- </div>
106
- </div>`;
107
- }
108
-
109
- return html`<div>
110
- <div className="flex items-center justify-between mb-4">
111
- <div>
112
- <div className="card-title">API Routes</div>
113
- <div className="card-subtitle">
114
- ${data.routes?.length || 0} route
115
- file${data.routes?.length !== 1 ? "s" : ""}
116
- </div>
117
- </div>
118
- <button
119
- className=${`btn ${showCreate ? "btn-ghost" : "btn-primary"}`}
120
- onClick=${() => {
121
- if (showCreate) {
122
- setShowCreate(false);
123
- if (data.routes?.length) selectRoute(data.routes[0]);
124
- } else {
125
- setShowCreate(true);
126
- setSelectedRoute(null);
127
- }
128
- }}
129
- >
130
- ${showCreate
131
- ? "Cancel"
132
- : html`<span className="sidebar-icon">${Icons.plus}</span> New
133
- Route`}
134
- </button>
135
- </div>
136
-
137
- <div className="routes-layout">
138
- <div className="card" style=${{ padding: 0, overflow: "hidden" }}>
139
- <div
140
- style=${{
141
- padding: "14px 16px 10px",
142
- borderBottom: "1px solid var(--border)",
143
- }}
144
- >
145
- <span
146
- style=${{
147
- fontWeight: 600,
148
- fontSize: "12px",
149
- textTransform: "uppercase",
150
- letterSpacing: "0.5px",
151
- color: "var(--text-muted)",
152
- }}
153
- >Files</span
154
- >
155
- </div>
156
- <div className="routes-list">
157
- ${(data.routes || []).map((r) => {
158
- const rName = r.replace(".js", "");
159
- const methods = getMethodsForFile(r);
160
- const isActive = selectedRoute === r && !showCreate;
161
- return html`<div
162
- key=${r}
163
- className=${`route-item ${isActive ? "active" : ""}`}
164
- onClick=${() => selectRoute(r)}
165
- >
166
- <div
167
- style=${{
168
- display: "flex",
169
- flexDirection: "column",
170
- gap: "4px",
171
- flex: 1,
172
- minWidth: 0,
173
- }}
174
- >
175
- <span className="ri-name">${rName}</span>
176
- <span className="ri-file">src/routes/${r}</span>
177
- </div>
178
- <div className="flex gap-1" style=${{ flexWrap: "wrap" }}>
179
- ${methods.length > 0
180
- ? methods.map(
181
- (m) =>
182
- html`<span
183
- key=${m}
184
- className=${`route-method ${m.toLowerCase()}`}
185
- >${m}</span
186
- >`,
187
- )
188
- : html`<span className="route-method multi">...</span>`}
189
- </div>
190
- </div>`;
191
- })}
192
- <div
193
- className=${`route-item ${showCreate ? "active" : ""}`}
194
- onClick=${() => {
195
- setShowCreate(true);
196
- setSelectedRoute(null);
197
- }}
198
- style=${{
199
- borderTop: data.routes?.length
200
- ? "1px solid var(--border)"
201
- : "none",
202
- }}
203
- >
204
- <span
205
- className="sidebar-icon"
206
- style=${{ color: "var(--accent-light)" }}
207
- >${Icons.plus}</span
208
- >
209
- <span
210
- className="ri-name"
211
- style=${{ color: "var(--accent-light)" }}
212
- >New Route</span
213
- >
214
- </div>
215
- </div>
216
- </div>
217
-
218
- <div className="routes-detail">
219
- ${showCreate &&
220
- html`<${CreateRoutePanel}
221
- onCreated=${() => {
222
- setShowCreate(false);
223
- refresh();
224
- }}
225
- />`}
226
- ${!showCreate &&
227
- !selectedRoute &&
228
- html`<div className="card">
229
- <div className="empty-state" style=${{ padding: "40px" }}>
230
- <p>Select a route to view details</p>
231
- </div>
232
- </div>`}
233
- ${!showCreate &&
234
- selectedRoute &&
235
- html`<div>
236
- <div className="card mb-4">
237
- <div className="flex items-center justify-between mb-3">
238
- <div>
239
- <div
240
- style=${{
241
- fontWeight: 600,
242
- fontSize: "16px",
243
- marginBottom: "2px",
244
- }}
245
- >
246
- ${selName}
247
- </div>
248
- <div
249
- className="text-sm text-muted"
250
- style=${{
251
- fontFamily: "'SF Mono', 'Fira Code', monospace",
252
- }}
253
- >
254
- src/routes/${selectedRoute}
255
- </div>
256
- </div>
257
- <div className="flex gap-2 items-center">
258
- ${hasAuth &&
259
- html`<span className="badge badge-warning">Auth</span>`}
260
- ${hasModel &&
261
- html`<span className="badge badge-info"
262
- >Model: ${hasModel[1]}</span
263
- >`}
264
- <button
265
- className=${`btn btn-sm ${showAddMethod ? 'btn-primary' : 'btn-ghost'}`}
266
- onClick=${() => {
267
- setShowAddMethod(!showAddMethod);
268
- setAmRouteType(null);
269
- setAmMethods([]);
270
- setAmModelName("");
271
- setAmLoginField("");
272
- setAmPasswordField("");
273
- setAmRegisterFields([]);
274
- setAmSelectedFields([]);
275
- setAmCustomFields([]);
276
- }}
277
- >
278
- ${showAddMethod ? "Cancel" : html`<span className="sidebar-icon">${Icons.plus}</span> Add`}
279
- </button>
280
- <button
281
- className="btn btn-danger btn-sm btn-icon"
282
- onClick=${async () => {
283
- const ok = await showConfirm('Delete route "' + selName + '"? This will remove the route file and its registration from index.js.', { danger: true, confirmLabel: 'Delete Route' });
284
- if (!ok) return;
285
- try {
286
- await api("/routes/" + selectedRoute, {
287
- method: "DELETE",
288
- });
289
- toast('Route "' + selName + '" deleted', "success");
290
- setSelectedRoute(null);
291
- setRouteContents((prev) => {
292
- const n = { ...prev };
293
- delete n[selectedRoute];
294
- return n;
295
- });
296
- refresh();
297
- } catch (err) {
298
- toast(err.message, "error");
299
- }
300
- }}
301
- >
302
- <span className="sidebar-icon">${Icons.trash}</span>
303
- </button>
304
- </div>
305
- </div>
306
-
307
- ${selEndpoints.length > 0 &&
308
- html`<div>
309
- <div
310
- style=${{
311
- fontSize: "11px",
312
- fontWeight: 600,
313
- textTransform: "uppercase",
314
- letterSpacing: "0.5px",
315
- color: "var(--text-muted)",
316
- marginBottom: "8px",
317
- }}
318
- >
319
- Endpoints
320
- </div>
321
- ${selEndpoints.map(
322
- (ep, i) =>
323
- html`<div className="route-endpoint-row" key=${i}>
324
- <span
325
- className=${`route-method ${ep.method.toLowerCase()}`}
326
- >${ep.method}</span
327
- >
328
- <span className="route-endpoint-path"
329
- >/api/${selName}${ep.path === "/"
330
- ? ""
331
- : ep.path}</span
332
- >
333
- </div>`,
334
- )}
335
- </div>`}
336
- </div>
337
-
338
- ${(() => {
339
- const amNeedsModel = amRouteType && amRouteType !== "methods" && amRouteType !== "plain";
340
- const amIsAuthType = amRouteType === "login" || amRouteType === "register";
341
- const amModelObj = data.models?.find(m => m.name === amModelName);
342
- const amModelFields = amModelObj?.fields || [];
343
- const amMethodColors = { get: "#22c55e", post: "#3b82f6", put: "#f59e0b", patch: "#a855f7", delete: "#ef4444" };
344
- const existingMethods = selEndpoints.map(ep => ep.method.toLowerCase());
345
- const amCanSubmit = amRouteType && (amRouteType !== "methods" || amMethods.length > 0) && (amRouteType !== "crud" || amMethods.length > 0) && (!amNeedsModel || amModelName) && (!amIsAuthType || amLoginField);
346
-
347
- return showAddMethod && html`<div className="card mb-4">
348
- <div style=${{ fontSize: "15px", fontWeight: 600, marginBottom: "14px" }}>Add to ${selName}</div>
349
-
350
- <div className="form-group">
351
- <label className="form-label">Type</label>
352
- <div style=${{ display: "grid", gridTemplateColumns: "repeat(5, 1fr)", gap: "6px" }}>
353
- ${[
354
- { id: "methods", label: "Methods", desc: "GET, POST, PUT...", color: "var(--accent-light)" },
355
- { id: "crud", label: "CRUD", desc: "Model CRUD ops", color: "#3b82f6" },
356
- { id: "login", label: "Login", desc: "JWT login", color: "#22d3ee" },
357
- { id: "register", label: "Register", desc: "Sign up", color: "#34d399" },
358
- ].map(t => {
359
- const active = amRouteType === t.id;
360
- return html`<button key=${t.id} style=${{
361
- padding: "8px 4px", borderRadius: "var(--radius)", cursor: "pointer", textAlign: "center",
362
- border: active ? "1.5px solid " + t.color : "1.5px solid var(--border)",
363
- background: active ? "rgba(255,255,255,0.04)" : "var(--bg-input)",
364
- opacity: active ? 1 : 0.5, transition: "all 0.15s",
365
- }} onClick=${() => { setAmRouteType(prev => prev === t.id ? null : t.id); if (t.id !== "methods" && t.id !== "crud") setAmMethods([]); }}>
366
- <div style=${{ fontSize: "11px", fontWeight: 700, color: active ? t.color : "var(--text-secondary)" }}>${t.label}</div>
367
- <div style=${{ fontSize: "9px", color: "var(--text-muted)", marginTop: "1px" }}>${t.desc}</div>
368
- </button>`;
369
- })}
370
- </div>
371
- </div>
372
-
373
- ${amRouteType === "methods" && html`<div className="form-group">
374
- <label className="form-label">HTTP Methods</label>
375
- <div className="flex gap-2">
376
- ${["get", "post", "put", "patch", "delete"].map(m => html`<button key=${m}
377
- className=${`route-method ${m}`}
378
- style=${{
379
- cursor: existingMethods.includes(m) ? "not-allowed" : "pointer",
380
- border: amMethods.includes(m) ? "2px solid " + amMethodColors[m] : "2px solid transparent",
381
- padding: "6px 12px", fontSize: "11px",
382
- opacity: existingMethods.includes(m) ? 0.15 : amMethods.includes(m) ? 1 : 0.35,
383
- borderRadius: "6px", transition: "all 0.15s",
384
- textDecoration: existingMethods.includes(m) ? "line-through" : "none",
385
- }}
386
- disabled=${existingMethods.includes(m)}
387
- onClick=${() => !existingMethods.includes(m) && setAmMethods(prev => prev.includes(m) ? prev.filter(x => x !== m) : [...prev, m])}
388
- >${m.toUpperCase()}</button>`)}
389
- </div>
390
- </div>`}
391
-
392
- ${amRouteType === "crud" && html`<div className="form-group">
393
- <label className="form-label">CRUD Methods <span style=${{ color: "var(--text-muted)", fontWeight: 400 }}>(select operations)</span></label>
394
- <div className="flex gap-2">
395
- ${["get", "post", "put", "patch", "delete"].map(m => {
396
- const labels = { get: "READ", post: "CREATE", put: "UPDATE", patch: "PATCH", delete: "DELETE" };
397
- return html`<button key=${m} className=${`route-method ${m}`} style=${{
398
- cursor: existingMethods.includes(m) ? "not-allowed" : "pointer",
399
- border: amMethods.includes(m) ? "2px solid " + amMethodColors[m] : "2px solid transparent",
400
- padding: "6px 12px", fontSize: "11px",
401
- opacity: existingMethods.includes(m) ? 0.15 : amMethods.includes(m) ? 1 : 0.35,
402
- borderRadius: "6px", transition: "all 0.15s",
403
- textDecoration: existingMethods.includes(m) ? "line-through" : "none",
404
- }} disabled=${existingMethods.includes(m)}
405
- onClick=${() => !existingMethods.includes(m) && setAmMethods(prev => prev.includes(m) ? prev.filter(x => x !== m) : [...prev, m])}
406
- >${labels[m]}</button>`;
407
- })}
408
- </div>
409
- </div>`}
410
-
411
- ${amNeedsModel && html`<div className="form-group">
412
- <label className="form-label">Model</label>
413
- <select className="form-select" value=${amModelName} onChange=${(e) => { setAmModelName(e.target.value); setAmLoginField(""); setAmPasswordField(""); setAmRegisterFields([]); }}>
414
- <option value="">Select a model...</option>
415
- ${(data.models || []).map(m => html`<option key=${m.name} value=${m.name}>${m.name}</option>`)}
416
- </select>
417
- </div>`}
418
-
419
- ${amRouteType === "methods" && html`<div className="form-group">
420
- <label className="form-label">Fields from Model <span style=${{ color: "var(--text-muted)", fontWeight: 400 }}>(optional)</span></label>
421
- ${data.models?.length > 0 ? html`<select className="form-select" style=${{ marginBottom: "8px" }} value=${amModelName}
422
- onChange=${(e) => { setAmModelName(e.target.value); setAmSelectedFields([]); }}>
423
- <option value="">Select a model...</option>
424
- ${data.models.map(m => html`<option key=${m.name} value=${m.name}>${m.name}</option>`)}
425
- </select>
426
- ${amModelName && amModelFields.length > 0 && html`<div className="flex gap-2" style=${{ flexWrap: "wrap" }}>
427
- ${amModelFields.map(f => {
428
- const active = amSelectedFields.includes(f.name);
429
- return html`<button key=${f.name} style=${{
430
- padding: "5px 12px", borderRadius: "6px", fontSize: "12px", cursor: "pointer",
431
- border: active ? "1.5px solid var(--accent)" : "1.5px solid var(--border)",
432
- background: active ? "var(--accent-glow)" : "var(--bg-input)",
433
- color: active ? "var(--accent-light)" : "var(--text-secondary)",
434
- fontWeight: active ? 600 : 400, transition: "all 0.15s",
435
- }} onClick=${() => setAmSelectedFields(prev => active ? prev.filter(x => x !== f.name) : [...prev, f.name])}>
436
- ${f.name} <span style=${{ fontSize: "10px", color: "var(--text-muted)", marginLeft: "4px" }}>${f.type}</span>
437
- </button>`;
438
- })}
439
- </div>`}` : html`<div style=${{ fontSize: "11px", color: "var(--text-muted)" }}>No models found.</div>`}
440
- </div>`}
441
-
442
- ${amRouteType === "login" && amModelName && amModelFields.length > 0 && html`<div className="form-group">
443
- <label className="form-label">Find user by</label>
444
- <div className="flex gap-2" style=${{ flexWrap: "wrap" }}>
445
- ${amModelFields.map(f => html`<button key=${f.name} style=${{
446
- padding: "5px 12px", borderRadius: "6px", fontSize: "12px", cursor: "pointer",
447
- border: amLoginField === f.name ? "1.5px solid var(--accent)" : "1.5px solid var(--border)",
448
- background: amLoginField === f.name ? "var(--accent-glow)" : "var(--bg-input)",
449
- color: amLoginField === f.name ? "var(--accent-light)" : "var(--text-secondary)",
450
- fontWeight: amLoginField === f.name ? 600 : 400, transition: "all 0.15s",
451
- }} onClick=${() => setAmLoginField(f.name)}>${f.name}</button>`)}
452
- </div>
453
- </div>`}
454
-
455
- ${amIsAuthType && amModelName && amModelFields.length > 0 && html`<div className="form-group">
456
- <label className="form-label">Password field <span style=${{ color: "var(--text-muted)", fontWeight: 400 }}>(optional — enables bcrypt)</span></label>
457
- <div className="flex gap-2" style=${{ flexWrap: "wrap" }}>
458
- <button style=${{
459
- padding: "5px 12px", borderRadius: "6px", fontSize: "12px", cursor: "pointer",
460
- border: !amPasswordField ? "1.5px solid var(--accent)" : "1.5px solid var(--border)",
461
- background: !amPasswordField ? "var(--accent-glow)" : "var(--bg-input)",
462
- color: !amPasswordField ? "var(--accent-light)" : "var(--text-secondary)",
463
- fontWeight: !amPasswordField ? 600 : 400, transition: "all 0.15s",
464
- }} onClick=${() => setAmPasswordField("")}>None</button>
465
- ${amModelFields.map(f => html`<button key=${f.name} style=${{
466
- padding: "5px 12px", borderRadius: "6px", fontSize: "12px", cursor: "pointer",
467
- border: amPasswordField === f.name ? "1.5px solid var(--warning)" : "1.5px solid var(--border)",
468
- background: amPasswordField === f.name ? "var(--warning-glow)" : "var(--bg-input)",
469
- color: amPasswordField === f.name ? "var(--warning)" : "var(--text-secondary)",
470
- fontWeight: amPasswordField === f.name ? 600 : 400, transition: "all 0.15s",
471
- }} onClick=${() => setAmPasswordField(f.name)}>${f.name}</button>`)}
472
- </div>
473
- </div>`}
474
-
475
- ${amRouteType === "register" && amModelName && amModelFields.length > 0 && html`<div className="form-group">
476
- <label className="form-label">Registration fields</label>
477
- <div className="flex gap-2" style=${{ flexWrap: "wrap" }}>
478
- ${amModelFields.map(f => {
479
- const active = amRegisterFields.includes(f.name);
480
- return html`<button key=${f.name} style=${{
481
- padding: "5px 12px", borderRadius: "6px", fontSize: "12px", cursor: "pointer",
482
- border: active ? "1.5px solid var(--success)" : "1.5px solid var(--border)",
483
- background: active ? "var(--success-glow)" : "var(--bg-input)",
484
- color: active ? "var(--success)" : "var(--text-secondary)",
485
- fontWeight: active ? 600 : 400, transition: "all 0.15s",
486
- }} onClick=${() => setAmRegisterFields(prev => active ? prev.filter(x => x !== f.name) : [...prev, f.name])}>${f.name}</button>`;
487
- })}
488
- </div>
489
- </div>`}
490
-
491
- ${amRouteType === "register" && amModelName && amModelFields.length > 0 && html`<div className="form-group">
492
- <label className="form-label">Check duplicate by</label>
493
- <div className="flex gap-2" style=${{ flexWrap: "wrap" }}>
494
- ${amModelFields.map(f => html`<button key=${f.name} style=${{
495
- padding: "5px 12px", borderRadius: "6px", fontSize: "12px", cursor: "pointer",
496
- border: amLoginField === f.name ? "1.5px solid var(--accent)" : "1.5px solid var(--border)",
497
- background: amLoginField === f.name ? "var(--accent-glow)" : "var(--bg-input)",
498
- color: amLoginField === f.name ? "var(--accent-light)" : "var(--text-secondary)",
499
- fontWeight: amLoginField === f.name ? 600 : 400, transition: "all 0.15s",
500
- }} onClick=${() => setAmLoginField(f.name)}>${f.name}</button>`)}
501
- </div>
502
- </div>`}
503
-
504
- ${amRouteType === "methods" && html`<div className="form-group">
505
- <label className="form-label">Custom Fields <span style=${{ color: "var(--text-muted)", fontWeight: 400 }}>(from client)</span></label>
506
- ${amCustomFields.length > 0 && html`<div style=${{ display: "flex", flexWrap: "wrap", gap: "6px", marginBottom: "8px" }}>
507
- ${amCustomFields.map((f, i) => html`<span key=${i} style=${{
508
- padding: "4px 10px", borderRadius: "6px", fontSize: "12px",
509
- background: "var(--bg-input)", border: "1.5px solid var(--border)",
510
- color: "var(--text-primary)", display: "flex", alignItems: "center", gap: "6px",
511
- }}>
512
- ${f.name} <span style=${{ fontSize: "10px", color: f.type === "File" ? "var(--warning)" : "var(--text-muted)" }}>${f.type}</span>
513
- <span style=${{ cursor: "pointer", color: "var(--text-muted)", fontSize: "14px", lineHeight: 1 }}
514
- onClick=${() => setAmCustomFields(prev => prev.filter((_, idx) => idx !== i))}>×</span>
515
- </span>`)}
516
- </div>`}
517
- <div className="flex gap-2" style=${{ alignItems: "center" }}>
518
- <input className="form-input" value=${amNewFieldName}
519
- onInput=${(e) => setAmNewFieldName(e.target.value)} placeholder="Field name"
520
- style=${{ fontSize: "13px", padding: "8px 12px", flex: 1 }}
521
- onKeyDown=${(e) => { if (e.key === "Enter" && amNewFieldName.trim()) { setAmCustomFields(prev => [...prev, { name: amNewFieldName.trim(), type: amNewFieldType }]); setAmNewFieldName(""); }}} />
522
- <select className="form-select" value=${amNewFieldType} onChange=${(e) => setAmNewFieldType(e.target.value)} style=${{ width: "auto", minWidth: "100px" }}>
523
- <option value="String">String</option><option value="Number">Number</option><option value="Boolean">Boolean</option>
524
- <option value="Date">Date</option><option value="Array">Array</option><option value="Object">Object</option><option value="File">File</option>
525
- </select>
526
- <button className="btn btn-sm" style=${{
527
- padding: "8px 14px", fontSize: "12px", background: "var(--bg-input)",
528
- border: "1.5px solid var(--border)", color: "var(--text-primary)",
529
- cursor: amNewFieldName.trim() ? "pointer" : "not-allowed",
530
- opacity: amNewFieldName.trim() ? 1 : 0.4, borderRadius: "6px",
531
- }} disabled=${!amNewFieldName.trim()} onClick=${() => { if (amNewFieldName.trim()) { setAmCustomFields(prev => [...prev, { name: amNewFieldName.trim(), type: amNewFieldType }]); setAmNewFieldName(""); } }}>+ Add</button>
532
- </div>
533
- </div>`}
534
-
535
- ${amCanSubmit && html`<button className="btn btn-primary" disabled=${amLoading}
536
- onClick=${async () => {
537
- setAmLoading(true);
538
- try {
539
- const result = await api("/routes/" + selectedRoute + "/add-method", {
540
- method: "POST",
541
- body: {
542
- routeType: amRouteType,
543
- methods: (amRouteType === "methods" || amRouteType === "crud") ? amMethods : undefined,
544
- modelName: amModelName || undefined,
545
- loginField: amIsAuthType ? amLoginField : undefined,
546
- passwordField: amIsAuthType && amPasswordField ? amPasswordField : undefined,
547
- registerFields: amRouteType === "register" ? amRegisterFields : undefined,
548
- selectedFields: amRouteType === "methods" && amSelectedFields.length > 0 ? amSelectedFields : undefined,
549
- customFields: amRouteType === "methods" && amCustomFields.length > 0 ? amCustomFields : undefined,
550
- },
551
- });
552
- toast(result.message, "success");
553
- setRouteContents(prev => ({ ...prev, [selectedRoute]: { content: result.content } }));
554
- setShowAddMethod(false);
555
- refresh();
556
- } catch (err) { toast(err.message, "error"); }
557
- setAmLoading(false);
558
- }}
559
- >${amLoading ? html`<span className="spinner" />` : "Add to Route"}</button>`}
560
- </div>`;
561
- })()}
562
-
563
- <div
564
- className="card"
565
- style=${{ padding: 0, overflow: "hidden" }}
566
- >
567
- <div
568
- style=${{
569
- padding: "12px 16px",
570
- borderBottom: "1px solid var(--border)",
571
- display: "flex",
572
- alignItems: "center",
573
- justifyContent: "space-between",
574
- }}
575
- >
576
- <span
577
- style=${{
578
- fontSize: "12px",
579
- fontWeight: 600,
580
- color: "var(--text-secondary)",
581
- }}
582
- >Source Code</span
583
- >
584
- <span className="field-count"
585
- >${selContent
586
- ? selContent.split("\\n").length + " lines"
587
- : ""}</span
588
- >
589
- </div>
590
- ${loadingRoute === selectedRoute &&
591
- html`<div style=${{ padding: "30px", textAlign: "center" }}>
592
- <span className="spinner" />
593
- </div>`}
594
- ${selContent &&
595
- html`<div className="route-code-wrap">
596
- <div className="code-block">${selContent}</div>
597
- </div>`}
598
- </div>
599
- </div>`}
600
- </div>
601
- </div>
602
- </div>`;
603
- }
604
-
605
- function CreateRoutePanel({ onCreated }) {
606
- const { data, toast } = useApp();
607
- const [name, setName] = useState("");
608
- const [routeType, setRouteType] = useState(null);
609
- const [methods, setMethods] = useState([]);
610
- const pathPrefix = "/api/";
611
- const [modelName, setModelName] = useState("");
612
- const [loginField, setLoginField] = useState("");
613
- const [passwordField, setPasswordField] = useState("");
614
- const [registerFields, setRegisterFields] = useState([]);
615
- const [selectedFields, setSelectedFields] = useState([]);
616
- const [customFields, setCustomFields] = useState([]);
617
- const [authRequired, setAuthRequired] = useState(false);
618
- const [newFieldName, setNewFieldName] = useState("");
619
- const [newFieldType, setNewFieldType] = useState("String");
620
- const [newFieldRequired, setNewFieldRequired] = useState(true);
621
- const [loading, setLoading] = useState(false);
622
- const nameRef = useRef(null);
623
-
624
- useEffect(() => {
625
- nameRef.current?.focus();
626
- }, []);
627
-
628
- const typeOptions = [
629
- {
630
- id: "plain",
631
- label: "Plain",
632
- desc: "Empty route",
633
- color: "#94a3b8",
634
- },
635
- {
636
- id: "methods",
637
- label: "Methods",
638
- desc: "GET, POST, PUT...",
639
- color: "var(--accent-light)",
640
- },
641
- {
642
- id: "crud",
643
- label: "CRUD",
644
- desc: "Model CRUD ops",
645
- color: "#3b82f6",
646
- },
647
- { id: "login", label: "Login", desc: "JWT login", color: "#22d3ee" },
648
- {
649
- id: "register",
650
- label: "Register",
651
- desc: "Sign up",
652
- color: "#34d399",
653
- },
654
- ];
655
-
656
- function selectType(id) {
657
- setRouteType((prev) => (prev === id ? null : id));
658
- if (id !== "methods" && id !== "crud") setMethods([]);
659
- }
660
-
661
- function toggleMethod(m) {
662
- setMethods((prev) =>
663
- prev.includes(m) ? prev.filter((x) => x !== m) : [...prev, m],
664
- );
665
- }
666
-
667
- const needsModel =
668
- routeType && routeType !== "methods" && routeType !== "plain";
669
- const isAuthType = routeType === "login" || routeType === "register";
670
-
671
- // Get fields from selected model
672
- const selectedModelObj = data.models?.find((m) => m.name === modelName);
673
- const modelFields = selectedModelObj?.fields || [];
674
-
675
- // Auto-select fields when model changes
676
- useEffect(() => {
677
- if (modelFields.length > 0 && isAuthType) {
678
- if (!loginField) {
679
- const emailField =
680
- modelFields.find((f) => f.name === "email") || modelFields[0];
681
- setLoginField(emailField?.name || "");
682
- }
683
- if (!passwordField) {
684
- const pwField = modelFields.find((f) => f.name === "password");
685
- setPasswordField(pwField?.name || "");
686
- }
687
- if (routeType === "register" && registerFields.length === 0) {
688
- setRegisterFields(modelFields.map((f) => f.name));
689
- }
690
- }
691
- }, [modelName, routeType]);
692
-
693
- async function handleCreate() {
694
- if (!name.trim() || !routeType) return;
695
- if (routeType === "methods" && methods.length === 0) {
696
- toast("Select at least one method", "error");
697
- return;
698
- }
699
- if (routeType === "crud" && methods.length === 0) {
700
- toast("Select at least one CRUD method", "error");
701
- return;
702
- }
703
- if (needsModel && !modelName) {
704
- toast("Select a model", "error");
705
- return;
706
- }
707
- if (isAuthType && !loginField) {
708
- toast("Select a login field", "error");
709
- return;
710
- }
711
- setLoading(true);
712
- try {
713
- const result = await api("/add-api", {
714
- method: "POST",
715
- body: {
716
- name: name.trim(),
717
- routeType,
718
- pathPrefix,
719
- methods:
720
- routeType === "methods" || routeType === "crud"
721
- ? methods
722
- : undefined,
723
- modelName: modelName || undefined,
724
- loginField: isAuthType ? loginField : undefined,
725
- passwordField:
726
- isAuthType && passwordField ? passwordField : undefined,
727
- registerFields:
728
- routeType === "register" ? registerFields : undefined,
729
- selectedFields:
730
- (routeType === "methods" || routeType === "crud") && selectedFields.length > 0
731
- ? selectedFields
732
- : undefined,
733
- customFields:
734
- (routeType === "methods" || routeType === "crud") && customFields.length > 0
735
- ? customFields
736
- : undefined,
737
- authRequired: !isAuthType ? authRequired : undefined,
738
- },
739
- });
740
- toast(result.message, "success");
741
- onCreated();
742
- } catch (err) {
743
- toast(err.message, "error");
744
- }
745
- setLoading(false);
746
- }
747
-
748
- const methodColors = {
749
- get: "#22c55e",
750
- post: "#3b82f6",
751
- put: "#f59e0b",
752
- patch: "#a855f7",
753
- delete: "#ef4444",
754
- };
755
- const canCreate =
756
- name.trim() &&
757
- routeType &&
758
- (routeType !== "methods" || methods.length > 0) &&
759
- (routeType !== "crud" || methods.length > 0) &&
760
- (!needsModel || modelName) &&
761
- (!isAuthType || loginField);
762
-
763
- return html`<div>
764
- <div className="card mb-4">
765
- <div
766
- className="flex items-center justify-between"
767
- style=${{ marginBottom: "16px" }}
768
- >
769
- <div style=${{ fontSize: "15px", fontWeight: 600 }}>
770
- Create New Route
771
- </div>
772
- <button
773
- className="btn btn-primary btn-sm"
774
- onClick=${handleCreate}
775
- disabled=${loading || !canCreate}
776
- >
777
- ${loading
778
- ? html`<span className="spinner" />`
779
- : html`<span className="sidebar-icon">${Icons.plus}</span>`}
780
- ${loading ? "Creating..." : "Create Route"}
781
- </button>
782
- </div>
783
-
784
- <div className="form-group">
785
- <label className="form-label">Route Name</label>
786
- <input
787
- ref=${nameRef}
788
- className="form-input"
789
- value=${name}
790
- onInput=${(e) => setName(e.target.value)}
791
- onKeyDown=${(e) =>
792
- e.key === "Enter" && canCreate && handleCreate()}
793
- placeholder="e.g. users, login, products"
794
- style=${{ fontSize: "14px", padding: "11px 14px" }}
795
- />
796
- </div>
797
-
798
- <div className="form-group">
799
- <label className="form-label">Type</label>
800
- <div
801
- style=${{
802
- display: "grid",
803
- gridTemplateColumns: "repeat(5, 1fr)",
804
- gap: "6px",
805
- }}
806
- >
807
- ${typeOptions.map((t) => {
808
- const active = routeType === t.id;
809
- return html`<button
810
- key=${t.id}
811
- style=${{
812
- padding: "8px 4px",
813
- borderRadius: "var(--radius)",
814
- cursor: "pointer",
815
- textAlign: "center",
816
- border: active
817
- ? "1.5px solid " + t.color
818
- : "1.5px solid var(--border)",
819
- background: active
820
- ? "rgba(255,255,255,0.04)"
821
- : "var(--bg-input)",
822
- opacity: active ? 1 : 0.5,
823
- transition: "all 0.15s",
824
- }}
825
- onClick=${() => selectType(t.id)}
826
- >
827
- <div
828
- style=${{
829
- fontSize: "11px",
830
- fontWeight: 700,
831
- color: active ? t.color : "var(--text-secondary)",
832
- }}
833
- >
834
- ${t.label}
835
- </div>
836
- <div
837
- style=${{
838
- fontSize: "9px",
839
- color: "var(--text-muted)",
840
- marginTop: "1px",
841
- }}
842
- >
843
- ${t.desc}
844
- </div>
845
- </button>`;
846
- })}
847
- </div>
848
- </div>
849
-
850
- ${routeType === "methods" &&
851
- html`<div className="form-group">
852
- <label className="form-label">HTTP Methods</label>
853
- <div className="flex gap-2">
854
- ${["get", "post", "put", "patch", "delete"].map(
855
- (m) =>
856
- html`<button
857
- key=${m}
858
- className=${`route-method ${m}`}
859
- style=${{
860
- cursor: "pointer",
861
- border: methods.includes(m)
862
- ? "2px solid " + methodColors[m]
863
- : "2px solid transparent",
864
- padding: "6px 12px",
865
- fontSize: "11px",
866
- opacity: methods.includes(m) ? 1 : 0.35,
867
- borderRadius: "6px",
868
- transition: "all 0.15s",
869
- }}
870
- onClick=${() => toggleMethod(m)}
871
- >
872
- ${m.toUpperCase()}
873
- </button>`,
874
- )}
875
- </div>
876
- </div>`}
877
- ${routeType === "crud" &&
878
- html`<div className="form-group">
879
- <label className="form-label"
880
- >CRUD Methods
881
- <span style=${{ color: "var(--text-muted)", fontWeight: 400 }}
882
- >(select which operations to include)</span
883
- ></label
884
- >
885
- <div className="flex gap-2">
886
- ${["get", "post", "put", "patch", "delete"].map((m) => {
887
- const labels = {
888
- get: "READ",
889
- post: "CREATE",
890
- put: "UPDATE",
891
- patch: "PATCH",
892
- delete: "DELETE",
893
- };
894
- return html`<button
895
- key=${m}
896
- className=${`route-method ${m}`}
897
- style=${{
898
- cursor: "pointer",
899
- border: methods.includes(m)
900
- ? "2px solid " + methodColors[m]
901
- : "2px solid transparent",
902
- padding: "6px 12px",
903
- fontSize: "11px",
904
- opacity: methods.includes(m) ? 1 : 0.35,
905
- borderRadius: "6px",
906
- transition: "all 0.15s",
907
- }}
908
- onClick=${() => toggleMethod(m)}
909
- >
910
- ${labels[m]}
911
- </button>`;
912
- })}
913
- </div>
914
- </div>`}
915
- ${isAuthType &&
916
- html`<div className="form-group">
917
- <label className="form-label">Model</label>
918
- <select
919
- className="form-select"
920
- value=${modelName}
921
- onChange=${(e) => {
922
- setModelName(e.target.value);
923
- setLoginField("");
924
- setPasswordField("");
925
- setRegisterFields([]);
926
- setSelectedFields([]);
927
- }}
928
- >
929
- <option value="">Select a model...</option>
930
- ${(data.models || []).map(
931
- (m) =>
932
- html`<option key=${m.name} value=${m.name}>
933
- ${m.name}
934
- </option>`,
935
- )}
936
- </select>
937
- ${!data.models?.length &&
938
- html`<div
939
- style=${{
940
- fontSize: "11px",
941
- color: "var(--warning)",
942
- marginTop: "4px",
943
- }}
944
- >
945
- No models found. Create a schema first.
946
- </div>`}
947
- </div>`}
948
- ${(routeType === "methods" || routeType === "crud") &&
949
- html`<div className="form-group">
950
- <label className="form-label"
951
- >Model Fields
952
- <span style=${{ color: "var(--text-muted)", fontWeight: 400 }}
953
- >(select fields to include in request)</span
954
- ></label
955
- >
956
- <select
957
- className="form-select"
958
- style=${{ marginBottom: "8px" }}
959
- value=${modelName}
960
- onChange=${(e) => {
961
- setModelName(e.target.value);
962
- setSelectedFields([]);
963
- }}
964
- >
965
- <option value="">Select a model...</option>
966
- ${(data.models || []).map(
967
- (m) =>
968
- html`<option key=${m.name} value=${m.name}>
969
- ${m.name}
970
- </option>`,
971
- )}
972
- </select>
973
- ${modelName &&
974
- modelFields.length > 0 &&
975
- html`<div>
976
- <div className="flex gap-2" style=${{ flexWrap: "wrap" }}>
977
- ${modelFields.map((f) => {
978
- const active = selectedFields.includes(f.name);
979
- return html`<button
980
- key=${f.name}
981
- style=${{
982
- padding: "5px 12px",
983
- borderRadius: "6px",
984
- fontSize: "12px",
985
- cursor: "pointer",
986
- border: active
987
- ? "1.5px solid var(--accent)"
988
- : "1.5px solid var(--border)",
989
- background: active
990
- ? "var(--accent-glow)"
991
- : "var(--bg-input)",
992
- color: active
993
- ? "var(--accent-light)"
994
- : "var(--text-secondary)",
995
- fontWeight: active ? 600 : 400,
996
- transition: "all 0.15s",
997
- }}
998
- onClick=${() =>
999
- setSelectedFields((prev) =>
1000
- active
1001
- ? prev.filter((x) => x !== f.name)
1002
- : [...prev, f.name],
1003
- )}
1004
- >
1005
- ${f.name}
1006
- <span
1007
- style=${{
1008
- fontSize: "10px",
1009
- color: "var(--text-muted)",
1010
- marginLeft: "4px",
1011
- }}
1012
- >${f.type}</span
1013
- >
1014
- </button>`;
1015
- })}
1016
- </div>
1017
- <div style=${{ display: "flex", gap: "8px", marginTop: "8px" }}>
1018
- <button
1019
- style=${{
1020
- padding: "3px 10px",
1021
- fontSize: "10px",
1022
- borderRadius: "4px",
1023
- border: "1px solid var(--border)",
1024
- background: "transparent",
1025
- color: "var(--accent)",
1026
- cursor: "pointer",
1027
- }}
1028
- onClick=${() => setSelectedFields(modelFields.map((f) => f.name))}
1029
- >
1030
- Select All
1031
- </button>
1032
- <button
1033
- style=${{
1034
- padding: "3px 10px",
1035
- fontSize: "10px",
1036
- borderRadius: "4px",
1037
- border: "1px solid var(--border)",
1038
- background: "transparent",
1039
- color: "var(--text-muted)",
1040
- cursor: "pointer",
1041
- }}
1042
- onClick=${() => setSelectedFields([])}
1043
- >
1044
- Clear
1045
- </button>
1046
- </div>
1047
- </div>`}
1048
- ${!data.models?.length &&
1049
- html`<div
1050
- style=${{
1051
- fontSize: "11px",
1052
- color: "var(--text-muted)",
1053
- marginTop: "4px",
1054
- }}
1055
- >
1056
- No models found. You can still add custom fields below.
1057
- </div>`}
1058
- </div>`}
1059
- ${(routeType === "methods" || routeType === "crud") &&
1060
- html`<div className="form-group">
1061
- <label className="form-label"
1062
- >Custom Fields
1063
- <span style=${{ color: "var(--text-muted)", fontWeight: 400 }}
1064
- >(add extra fields received from client)</span
1065
- ></label
1066
- >
1067
- ${customFields.length > 0 &&
1068
- html`<div
1069
- style=${{
1070
- display: "flex",
1071
- flexWrap: "wrap",
1072
- gap: "6px",
1073
- marginBottom: "8px",
1074
- }}
1075
- >
1076
- ${customFields.map(
1077
- (f, i) =>
1078
- html`<span
1079
- key=${i}
1080
- style=${{
1081
- padding: "4px 10px",
1082
- borderRadius: "6px",
1083
- fontSize: "12px",
1084
- background: "var(--bg-input)",
1085
- border: "1.5px solid var(--border)",
1086
- color: "var(--text-primary)",
1087
- display: "flex",
1088
- alignItems: "center",
1089
- gap: "6px",
1090
- }}
1091
- >
1092
- ${f.name}
1093
- <span
1094
- style=${{
1095
- fontSize: "10px",
1096
- color:
1097
- f.type === "File"
1098
- ? "var(--warning)"
1099
- : "var(--text-muted)",
1100
- }}
1101
- >${f.type}</span
1102
- >
1103
- <span
1104
- title="Click to toggle required / optional"
1105
- style=${{
1106
- cursor: "pointer",
1107
- fontSize: "9px",
1108
- fontWeight: 600,
1109
- textTransform: "uppercase",
1110
- letterSpacing: "0.3px",
1111
- padding: "1px 5px",
1112
- borderRadius: "4px",
1113
- color:
1114
- f.required === false
1115
- ? "var(--text-muted)"
1116
- : "var(--success)",
1117
- background:
1118
- f.required === false
1119
- ? "var(--bg-input)"
1120
- : "var(--success-glow)",
1121
- }}
1122
- onClick=${() =>
1123
- setCustomFields((prev) =>
1124
- prev.map((x, idx) =>
1125
- idx === i
1126
- ? { ...x, required: x.required === false }
1127
- : x,
1128
- ),
1129
- )}
1130
- >${f.required === false ? "optional" : "required"}</span
1131
- >
1132
- <span
1133
- style=${{
1134
- cursor: "pointer",
1135
- color: "var(--text-muted)",
1136
- fontSize: "14px",
1137
- lineHeight: 1,
1138
- marginLeft: "2px",
1139
- }}
1140
- onClick=${() =>
1141
- setCustomFields((prev) =>
1142
- prev.filter((_, idx) => idx !== i),
1143
- )}
1144
- >×</span
1145
- >
1146
- </span>`,
1147
- )}
1148
- </div>`}
1149
- <div className="flex gap-2" style=${{ alignItems: "center" }}>
1150
- <input
1151
- className="form-input"
1152
- value=${newFieldName}
1153
- onInput=${(e) => setNewFieldName(e.target.value)}
1154
- placeholder="Field name"
1155
- style=${{
1156
- fontSize: "13px",
1157
- padding: "8px 12px",
1158
- flex: 1,
1159
- }}
1160
- onKeyDown=${(e) => {
1161
- if (e.key === "Enter" && newFieldName.trim()) {
1162
- setCustomFields((prev) => [
1163
- ...prev,
1164
- { name: newFieldName.trim(), type: newFieldType, required: newFieldRequired },
1165
- ]);
1166
- setNewFieldName("");
1167
- }
1168
- }}
1169
- />
1170
- <select
1171
- className="form-select"
1172
- value=${newFieldType}
1173
- onChange=${(e) => setNewFieldType(e.target.value)}
1174
- style=${{ width: "auto", minWidth: "100px" }}
1175
- >
1176
- <option value="String">String</option>
1177
- <option value="Number">Number</option>
1178
- <option value="Boolean">Boolean</option>
1179
- <option value="Date">Date</option>
1180
- <option value="Array">Array</option>
1181
- <option value="Object">Object</option>
1182
- <option value="File">File</option>
1183
- </select>
1184
- <label
1185
- title="New field is required in the request body (validated by Zod)"
1186
- style=${{
1187
- display: "flex",
1188
- alignItems: "center",
1189
- gap: "5px",
1190
- fontSize: "12px",
1191
- color: "var(--text-secondary)",
1192
- cursor: "pointer",
1193
- whiteSpace: "nowrap",
1194
- }}
1195
- >
1196
- <input
1197
- type="checkbox"
1198
- checked=${newFieldRequired}
1199
- onChange=${(e) => setNewFieldRequired(e.target.checked)}
1200
- />
1201
- required
1202
- </label>
1203
- <button
1204
- className="btn btn-sm"
1205
- style=${{
1206
- padding: "8px 14px",
1207
- fontSize: "12px",
1208
- background: "var(--bg-input)",
1209
- border: "1.5px solid var(--border)",
1210
- color: "var(--text-primary)",
1211
- cursor: newFieldName.trim() ? "pointer" : "not-allowed",
1212
- opacity: newFieldName.trim() ? 1 : 0.4,
1213
- borderRadius: "6px",
1214
- }}
1215
- disabled=${!newFieldName.trim()}
1216
- onClick=${() => {
1217
- if (newFieldName.trim()) {
1218
- setCustomFields((prev) => [
1219
- ...prev,
1220
- { name: newFieldName.trim(), type: newFieldType, required: newFieldRequired },
1221
- ]);
1222
- setNewFieldName("");
1223
- }
1224
- }}
1225
- >
1226
- + Add
1227
- </button>
1228
- </div>
1229
- </div>`}
1230
- ${routeType && !isAuthType &&
1231
- html`<div className="form-group" style=${{ marginTop: "4px" }}>
1232
- <label style=${{ display: "flex", alignItems: "center", gap: "8px", fontSize: "13px", cursor: "pointer" }}>
1233
- <input type="checkbox" checked=${authRequired} onChange=${(e) => setAuthRequired(e.target.checked)} />
1234
- <span>Require authentication (JWT)</span>
1235
- </label>
1236
- <div className="text-sm text-muted" style=${{ marginTop: "4px", marginLeft: "24px" }}>
1237
- Protects every endpoint with the <span style=${{ fontFamily: "monospace" }}>auth</span> middleware — callers must send a valid Bearer token in the Authorization header.
1238
- </div>
1239
- </div>`}
1240
- ${routeType === "login" &&
1241
- modelName &&
1242
- modelFields.length > 0 &&
1243
- html`<div className="form-group">
1244
- <label className="form-label">Find user by</label>
1245
- <div className="flex gap-2" style=${{ flexWrap: "wrap" }}>
1246
- ${modelFields.map(
1247
- (f) =>
1248
- html`<button
1249
- key=${f.name}
1250
- style=${{
1251
- padding: "5px 12px",
1252
- borderRadius: "6px",
1253
- fontSize: "12px",
1254
- cursor: "pointer",
1255
- border:
1256
- loginField === f.name
1257
- ? "1.5px solid var(--accent)"
1258
- : "1.5px solid var(--border)",
1259
- background:
1260
- loginField === f.name
1261
- ? "var(--accent-glow)"
1262
- : "var(--bg-input)",
1263
- color:
1264
- loginField === f.name
1265
- ? "var(--accent-light)"
1266
- : "var(--text-secondary)",
1267
- fontWeight: loginField === f.name ? 600 : 400,
1268
- transition: "all 0.15s",
1269
- }}
1270
- onClick=${() => setLoginField(f.name)}
1271
- >
1272
- ${f.name}
1273
- </button>`,
1274
- )}
1275
- </div>
1276
- </div>`}
1277
- ${routeType === "login" &&
1278
- modelName &&
1279
- modelFields.length > 0 &&
1280
- html`<div className="form-group">
1281
- <label className="form-label"
1282
- >Password field
1283
- <span style=${{ color: "var(--text-muted)", fontWeight: 400 }}
1284
- >(optional — enables bcrypt)</span
1285
- ></label
1286
- >
1287
- <div className="flex gap-2" style=${{ flexWrap: "wrap" }}>
1288
- <button
1289
- style=${{
1290
- padding: "5px 12px",
1291
- borderRadius: "6px",
1292
- fontSize: "12px",
1293
- cursor: "pointer",
1294
- border: !passwordField
1295
- ? "1.5px solid var(--accent)"
1296
- : "1.5px solid var(--border)",
1297
- background: !passwordField
1298
- ? "var(--accent-glow)"
1299
- : "var(--bg-input)",
1300
- color: !passwordField
1301
- ? "var(--accent-light)"
1302
- : "var(--text-secondary)",
1303
- fontWeight: !passwordField ? 600 : 400,
1304
- transition: "all 0.15s",
1305
- }}
1306
- onClick=${() => setPasswordField("")}
1307
- >
1308
- None
1309
- </button>
1310
- ${modelFields.map(
1311
- (f) =>
1312
- html`<button
1313
- key=${f.name}
1314
- style=${{
1315
- padding: "5px 12px",
1316
- borderRadius: "6px",
1317
- fontSize: "12px",
1318
- cursor: "pointer",
1319
- border:
1320
- passwordField === f.name
1321
- ? "1.5px solid var(--warning)"
1322
- : "1.5px solid var(--border)",
1323
- background:
1324
- passwordField === f.name
1325
- ? "var(--warning-glow)"
1326
- : "var(--bg-input)",
1327
- color:
1328
- passwordField === f.name
1329
- ? "var(--warning)"
1330
- : "var(--text-secondary)",
1331
- fontWeight: passwordField === f.name ? 600 : 400,
1332
- transition: "all 0.15s",
1333
- }}
1334
- onClick=${() => setPasswordField(f.name)}
1335
- >
1336
- ${f.name}
1337
- </button>`,
1338
- )}
1339
- </div>
1340
- </div>`}
1341
- ${routeType === "register" &&
1342
- modelName &&
1343
- modelFields.length > 0 &&
1344
- html`<div className="form-group">
1345
- <label className="form-label">Registration fields</label>
1346
- <div className="flex gap-2" style=${{ flexWrap: "wrap" }}>
1347
- ${modelFields.map((f) => {
1348
- const active = registerFields.includes(f.name);
1349
- return html`<button
1350
- key=${f.name}
1351
- style=${{
1352
- padding: "5px 12px",
1353
- borderRadius: "6px",
1354
- fontSize: "12px",
1355
- cursor: "pointer",
1356
- border: active
1357
- ? "1.5px solid var(--success)"
1358
- : "1.5px solid var(--border)",
1359
- background: active
1360
- ? "var(--success-glow)"
1361
- : "var(--bg-input)",
1362
- color: active
1363
- ? "var(--success)"
1364
- : "var(--text-secondary)",
1365
- fontWeight: active ? 600 : 400,
1366
- transition: "all 0.15s",
1367
- }}
1368
- onClick=${() =>
1369
- setRegisterFields((prev) =>
1370
- active
1371
- ? prev.filter((x) => x !== f.name)
1372
- : [...prev, f.name],
1373
- )}
1374
- >
1375
- ${f.name}
1376
- </button>`;
1377
- })}
1378
- </div>
1379
- </div>`}
1380
- ${routeType === "register" &&
1381
- modelName &&
1382
- modelFields.length > 0 &&
1383
- html`<div className="form-group">
1384
- <label className="form-label">Check duplicate by</label>
1385
- <div className="flex gap-2" style=${{ flexWrap: "wrap" }}>
1386
- ${modelFields.map(
1387
- (f) =>
1388
- html`<button
1389
- key=${f.name}
1390
- style=${{
1391
- padding: "5px 12px",
1392
- borderRadius: "6px",
1393
- fontSize: "12px",
1394
- cursor: "pointer",
1395
- border:
1396
- loginField === f.name
1397
- ? "1.5px solid var(--accent)"
1398
- : "1.5px solid var(--border)",
1399
- background:
1400
- loginField === f.name
1401
- ? "var(--accent-glow)"
1402
- : "var(--bg-input)",
1403
- color:
1404
- loginField === f.name
1405
- ? "var(--accent-light)"
1406
- : "var(--text-secondary)",
1407
- fontWeight: loginField === f.name ? 600 : 400,
1408
- transition: "all 0.15s",
1409
- }}
1410
- onClick=${() => setLoginField(f.name)}
1411
- >
1412
- ${f.name}
1413
- </button>`,
1414
- )}
1415
- </div>
1416
- </div>`}
1417
- ${routeType === "register" &&
1418
- modelName &&
1419
- modelFields.length > 0 &&
1420
- html`<div className="form-group">
1421
- <label className="form-label"
1422
- >Password field
1423
- <span style=${{ color: "var(--text-muted)", fontWeight: 400 }}
1424
- >(optional — saves as hashed)</span
1425
- ></label
1426
- >
1427
- <div className="flex gap-2" style=${{ flexWrap: "wrap" }}>
1428
- <button
1429
- style=${{
1430
- padding: "5px 12px",
1431
- borderRadius: "6px",
1432
- fontSize: "12px",
1433
- cursor: "pointer",
1434
- border: !passwordField
1435
- ? "1.5px solid var(--accent)"
1436
- : "1.5px solid var(--border)",
1437
- background: !passwordField
1438
- ? "var(--accent-glow)"
1439
- : "var(--bg-input)",
1440
- color: !passwordField
1441
- ? "var(--accent-light)"
1442
- : "var(--text-secondary)",
1443
- fontWeight: !passwordField ? 600 : 400,
1444
- transition: "all 0.15s",
1445
- }}
1446
- onClick=${() => setPasswordField("")}
1447
- >
1448
- None
1449
- </button>
1450
- ${registerFields.map(
1451
- (f) =>
1452
- html`<button
1453
- key=${f}
1454
- style=${{
1455
- padding: "5px 12px",
1456
- borderRadius: "6px",
1457
- fontSize: "12px",
1458
- cursor: "pointer",
1459
- border:
1460
- passwordField === f
1461
- ? "1.5px solid var(--warning)"
1462
- : "1.5px solid var(--border)",
1463
- background:
1464
- passwordField === f
1465
- ? "var(--warning-glow)"
1466
- : "var(--bg-input)",
1467
- color:
1468
- passwordField === f
1469
- ? "var(--warning)"
1470
- : "var(--text-secondary)",
1471
- fontWeight: passwordField === f ? 600 : 400,
1472
- transition: "all 0.15s",
1473
- }}
1474
- onClick=${() => setPasswordField(f)}
1475
- >
1476
- ${f}
1477
- </button>`,
1478
- )}
1479
- </div>
1480
- </div>`}
1481
-
1482
- </div>
1483
- </div>`;
1484
- }
1485
-