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
@@ -25,33 +25,66 @@ export function LogsPage() {
25
25
  const [connected, setConnected] = useState(false);
26
26
  const bottomRef = useRef(null);
27
27
  const containerRef = useRef(null);
28
- const eventSourceRef = useRef(null);
29
28
 
30
29
  // Load existing logs
31
30
  useEffect(() => {
32
31
  api("/logs").then(setLogs).catch(() => {});
33
32
  }, []);
34
33
 
35
- // SSE stream for real-time logs
34
+ // SSE stream for real-time logs.
35
+ // Uses fetch() so the passkey travels in the x-passkey header instead of the
36
+ // query string (EventSource can't set headers, which leaked it into logs).
36
37
  useEffect(() => {
37
- const passkey = getStoredPasskey();
38
- const url = API + "/logs/stream" + (passkey ? "?passkey=" + encodeURIComponent(passkey) : "");
39
- const es = new EventSource(url);
40
- eventSourceRef.current = es;
38
+ const controller = new AbortController();
39
+ let aborted = false;
40
+ let retryTimer = null;
41
41
 
42
- es.onopen = () => setConnected(true);
43
- es.onmessage = (e) => {
42
+ async function stream() {
43
+ const passkey = getStoredPasskey();
44
44
  try {
45
- const entry = JSON.parse(e.data);
46
- setLogs((prev) => {
47
- const next = [...prev, entry];
48
- return next.length > 500 ? next.slice(-500) : next;
45
+ const res = await fetch(API + "/logs/stream", {
46
+ headers: passkey ? { "x-passkey": passkey } : {},
47
+ signal: controller.signal,
49
48
  });
50
- } catch {}
51
- };
52
- es.onerror = () => setConnected(false);
49
+ if (!res.ok || !res.body) throw new Error("stream failed");
50
+ setConnected(true);
51
+ const reader = res.body.getReader();
52
+ const decoder = new TextDecoder();
53
+ let buffer = "";
54
+ while (true) {
55
+ const { value, done } = await reader.read();
56
+ if (done) break;
57
+ buffer += decoder.decode(value, { stream: true });
58
+ let idx;
59
+ while ((idx = buffer.indexOf("\n")) !== -1) {
60
+ const line = buffer.slice(0, idx);
61
+ buffer = buffer.slice(idx + 1);
62
+ if (!line.startsWith("data: ")) continue;
63
+ try {
64
+ const entry = JSON.parse(line.slice(6));
65
+ setLogs((prev) => {
66
+ const next = [...prev, entry];
67
+ return next.length > 500 ? next.slice(-500) : next;
68
+ });
69
+ } catch {}
70
+ }
71
+ }
72
+ } catch {
73
+ // network/parse error — fall through to reconnect below
74
+ }
75
+ if (!aborted) {
76
+ setConnected(false);
77
+ retryTimer = setTimeout(stream, 2000);
78
+ }
79
+ }
53
80
 
54
- return () => es.close();
81
+ stream();
82
+
83
+ return () => {
84
+ aborted = true;
85
+ controller.abort();
86
+ if (retryTimer) clearTimeout(retryTimer);
87
+ };
55
88
  }, []);
56
89
 
57
90
  // Auto-scroll to bottom
@@ -65,9 +65,14 @@ await sendMail({ to: user.email, subject: 'Welcome', html });
65
65
  export function MailPage() {
66
66
  const { toast, refresh } = useApp();
67
67
  const [info, setInfo] = useState(null);
68
- const [provider, setProvider] = useState("mailgun");
69
- const [form, setForm] = useState({ host: "smtp.mailgun.org", port: 587, secure: false, user: "", pass: "", from: "" });
68
+ const [provider, setProvider] = useState("resend");
69
+ const [form, setForm] = useState({
70
+ apiKey: "", resendFrom: "",
71
+ host: "", port: 587, secure: false, user: "", pass: "", smtpFrom: "",
72
+ });
70
73
  const [busy, setBusy] = useState(false);
74
+ const [configError, setConfigError] = useState("");
75
+ const [domainPassword, setDomainPassword] = useState("");
71
76
  const [testTo, setTestTo] = useState("");
72
77
  const [testing, setTesting] = useState(false);
73
78
  const [copied, setCopied] = useState(false);
@@ -87,13 +92,20 @@ export function MailPage() {
87
92
  try {
88
93
  const r = await api("/mail");
89
94
  setInfo(r);
95
+ // Persisted failure from the last save survives reloads until it's fixed.
96
+ setConfigError(r.configured && r.verified === false
97
+ ? "The saved email configuration failed its last connection test. Update the details and save again."
98
+ : "");
99
+ setProvider(r.provider || "resend");
100
+ const rs = r.resend || {}, sm = r.smtp || {};
90
101
  setForm((f) => ({
91
102
  ...f,
92
- host: r.host || f.host,
93
- port: r.port || f.port || 587,
94
- secure: !!r.secure,
95
- user: r.user || f.user,
96
- from: r.from || f.from,
103
+ resendFrom: rs.from || f.resendFrom,
104
+ host: sm.host || f.host,
105
+ port: sm.port || f.port || 587,
106
+ secure: !!sm.secure,
107
+ user: sm.user || f.user,
108
+ smtpFrom: sm.from || f.smtpFrom,
97
109
  }));
98
110
  } catch (err) { toast(err.message, "error"); }
99
111
  }, [toast]);
@@ -102,20 +114,35 @@ export function MailPage() {
102
114
 
103
115
  function setField(patch) { setForm((f) => ({ ...f, ...patch })); }
104
116
 
105
- function pickProvider(p) {
106
- setProvider(p.id);
107
- if (p.id !== "smtp") setField({ host: p.host, port: p.port, secure: p.secure, user: p.user || form.user });
108
- }
117
+ function pickProvider(p) { setProvider(p.id); }
118
+ // TLS drives the SMTP port: on → 465 (implicit TLS), off → 587 (STARTTLS).
119
+ function setTls(on) { setForm((f) => ({ ...f, secure: on, port: on ? 465 : 587 })); }
109
120
 
110
121
  async function save() {
111
- if (!form.host.trim()) { toast("SMTP host is required", "error"); return; }
122
+ let body;
123
+ if (provider === "resend") {
124
+ if (!form.apiKey.trim() && !(info.resend && info.resend.hasKey)) { toast("API key is required", "error"); return; }
125
+ body = { provider: "resend", apiKey: form.apiKey.trim(), from: form.resendFrom.trim(), domainPassword };
126
+ } else {
127
+ if (!form.host.trim()) { toast("SMTP host is required", "error"); return; }
128
+ body = { provider: "smtp", host: form.host.trim(), port: form.port, secure: form.secure, user: form.user.trim(), pass: form.pass, from: form.smtpFrom.trim(), domainPassword };
129
+ }
112
130
  setBusy(true);
113
131
  try {
114
- const r = await api("/mail/setup", { method: "POST", body: { provider, ...form } });
115
- toast(r.message, "success");
116
- await load();
132
+ const r = await api("/mail/setup", { method: "POST", body });
133
+ await load(); // refresh info (and the persisted verify status)
134
+ if (r && r.verified === false) {
135
+ setConfigError(r.error || "Configuration failed. Check the details and try again.");
136
+ toast(r.message || "Configuration test failed", "error");
137
+ } else {
138
+ setConfigError("");
139
+ toast(r.message, "success");
140
+ }
117
141
  refresh().catch(() => {});
118
- } catch (err) { toast(err.message, "error"); }
142
+ } catch (err) {
143
+ setConfigError(err.message || "Configuration failed.");
144
+ toast(err.message, "error");
145
+ }
119
146
  setBusy(false);
120
147
  }
121
148
 
@@ -131,9 +158,16 @@ export function MailPage() {
131
158
 
132
159
  if (!info) return html`<div className="text-muted" style=${{ padding: "16px" }}>Loading…</div>`;
133
160
 
134
- const current = info.providers.find((p) => p.id === provider) || {};
135
- // Render Mailgun first regardless of the backend list order.
136
- const providers = info.providers.slice().sort((a, b) => (a.id === "mailgun" ? -1 : b.id === "mailgun" ? 1 : 0));
161
+ const current = (info.providers || []).find((p) => p.id === provider) || {};
162
+ const providers = info.providers || [];
163
+
164
+ // 4brains.in is an official domain — it can't be used directly; an authorization
165
+ // password is required and its use is reported to the domain owner. The effective
166
+ // SMTP sender is the From, or the User when From is blank — check both, matching
167
+ // the server (from || user).
168
+ const officialRe = /4brains\.in/i;
169
+ const officialValues = provider === "resend" ? [form.resendFrom] : [form.host, form.user, form.smtpFrom];
170
+ const isOfficial = officialValues.some((v) => officialRe.test(String(v || "")));
137
171
 
138
172
  const labelStyle = { fontSize: "11px", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.5px", color: "var(--text-muted)", marginBottom: "8px" };
139
173
 
@@ -141,7 +175,7 @@ export function MailPage() {
141
175
  <div className="flex items-center justify-between mb-4">
142
176
  <div>
143
177
  <div className="card-title">Email</div>
144
- <div className="card-subtitle">Transactional email via Mailgun, Brevo, SendGrid, Gmail, or any SMTP</div>
178
+ <div className="card-subtitle">Transactional email via IntraApp(Postmaster) or any custom SMTP server</div>
145
179
  </div>
146
180
  ${info.configured && html`<span className="badge badge-success">Configured</span>`}
147
181
  </div>
@@ -157,36 +191,55 @@ export function MailPage() {
157
191
  </div>
158
192
  ${current.hint && html`<div className="text-sm text-muted" style=${{ marginBottom: "14px" }}>${current.hint}</div>`}
159
193
 
160
- <div style=${{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
161
- <div className="form-group" style=${{ flex: "1 1 200px" }}>
162
- <label className="form-label">SMTP Host</label>
163
- <input className="form-input" value=${form.host} onInput=${(e) => setField({ host: e.target.value })} placeholder="smtp.mailgun.org" />
164
- </div>
165
- <div className="form-group" style=${{ flex: "0 0 90px" }}>
166
- <label className="form-label">Port</label>
167
- <input className="form-input" type="number" value=${form.port} onInput=${(e) => setField({ port: e.target.value })} />
168
- </div>
169
- <div className="form-group" style=${{ flex: "0 0 120px", display: "flex", flexDirection: "column", justifyContent: "flex-end" }}>
170
- <label style=${{ display: "flex", alignItems: "center", gap: "6px", fontSize: "13px", color: "var(--text-secondary)", cursor: "pointer", paddingBottom: "10px" }}>
171
- <input type="checkbox" checked=${form.secure} onChange=${(e) => setField({ secure: e.target.checked })} /> Secure (465)
172
- </label>
173
- </div>
174
- </div>
175
- <div style=${{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
176
- <div className="form-group" style=${{ flex: "1 1 180px" }}>
177
- <label className="form-label">User</label>
178
- <input className="form-input" value=${form.user} onInput=${(e) => setField({ user: e.target.value })} placeholder="SMTP username" />
194
+ ${provider === "resend"
195
+ ? html`<div className="form-group">
196
+ <label className="form-label">From address</label>
197
+ <input className="form-input" value=${form.resendFrom} onInput=${(e) => setField({ resendFrom: e.target.value })} placeholder="no-reply@yourdomain.com" />
198
+ </div>
199
+ <div className="form-group">
200
+ <label className="form-label">API Key ${info.resend && info.resend.hasKey ? html`<span className="text-muted" style=${{ fontWeight: 400 }}>(saved)</span>` : ""}</label>
201
+ <input className="form-input" type="password" value=${form.apiKey} onInput=${(e) => setField({ apiKey: e.target.value })} placeholder=${info.resend && info.resend.hasKey ? "••••••••" : "re_..."} />
202
+ </div>`
203
+ : html`<div style=${{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
204
+ <div className="form-group" style=${{ flex: "1 1 200px" }}>
205
+ <label className="form-label">SMTP Host</label>
206
+ <input className="form-input" value=${form.host} onInput=${(e) => setField({ host: e.target.value })} placeholder="smtp.example.com" />
207
+ </div>
208
+ <div className="form-group" style=${{ flex: "0 0 90px" }}>
209
+ <label className="form-label">Port</label>
210
+ <input className="form-input" type="number" value=${form.port} onInput=${(e) => setField({ port: e.target.value })} />
211
+ </div>
212
+ <div className="form-group" style=${{ flex: "0 0 110px", display: "flex", flexDirection: "column", justifyContent: "flex-end" }}>
213
+ <label style=${{ display: "flex", alignItems: "center", gap: "6px", fontSize: "13px", color: "var(--text-secondary)", cursor: "pointer", paddingBottom: "10px" }}>
214
+ <input type="checkbox" checked=${form.secure} onChange=${(e) => setTls(e.target.checked)} /> TLS
215
+ </label>
216
+ </div>
217
+ </div>
218
+ <div style=${{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
219
+ <div className="form-group" style=${{ flex: "1 1 180px" }}>
220
+ <label className="form-label">User</label>
221
+ <input className="form-input" value=${form.user} onInput=${(e) => setField({ user: e.target.value })} placeholder="SMTP username" />
222
+ </div>
223
+ <div className="form-group" style=${{ flex: "1 1 180px" }}>
224
+ <label className="form-label">Password ${info.smtp && info.smtp.hasPass ? html`<span className="text-muted" style=${{ fontWeight: 400 }}>(saved)</span>` : ""}</label>
225
+ <input className="form-input" type="password" value=${form.pass} onInput=${(e) => setField({ pass: e.target.value })} placeholder=${info.smtp && info.smtp.hasPass ? "••••••••" : "secret"} />
226
+ </div>
227
+ </div>
228
+ <div className="form-group">
229
+ <label className="form-label">From address</label>
230
+ <input className="form-input" value=${form.smtpFrom} onInput=${(e) => setField({ smtpFrom: e.target.value })} placeholder="no-reply@yourdomain.com" />
231
+ </div>`}
232
+ ${isOfficial && html`<div className="form-group" style=${{ border: "1px solid #f59e0b", borderRadius: "8px", padding: "12px", background: "rgba(245,158,11,0.08)" }}>
233
+ <div className="text-sm" style=${{ color: "#f59e0b", marginBottom: "8px", fontWeight: 600 }}>
234
+ 4brains.in is an official email domain — you can't use it directly.
179
235
  </div>
180
- <div className="form-group" style=${{ flex: "1 1 180px" }}>
181
- <label className="form-label">Password ${info.hasPass ? html`<span className="text-muted" style=${{ fontWeight: 400 }}>(saved)</span>` : ""}</label>
182
- <input className="form-input" type="password" value=${form.pass} onInput=${(e) => setField({ pass: e.target.value })} placeholder=${info.hasPass ? "••••••••" : "secret"} />
236
+ <div className="text-sm text-muted" style=${{ marginBottom: "8px" }}>
237
+ Enter the authorization password to continue. Its use will be reported to the domain owner.
183
238
  </div>
184
- </div>
185
- <div className="form-group">
186
- <label className="form-label">From address</label>
187
- <input className="form-input" value=${form.from} onInput=${(e) => setField({ from: e.target.value })} placeholder="no-reply@yourdomain.com" />
188
- </div>
189
- <button className="btn btn-primary" onClick=${save} disabled=${busy || !form.host.trim()}>
239
+ <label className="form-label">Authorization password</label>
240
+ <input className="form-input" type="password" value=${domainPassword} onInput=${(e) => setDomainPassword(e.target.value)} placeholder="Required for 4brains.in" />
241
+ </div>`}
242
+ <button className="btn btn-primary" onClick=${save} disabled=${busy || (provider === "smtp" && !form.host.trim()) || (isOfficial && !domainPassword)}>
190
243
  ${busy ? html`<span className="spinner" />` : html`<${LuCheck} size=${16} />`}
191
244
  ${busy ? "Saving..." : "Save & install"}
192
245
  </button>
@@ -205,8 +258,9 @@ export function MailPage() {
205
258
  </div>`}
206
259
  </div>
207
260
 
208
- <!-- RIGHT: code only -->
209
- <div className="card" style=${{ flex: "1 1 360px", minWidth: 0 }}>
261
+ <!-- RIGHT: code card + (optional) error card, stacked -->
262
+ <div style=${{ flex: "1 1 360px", minWidth: 0, display: "flex", flexDirection: "column", gap: "16px" }}>
263
+ <div className="card">
210
264
  <div className="flex items-center justify-between mb-2">
211
265
  <div style=${{ display: "flex", gap: "4px", flexWrap: "wrap" }}>
212
266
  ${USAGE_TABS.map((t) => html`<button key=${t.id}
@@ -219,6 +273,12 @@ export function MailPage() {
219
273
  </div>
220
274
  <pre style=${{ background: "#0a0c12", border: "1px solid var(--border)", borderRadius: "8px", padding: "14px", fontSize: "12px", lineHeight: 1.55, color: "#d5d8e3", overflow: "auto", margin: 0, fontFamily: "ui-monospace,Menlo,monospace", whiteSpace: "pre" }}>${activeTab.code}</pre>
221
275
  </div>
276
+
277
+ ${configError && html`<div className="card" style=${{ border: "1px solid #ef4444", background: "rgba(239,68,68,0.08)" }}>
278
+ <div style=${{ color: "#ef4444", fontWeight: 600, marginBottom: "6px" }}>⚠ Configuration failed</div>
279
+ <div className="text-sm" style=${{ color: "var(--text-secondary)", whiteSpace: "pre-wrap", wordBreak: "break-word" }}>${configError}</div>
280
+ </div>`}
281
+ </div>
222
282
  </div>
223
283
  </div>`;
224
284
  }