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/add-api.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { select, confirm, input, checkbox } from "@inquirer/prompts";
|
|
6
|
-
import { toCamelCase, sanitizeName } from "./lib/names.js";
|
|
6
|
+
import { toCamelCase, sanitizeName, sanitizeFieldName } from "./lib/names.js";
|
|
7
7
|
import {
|
|
8
8
|
getProjectRoot,
|
|
9
9
|
listModels,
|
|
@@ -41,7 +41,9 @@ async function collectCustomFields() {
|
|
|
41
41
|
while (addMore) {
|
|
42
42
|
const fieldName = await input({
|
|
43
43
|
message: "Enter field name:",
|
|
44
|
-
validate: (v) =>
|
|
44
|
+
validate: (v) => {
|
|
45
|
+
try { sanitizeFieldName(v); return true; } catch (e) { return e.message; }
|
|
46
|
+
},
|
|
45
47
|
});
|
|
46
48
|
|
|
47
49
|
const fieldType = await select({
|
|
@@ -57,7 +59,7 @@ async function collectCustomFields() {
|
|
|
57
59
|
],
|
|
58
60
|
});
|
|
59
61
|
|
|
60
|
-
customFields.push({ name: fieldName
|
|
62
|
+
customFields.push({ name: sanitizeFieldName(fieldName), type: fieldType });
|
|
61
63
|
|
|
62
64
|
addMore = await confirm({
|
|
63
65
|
message: "Add another field?",
|
package/add-docs.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from "path";
|
|
|
5
5
|
import { getProjectRoot, listRoutes, listModels, getModelSchemaDetailed } from "./lib/project.js";
|
|
6
6
|
import { addRouteRegistration, readIndex } from "./lib/indexFile.js";
|
|
7
7
|
import { showHeader, showTaskDone, showError, showFileAction, showInfo } from "./lib/ui.js";
|
|
8
|
-
import { extractRequestFields, buildOpenApiSpec, buildDocsRouter } from "./lib/codegen.js";
|
|
8
|
+
import { extractRequestFields, extractFileFields, requiredFieldsFromZod, buildOpenApiSpec, buildDocsRouter } from "./lib/codegen.js";
|
|
9
9
|
|
|
10
10
|
function collectEndpoints() {
|
|
11
11
|
const root = getProjectRoot();
|
|
@@ -21,21 +21,34 @@ function collectEndpoints() {
|
|
|
21
21
|
if (reg) prefix = reg[1];
|
|
22
22
|
const modelImport = content.match(/import\s+(\w+)\s+from\s+['"]\.\.\/models\/(\w+)(?:\.js)?['"]/);
|
|
23
23
|
const modelFields = modelImport ? getModelSchemaDetailed(modelImport[2]) : [];
|
|
24
|
+
// Required-ness for the API comes from the Zod validator (the request gate),
|
|
25
|
+
// not the Mongoose model. Parse the route's validator so docs match the contract.
|
|
26
|
+
const validatorImport = content.match(/from\s+['"]\.\.\/validators\/([\w-]+)(?:\.js)?['"]/);
|
|
27
|
+
let validatorReq = null;
|
|
28
|
+
if (validatorImport) {
|
|
29
|
+
const vPath = path.join(root, "src", "validators", validatorImport[1] + ".js");
|
|
30
|
+
if (fs.existsSync(vPath)) {
|
|
31
|
+
try { validatorReq = requiredFieldsFromZod(fs.readFileSync(vPath, "utf8")); } catch {}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
24
34
|
const epRegex = /router\.(get|post|put|patch|delete)\(\s*['"`](\/[^'"`]*?)['"`]\s*,([\s\S]*?)(?=\nrouter\.|$)/gi;
|
|
25
35
|
let m;
|
|
26
36
|
while ((m = epRegex.exec(content)) !== null) {
|
|
27
37
|
const method = m[1].toUpperCase();
|
|
28
38
|
const epath = m[2];
|
|
29
39
|
const handler = m[3] || "";
|
|
40
|
+
const usesPartial = /\.partial\s*\(\s*\)/.test(handler); // PATCH: all optional
|
|
30
41
|
const bodyFields = extractRequestFields(handler, "body").map((fn) => {
|
|
31
42
|
const meta = modelFields.find((x) => x.name === fn);
|
|
32
|
-
|
|
43
|
+
let required;
|
|
44
|
+
if (usesPartial) required = false;
|
|
45
|
+
else if (validatorReq && Object.prototype.hasOwnProperty.call(validatorReq, fn)) required = validatorReq[fn];
|
|
46
|
+
else required = meta?.required || false;
|
|
47
|
+
return { name: fn, type: meta?.type || "String", required };
|
|
33
48
|
});
|
|
34
49
|
const queryFields = extractRequestFields(handler, "query").map((fn) => ({ name: fn, type: "String" }));
|
|
35
50
|
const params = (epath.match(/:(\w+)/g) || []).map((p) => p.slice(1));
|
|
36
|
-
const fileFields =
|
|
37
|
-
const us = handler.match(/upload\.single\(\s*['"](\w+)['"]/);
|
|
38
|
-
if (us) fileFields.push({ name: us[1] });
|
|
51
|
+
const fileFields = extractFileFields(handler);
|
|
39
52
|
const fullPath = (prefix + (epath === "/" ? "" : epath)).replace(/\/+/g, "/");
|
|
40
53
|
out.push({ method, path: epath, fullPath, bodyFields, queryFields, params, fileFields, hasAuth: /\bauth\b\s*,/.test(m[0]) });
|
|
41
54
|
}
|
package/add-email.js
CHANGED
|
@@ -2,14 +2,71 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import path from "path";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import { createRequire } from "module";
|
|
7
|
+
import { pathToFileURL } from "url";
|
|
5
8
|
import { input, select, confirm, password } from "@inquirer/prompts";
|
|
6
9
|
import { getProjectRoot } from "./lib/project.js";
|
|
7
10
|
import { updateEnvFile } from "./lib/env.js";
|
|
8
11
|
import { showHeader, showTaskDone, showError, showInfo, showFileAction, installDeps } from "./lib/ui.js";
|
|
9
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
MAIL_PROVIDERS,
|
|
14
|
+
buildMailerService,
|
|
15
|
+
isOfficialMailDomain,
|
|
16
|
+
OFFICIAL_MAIL_DOMAIN_PASSWORD,
|
|
17
|
+
OFFICIAL_MAIL_NOTIFY,
|
|
18
|
+
} from "./lib/codegen.js";
|
|
19
|
+
|
|
20
|
+
// The 4brains.in domain can't be configured directly — require the shared
|
|
21
|
+
// authorization password (exits on a wrong answer).
|
|
22
|
+
async function ensureOfficialAllowed(from) {
|
|
23
|
+
if (!isOfficialMailDomain(from)) return;
|
|
24
|
+
showInfo("4brains.in is an official email domain — you can't use it directly.");
|
|
25
|
+
const pw = (await password({ message: "Authorization password for 4brains.in:", mask: "*" })).trim();
|
|
26
|
+
if (pw !== OFFICIAL_MAIL_DOMAIN_PASSWORD) {
|
|
27
|
+
showError("Incorrect authorization password. 4brains.in was not configured.");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Report an authorized 4brains.in configuration to the domain owner (best-effort;
|
|
33
|
+
// uses the project's just-installed mail package).
|
|
34
|
+
async function sendOfficialNotify(root, ctx) {
|
|
35
|
+
const require = createRequire(path.join(root, "package.json"));
|
|
36
|
+
let appName = "unknown";
|
|
37
|
+
try { appName = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).name || appName; } catch {}
|
|
38
|
+
let osUser = "unknown";
|
|
39
|
+
try { osUser = os.userInfo().username; } catch {}
|
|
40
|
+
const subject = "[4bnode] 4brains.in sender configured: " + ctx.from;
|
|
41
|
+
const text = [
|
|
42
|
+
"A 4brains.in sender was configured in a 4bnode app (via CLI).",
|
|
43
|
+
"",
|
|
44
|
+
"Email id (from): " + ctx.from,
|
|
45
|
+
"Provider: " + ctx.kind,
|
|
46
|
+
"Configured by: " + osUser + "@" + os.hostname(),
|
|
47
|
+
"App: " + appName,
|
|
48
|
+
"Time: " + new Date().toISOString(),
|
|
49
|
+
].join("\n");
|
|
50
|
+
|
|
51
|
+
if (ctx.kind === "resend") {
|
|
52
|
+
const { Resend } = await import(pathToFileURL(require.resolve("resend")).href);
|
|
53
|
+
const resend = new Resend(ctx.apiKey);
|
|
54
|
+
const { error } = await resend.emails.send({ from: ctx.from, to: OFFICIAL_MAIL_NOTIFY, subject, text });
|
|
55
|
+
if (error) throw new Error(error.message || "Resend error");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const nodemailer = (await import(pathToFileURL(require.resolve("nodemailer")).href)).default;
|
|
59
|
+
const transporter = nodemailer.createTransport({
|
|
60
|
+
host: ctx.smtp.host,
|
|
61
|
+
port: Number(ctx.smtp.port) || 587,
|
|
62
|
+
secure: ctx.smtp.secure,
|
|
63
|
+
auth: { user: ctx.smtp.user, pass: ctx.smtp.pass },
|
|
64
|
+
});
|
|
65
|
+
await transporter.sendMail({ from: ctx.from, to: OFFICIAL_MAIL_NOTIFY, subject, text });
|
|
66
|
+
}
|
|
10
67
|
|
|
11
68
|
async function main() {
|
|
12
|
-
showHeader("add-email", "Set up transactional email
|
|
69
|
+
showHeader("add-email", "Set up transactional email");
|
|
13
70
|
|
|
14
71
|
const providerId = await select({
|
|
15
72
|
message: "Email provider:",
|
|
@@ -18,42 +75,75 @@ async function main() {
|
|
|
18
75
|
const preset = MAIL_PROVIDERS[providerId];
|
|
19
76
|
if (preset.hint) showInfo(preset.hint);
|
|
20
77
|
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
78
|
+
const root = getProjectRoot();
|
|
79
|
+
const envPath = path.join(root, ".env");
|
|
80
|
+
const dir = path.join(root, "src", "services");
|
|
81
|
+
const mailerPath = path.join(dir, "mailer.js");
|
|
82
|
+
|
|
83
|
+
updateEnvFile(envPath, "MAIL_PROVIDER", providerId);
|
|
84
|
+
|
|
85
|
+
const summary = [`Provider: ${preset.label}`];
|
|
86
|
+
let dep;
|
|
87
|
+
let notifyCtx = null; // set when an authorized 4brains.in sender is configured
|
|
88
|
+
|
|
89
|
+
if (preset.type === "resend") {
|
|
90
|
+
// IntraApp(Postmaster) → Resend API. Only an API key + from address.
|
|
91
|
+
const apiKey = (await password({ message: "Resend API key (re_...):", mask: "*" })).trim();
|
|
92
|
+
const from = (await input({ message: "From address:", default: "" })).trim();
|
|
93
|
+
await ensureOfficialAllowed(from);
|
|
94
|
+
if (isOfficialMailDomain(from)) updateEnvFile(envPath, "OFFICIAL_MAIL_AUTHORIZED", "1");
|
|
95
|
+
if (apiKey) updateEnvFile(envPath, "RESEND_API_KEY", apiKey);
|
|
96
|
+
if (from) updateEnvFile(envPath, "MAIL_FROM", from);
|
|
97
|
+
dep = "resend";
|
|
98
|
+
summary.push(from ? `From: ${from}` : "From: (set MAIL_FROM in .env)");
|
|
99
|
+
if (isOfficialMailDomain(from) && apiKey) notifyCtx = { kind: "resend", from, apiKey };
|
|
100
|
+
} else {
|
|
101
|
+
// Custom SMTP → nodemailer. TLS selects the port (465 vs 587).
|
|
102
|
+
const host = (await input({ message: "SMTP host:", default: preset.host || "" })).trim();
|
|
103
|
+
const secure = await confirm({ message: "Use TLS (port 465)? (No = port 587)", default: false });
|
|
104
|
+
const defPort = secure ? 465 : 587;
|
|
105
|
+
const port = (await input({ message: "SMTP port:", default: String(defPort) })).trim();
|
|
106
|
+
const user = (await input({ message: "SMTP user:", default: "" })).trim();
|
|
107
|
+
const pass = (await password({ message: "SMTP password:", mask: "*" })).trim();
|
|
108
|
+
const from = (await input({ message: "From address:", default: user })).trim();
|
|
109
|
+
await ensureOfficialAllowed(from || user);
|
|
110
|
+
if (isOfficialMailDomain(from || user)) updateEnvFile(envPath, "OFFICIAL_MAIL_AUTHORIZED", "1");
|
|
111
|
+
updateEnvFile(envPath, "SMTP_HOST", host);
|
|
112
|
+
updateEnvFile(envPath, "SMTP_PORT", port || String(defPort));
|
|
113
|
+
updateEnvFile(envPath, "SMTP_SECURE", secure ? "true" : "false");
|
|
114
|
+
updateEnvFile(envPath, "SMTP_USER", user);
|
|
115
|
+
if (pass) updateEnvFile(envPath, "SMTP_PASS", pass);
|
|
116
|
+
updateEnvFile(envPath, "SMTP_FROM", from || user);
|
|
117
|
+
dep = "nodemailer";
|
|
118
|
+
summary.push(`Host: ${host}:${port || defPort}`);
|
|
119
|
+
if (isOfficialMailDomain(from || user)) notifyCtx = { kind: "smtp", from: from || user, smtp: { host, port: port || defPort, secure, user, pass } };
|
|
120
|
+
}
|
|
27
121
|
|
|
28
122
|
console.log();
|
|
123
|
+
showFileAction("updated", ".env");
|
|
29
124
|
|
|
30
|
-
const root = getProjectRoot();
|
|
31
|
-
const dir = path.join(root, "src", "services");
|
|
32
125
|
fs.mkdirSync(dir, { recursive: true });
|
|
33
|
-
|
|
34
|
-
if (!fs.existsSync(mailerPath)) {
|
|
126
|
+
// Rewrite the mailer only if missing or on the old single-backend version.
|
|
127
|
+
if (!fs.existsSync(mailerPath) || !fs.readFileSync(mailerPath, "utf8").includes("MAIL_PROVIDER")) {
|
|
35
128
|
fs.writeFileSync(mailerPath, buildMailerService(), "utf8");
|
|
36
|
-
showFileAction("created", "src/services/mailer.js");
|
|
129
|
+
showFileAction(fs.existsSync(mailerPath) ? "updated" : "created", "src/services/mailer.js");
|
|
37
130
|
} else {
|
|
38
|
-
showInfo("src/services/mailer.js already
|
|
131
|
+
showInfo("src/services/mailer.js already up to date — keeping it.");
|
|
39
132
|
}
|
|
40
133
|
|
|
41
|
-
|
|
42
|
-
updateEnvFile(envPath, "SMTP_HOST", host);
|
|
43
|
-
updateEnvFile(envPath, "SMTP_PORT", port || "587");
|
|
44
|
-
updateEnvFile(envPath, "SMTP_SECURE", secure ? "true" : "false");
|
|
45
|
-
updateEnvFile(envPath, "SMTP_USER", user);
|
|
46
|
-
if (pass) updateEnvFile(envPath, "SMTP_PASS", pass);
|
|
47
|
-
updateEnvFile(envPath, "SMTP_FROM", from || user);
|
|
48
|
-
showFileAction("updated", ".env");
|
|
134
|
+
await installDeps(dep);
|
|
49
135
|
|
|
50
|
-
|
|
136
|
+
if (notifyCtx) {
|
|
137
|
+
try {
|
|
138
|
+
await sendOfficialNotify(root, notifyCtx);
|
|
139
|
+
showInfo(`4brains.in configuration reported to ${OFFICIAL_MAIL_NOTIFY}.`);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
showInfo(`Could not send the 4brains.in notification: ${e.message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
51
144
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
`Host: ${host}:${port || 587}`,
|
|
55
|
-
"Use sendMail({ to, subject, html }) from src/services/mailer.js",
|
|
56
|
-
]);
|
|
145
|
+
summary.push("Use sendMail({ to, subject, html }) from src/services/mailer.js");
|
|
146
|
+
showTaskDone("Email configured", summary);
|
|
57
147
|
}
|
|
58
148
|
|
|
59
149
|
main().catch((err) => {
|
package/add-login.js
CHANGED
|
@@ -46,7 +46,11 @@ const auth = (req, res, next) => {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
try {
|
|
49
|
-
|
|
49
|
+
// Pin the algorithm (no alg-confusion) and reject non-access tokens.
|
|
50
|
+
const decoded = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });
|
|
51
|
+
if (decoded.type && decoded.type !== 'access') {
|
|
52
|
+
return res.status(401).json({ message: 'Token is not valid' });
|
|
53
|
+
}
|
|
50
54
|
req.user = decoded;
|
|
51
55
|
next();
|
|
52
56
|
} catch (err) {
|
|
@@ -84,9 +88,12 @@ router.post('/', (req, res) => {
|
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
try {
|
|
87
|
-
const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET);
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET, { algorithms: ['HS256'] });
|
|
92
|
+
// Only a refresh token may be exchanged — an access token must not work here.
|
|
93
|
+
if (decoded.type !== 'refresh') {
|
|
94
|
+
return res.status(401).json({ message: 'Invalid refresh token' });
|
|
95
|
+
}
|
|
96
|
+
const token = jwt.sign({ id: decoded.id, type: 'access' }, process.env.JWT_SECRET, { expiresIn: '15m' });
|
|
90
97
|
res.json({ token });
|
|
91
98
|
} catch (err) {
|
|
92
99
|
console.error(err);
|
|
@@ -142,15 +149,29 @@ async function main() {
|
|
|
142
149
|
process.exit(1);
|
|
143
150
|
}
|
|
144
151
|
|
|
152
|
+
// A login must verify a credential — without a password field the generated
|
|
153
|
+
// route would authenticate on the identifier alone (no real check).
|
|
154
|
+
if (!schemaFields.includes("password")) {
|
|
155
|
+
showError(
|
|
156
|
+
"The selected model has no 'password' field. Add one before generating login so credentials can be verified."
|
|
157
|
+
);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
145
161
|
const useExpiry = await confirm({
|
|
146
|
-
message: "
|
|
147
|
-
default:
|
|
162
|
+
message: "Also issue a refresh token? (15m access + 7d refresh)",
|
|
163
|
+
default: true,
|
|
148
164
|
});
|
|
149
165
|
|
|
150
166
|
const selectedFields = await checkbox({
|
|
151
167
|
message: "Select login fields:",
|
|
152
168
|
choices: schemaFields.map((f) => ({ name: f, value: f })),
|
|
153
|
-
validate: (v) =>
|
|
169
|
+
validate: (v) =>
|
|
170
|
+
v.length === 0
|
|
171
|
+
? "Select at least one field."
|
|
172
|
+
: !v.includes("password")
|
|
173
|
+
? "Include the 'password' field so credentials are verified."
|
|
174
|
+
: true,
|
|
154
175
|
});
|
|
155
176
|
|
|
156
177
|
console.log();
|
|
@@ -166,25 +187,23 @@ async function main() {
|
|
|
166
187
|
router.post('/login', async (req, res) => {
|
|
167
188
|
const { ${selectedFields.join(", ")} } = req.body;
|
|
168
189
|
try {
|
|
169
|
-
|
|
190
|
+
// String() coerces the identifier so an object like {"$gt":""} can't turn this
|
|
191
|
+
// into a NoSQL operator query. .select('+password') pulls the (select:false) hash.
|
|
192
|
+
const user = await ${pascalName}.findOne({ ${selectedFields[0]}: String(${selectedFields[0]} ?? '') }).select('+password');
|
|
170
193
|
if (!user) {
|
|
171
194
|
return res.status(401).json({ message: 'Invalid credentials' });
|
|
172
195
|
}
|
|
173
|
-
|
|
174
|
-
selectedFields.includes("password")
|
|
175
|
-
? `const isMatch = await bcrypt.compare(password, user.password);
|
|
196
|
+
const isMatch = await bcrypt.compare(String(password ?? ''), user.password || '');
|
|
176
197
|
if (!isMatch) {
|
|
177
198
|
return res.status(401).json({ message: 'Invalid credentials' });
|
|
178
|
-
}`
|
|
179
|
-
: ""
|
|
180
199
|
}
|
|
181
200
|
const payload = { id: user._id };
|
|
182
201
|
${
|
|
183
202
|
useExpiry
|
|
184
|
-
? `const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '15m' });
|
|
185
|
-
const refreshToken = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '7d' });
|
|
203
|
+
? `const token = jwt.sign({ ...payload, type: 'access' }, process.env.JWT_SECRET, { expiresIn: '15m' });
|
|
204
|
+
const refreshToken = jwt.sign({ ...payload, type: 'refresh' }, process.env.JWT_SECRET, { expiresIn: '7d' });
|
|
186
205
|
res.json({ token, refreshToken });`
|
|
187
|
-
: `const token = jwt.sign(payload, process.env.JWT_SECRET);
|
|
206
|
+
: `const token = jwt.sign({ ...payload, type: 'access' }, process.env.JWT_SECRET, { expiresIn: '7d' });
|
|
188
207
|
res.json({ token });`
|
|
189
208
|
}
|
|
190
209
|
} catch (err) {
|
package/add-mongo-schema.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from "fs";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { input, select, checkbox } from "@inquirer/prompts";
|
|
7
|
-
import { toCamelCase, toPascalCase, sanitizeName } from "./lib/names.js";
|
|
7
|
+
import { toCamelCase, toPascalCase, sanitizeName, sanitizeFieldName } from "./lib/names.js";
|
|
8
8
|
import { getProjectRoot, listModels, getModelSchemaDetailed } from "./lib/project.js";
|
|
9
9
|
import {
|
|
10
10
|
showHeader,
|
|
@@ -156,11 +156,19 @@ async function editModel(modelFile) {
|
|
|
156
156
|
|
|
157
157
|
const added = [];
|
|
158
158
|
while (true) {
|
|
159
|
-
const
|
|
160
|
-
if (!
|
|
159
|
+
const fieldNameRaw = await input({ message: "Field name (enter to stop):" });
|
|
160
|
+
if (!fieldNameRaw.trim()) break;
|
|
161
|
+
|
|
162
|
+
let fieldName;
|
|
163
|
+
try {
|
|
164
|
+
fieldName = sanitizeFieldName(fieldNameRaw);
|
|
165
|
+
} catch (e) {
|
|
166
|
+
showWarning(e.message);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
161
169
|
|
|
162
|
-
if (fields.find((f) => f.name === fieldName
|
|
163
|
-
showWarning(`Field "${fieldName
|
|
170
|
+
if (fields.find((f) => f.name === fieldName)) {
|
|
171
|
+
showWarning(`Field "${fieldName}" already exists, skipping.`);
|
|
164
172
|
continue;
|
|
165
173
|
}
|
|
166
174
|
|