4bnode 3.5.2 → 4.0.0

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 (43) hide show
  1. package/README.md +139 -114
  2. package/add-api.js +288 -156
  3. package/add-crud.js +226 -0
  4. package/add-login.js +152 -319
  5. package/add-mongo-delete.js +67 -138
  6. package/add-mongo-insert.js +87 -204
  7. package/add-mongo-read.js +100 -216
  8. package/add-mongo-schema.js +431 -199
  9. package/add-mongo-update.js +89 -202
  10. package/add-mongo.js +125 -218
  11. package/add-serialport.js +53 -98
  12. package/add-socket.js +43 -66
  13. package/add-websocket.js +68 -140
  14. package/index.js +288 -155
  15. package/lib/env.js +18 -0
  16. package/lib/indexFile.js +87 -0
  17. package/lib/names.js +21 -0
  18. package/lib/project.js +122 -0
  19. package/lib/routeFile.js +56 -0
  20. package/lib/ui.js +161 -0
  21. package/package.json +9 -16
  22. package/skeleton/dev-api.js +2785 -0
  23. package/skeleton/index.js +10 -3
  24. package/skeleton/package.json +9 -5
  25. package/skeleton/public/fonts/NotoSans-Bold.woff2 +0 -0
  26. package/skeleton/public/fonts/NotoSans-Light.woff2 +0 -0
  27. package/skeleton/public/fonts/NotoSans-Medium.woff2 +0 -0
  28. package/skeleton/public/fonts/NotoSans-Regular.woff2 +0 -0
  29. package/skeleton/public/fonts/NotoSans-SemiBold.woff2 +0 -0
  30. package/skeleton/public/images/mongo.png +0 -0
  31. package/skeleton/public/images/mysql.png +0 -0
  32. package/skeleton/public/images/postgresql.png +0 -0
  33. package/skeleton/public/images/sqlite.png +0 -0
  34. package/skeleton/public/index.html +7562 -64
  35. package/README_4bnode.md +0 -197
  36. package/src/auth/passport.js +0 -21
  37. package/src/db.js +0 -13
  38. package/test-server/.env.development +0 -1
  39. package/test-server/.env.production +0 -1
  40. package/test-server/index.js +0 -82
  41. package/test-server/package-lock.json +0 -1267
  42. package/test-server/package.json +0 -20
  43. package/test-server/public/index.html +0 -90
package/add-login.js CHANGED
@@ -2,219 +2,42 @@
2
2
 
3
3
  import fs from "fs";
4
4
  import path from "path";
5
- import readline from "readline";
6
5
  import crypto from "crypto";
7
- import { execSync } from "child_process";
8
- import chalk from "chalk";
9
-
10
- const rl = readline.createInterface({
11
- input: process.stdin,
12
- output: process.stdout,
13
- });
14
-
15
- function listRoutes() {
16
- const projectRoot = process.cwd();
17
- const routesDir = path.join(projectRoot, "src", "routes");
18
- if (!fs.existsSync(routesDir)) {
19
- console.error(chalk.red("No routes directory found in src/routes"));
20
- process.exit(0);
21
- }
22
- const routeFiles = fs
23
- .readdirSync(routesDir)
24
- .filter((file) => file.endsWith(".js"));
25
- if (routeFiles.length === 0) {
26
- console.error(chalk.red("No route files found in src/routes"));
27
- process.exit(0);
28
- }
29
- return routeFiles;
30
- }
31
-
32
- function listModels() {
33
- const projectRoot = process.cwd();
34
- const modelsDir = path.join(projectRoot, "src", "models");
35
- if (!fs.existsSync(modelsDir)) {
36
- console.error(chalk.red("No models directory found in src/models"));
37
- process.exit(0);
38
- }
39
- const modelFiles = fs
40
- .readdirSync(modelsDir)
41
- .filter((file) => file.endsWith(".js"));
42
- if (modelFiles.length === 0) {
43
- console.error(chalk.red("No model files found in src/models"));
44
- process.exit(0);
45
- }
46
- return modelFiles;
47
- }
48
-
49
- function getModelSchema(modelName) {
50
- const projectRoot = process.cwd();
51
- const modelFilePath = path.join(
52
- projectRoot,
53
- "src",
54
- "models",
55
- modelName + ".js"
56
- );
57
- const modelFileContent = fs.readFileSync(modelFilePath, "utf8");
58
-
59
- const schemaRegex = /new mongoose\.Schema\(\s*\{([\s\S]*?)\}\s*\)/m;
60
- const match = modelFileContent.match(schemaRegex);
61
-
62
- if (match) {
63
- try {
64
- const schemaContent = match[1];
65
- const fieldRegex = /(\w+):\s*\{[\s\S]*?\}/g;
66
- const schemaKeys = [];
67
- let fieldMatch;
68
- while ((fieldMatch = fieldRegex.exec(schemaContent)) !== null) {
69
- schemaKeys.push(fieldMatch[1]);
70
- }
71
- return schemaKeys;
72
- } catch (error) {
73
- console.error(chalk.red("Error parsing schema: "), error);
74
- process.exit(0);
75
- }
76
- } else {
77
- console.error(chalk.red("Schema not found in model file."));
78
- process.exit(0);
79
- }
80
- }
81
-
82
- function promptSelectFields(fields, callback) {
83
- console.log(
84
- chalk.yellow(
85
- "Select the fields to include in the data insertion (space-separated list of numbers):"
86
- )
87
- );
88
- fields.forEach((field, index) => {
89
- console.log(chalk.cyan(`${index + 1}. ${field}`));
90
- });
91
-
92
- rl.question(
93
- chalk.yellow("Enter the numbers of the fields: "),
94
- (fieldAnswer) => {
95
- const selectedIndexes = fieldAnswer
96
- .split(" ")
97
- .map((index) => parseInt(index.trim(), 10) - 1);
98
- const selectedFields = selectedIndexes
99
- .map((index) => fields[index])
100
- .filter((field) => field !== undefined);
101
- callback(selectedFields);
102
- rl.close();
103
- }
104
- );
105
- }
106
-
107
- function generateJwtSecret() {
108
- return crypto.randomBytes(32).toString("hex");
109
- }
110
-
111
- function updateEnvFile(filePath, jwtSecret) {
112
- const jwtSecretLine = `JWT_SECRET=${jwtSecret}\n`;
113
-
114
- if (fs.existsSync(filePath)) {
115
- let envContent = fs.readFileSync(filePath, "utf8");
116
- if (envContent.includes("JWT_SECRET=")) {
117
- envContent = envContent.replace(/JWT_SECRET=.*/, jwtSecretLine.trim());
118
- } else {
119
- envContent = envContent.trim() + "\n" + jwtSecretLine;
120
- }
121
- fs.writeFileSync(filePath, envContent, "utf8");
122
- } else {
123
- fs.writeFileSync(filePath, jwtSecretLine, "utf8");
124
- }
125
- }
126
-
127
- function installDependencies() {
128
- console.log(chalk.green("Installing dependencies: jsonwebtoken, bcrypt..."));
129
- execSync("npm install jsonwebtoken bcrypt", { stdio: "inherit" });
130
- }
131
-
132
- function addLoginTemplateToRoute(routeFile, fields, modelName, useExpiry) {
133
- const projectRoot = process.cwd();
134
- const routeFilePath = path.join(projectRoot, "src", "routes", routeFile);
135
- const pascalModelName =
136
- modelName.charAt(0).toUpperCase() + modelName.slice(1);
137
- const loginTemplateImport = `
138
- import bcrypt from 'bcrypt';
139
- import jwt from 'jsonwebtoken';
140
- import ${pascalModelName} from '../models/${modelName}.js';
141
- `;
142
-
143
- const loginTemplateFunction = `
144
- // Login logic
145
- const { ${fields.join(", ")} } = req.body;
146
- try {
147
- const user = await ${pascalModelName}.findOne({ ${fields[0]}: ${fields[0]} });
148
- if (!user) {
149
- return res.status(404).json({ message: 'User not found' });
150
- }
151
- ${
152
- fields.includes("password")
153
- ? `
154
- const isMatch = await bcrypt.compare(password, user.password);
155
- if (!isMatch) {
156
- return res.status(400).json({ message: 'Invalid credentials' });
157
- }`
158
- : ""
159
- }
160
- const payload = { id: user._id };
161
- ${
162
- useExpiry
163
- ? `
164
- const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '15m' });
165
- const refreshToken = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '7d' });
166
- res.json({ token, refreshToken });`
167
- : `
168
- const token = jwt.sign(payload, process.env.JWT_SECRET);
169
- res.json({ token });`
170
- }
171
- } catch (err) {
172
- console.error(err);
173
- res.status(500).json({ message: 'Server error' });
174
- }
175
- `;
176
-
177
- if (!fs.existsSync(routeFilePath)) {
178
- console.error(
179
- chalk.red(`The specified route file does not exist: ${routeFilePath}`)
180
- );
181
- process.exit(0);
182
- }
183
-
184
- let routeContent = fs.readFileSync(routeFilePath, "utf8");
185
-
186
- if (!routeContent.includes("import bcrypt")) {
187
- routeContent = loginTemplateImport + routeContent;
188
- }
189
-
190
- const lastCloseIndex = routeContent.lastIndexOf("});");
191
- if (lastCloseIndex !== -1) {
192
- const updatedContent = [
193
- routeContent.slice(0, lastCloseIndex),
194
- loginTemplateFunction,
195
- routeContent.slice(lastCloseIndex),
196
- ].join("\n");
197
-
198
- routeContent = updatedContent;
199
- } else {
200
- routeContent += `\n${loginTemplateFunction}\n`;
201
- }
202
-
203
- fs.writeFileSync(routeFilePath, routeContent, "utf8");
204
- console.log(chalk.green(`Login template added to ${routeFile}`));
205
- }
6
+ import { select, confirm, checkbox } from "@inquirer/prompts";
7
+ import { toPascalCase } from "./lib/names.js";
8
+ import {
9
+ getProjectRoot,
10
+ listRoutes,
11
+ listModels,
12
+ getModelSchema,
13
+ } from "./lib/project.js";
14
+ import { updateEnvFile } from "./lib/env.js";
15
+ import { addRouteRegistration } from "./lib/indexFile.js";
16
+ import {
17
+ getRouteFilePath,
18
+ addImportToRoute,
19
+ insertCodeIntoRoute,
20
+ } from "./lib/routeFile.js";
21
+ import {
22
+ showHeader,
23
+ showTaskDone,
24
+ showError,
25
+ showInfo,
26
+ showFileAction,
27
+ installDeps,
28
+ } from "./lib/ui.js";
206
29
 
207
30
  function addAuthMiddleware() {
208
- const projectRoot = process.cwd();
209
- const authFilePath = path.join(projectRoot, "src", "middleware", "auth.js");
31
+ const projectRoot = getProjectRoot();
32
+ const authDir = path.join(projectRoot, "src", "middleware");
33
+ const authFilePath = path.join(authDir, "auth.js");
210
34
 
211
35
  if (fs.existsSync(authFilePath)) {
212
- console.log(chalk.yellow("Auth middleware already exists."));
36
+ showInfo("Auth middleware already exists.");
213
37
  return;
214
38
  }
215
39
 
216
- const authMiddlewareContent = `
217
- import jwt from 'jsonwebtoken';
40
+ const content = `import jwt from 'jsonwebtoken';
218
41
 
219
42
  const auth = (req, res, next) => {
220
43
  const token = req.header('Authorization')?.replace('Bearer ', '');
@@ -234,25 +57,22 @@ const auth = (req, res, next) => {
234
57
  export default auth;
235
58
  `;
236
59
 
237
- fs.mkdirSync(path.join(projectRoot, "src", "middleware"), {
238
- recursive: true,
239
- });
240
- fs.writeFileSync(authFilePath, authMiddlewareContent.trim(), "utf8");
241
- console.log(chalk.green("Auth middleware created successfully."));
60
+ fs.mkdirSync(authDir, { recursive: true });
61
+ fs.writeFileSync(authFilePath, content, "utf8");
62
+ showFileAction("created", "src/middleware/auth.js");
242
63
  }
243
64
 
244
65
  function addRefreshTokenRoute() {
245
- const projectRoot = process.cwd();
66
+ const projectRoot = getProjectRoot();
246
67
  const routesDir = path.join(projectRoot, "src", "routes");
247
- const refreshTokenFilePath = path.join(routesDir, "refresh-token.js");
68
+ const filePath = path.join(routesDir, "refresh-token.js");
248
69
 
249
- if (fs.existsSync(refreshTokenFilePath)) {
250
- console.log(chalk.yellow("Refresh token route already exists."));
70
+ if (fs.existsSync(filePath)) {
71
+ showInfo("Refresh token route already exists.");
251
72
  return;
252
73
  }
253
74
 
254
- const refreshTokenContent = `
255
- import express from 'express';
75
+ const content = `import express from 'express';
256
76
  import jwt from 'jsonwebtoken';
257
77
 
258
78
  const router = express.Router();
@@ -278,118 +98,131 @@ export default router;
278
98
  `;
279
99
 
280
100
  fs.mkdirSync(routesDir, { recursive: true });
281
- fs.writeFileSync(refreshTokenFilePath, refreshTokenContent.trim(), "utf8");
101
+ fs.writeFileSync(filePath, content, "utf8");
102
+ showFileAction("created", "src/routes/refresh-token.js");
103
+
104
+ addRouteRegistration(
105
+ "import refreshToken from './src/routes/refresh-token.js';",
106
+ "app.use('/refresh-token', refreshToken);"
107
+ );
108
+ showFileAction("updated", "index.js");
109
+ }
282
110
 
283
- // Update index.js to include the new route
284
- const indexPath = path.join(projectRoot, "index.js");
285
- const routeImport = `import refreshToken from './src/routes/refresh-token.js';\n`;
286
- const routeUse = `app.use('/refresh-token', refreshToken);\n`;
111
+ async function main() {
112
+ showHeader("add-login", "Setup authentication & login");
287
113
 
288
- let indexContent = fs.readFileSync(indexPath, "utf8");
289
- if (!indexContent.includes(routeImport)) {
290
- indexContent = routeImport + indexContent;
114
+ const routeFiles = listRoutes();
115
+ if (routeFiles.length === 0) {
116
+ showError("No route files found in src/routes.");
117
+ process.exit(1);
291
118
  }
292
119
 
293
- if (!indexContent.includes(routeUse)) {
294
- if (indexContent.includes("app.listen(port")) {
295
- indexContent = indexContent.replace(
296
- "app.listen(port",
297
- routeUse + "app.listen(port"
298
- );
299
- } else if (indexContent.includes("server.listen(port")) {
300
- indexContent = indexContent.replace(
301
- "server.listen(port",
302
- routeUse + "server.listen(port"
303
- );
304
- }
120
+ const modelFiles = listModels();
121
+ if (modelFiles.length === 0) {
122
+ showError("No model files found in src/models.");
123
+ process.exit(1);
305
124
  }
306
125
 
307
- fs.writeFileSync(indexPath, indexContent, "utf8");
126
+ const selectedRoute = await select({
127
+ message: "Select route file to modify:",
128
+ choices: routeFiles.map((f) => ({ name: f, value: f })),
129
+ });
308
130
 
309
- console.log(chalk.green("Refresh token route created successfully."));
310
- }
131
+ const selectedModelFile = await select({
132
+ message: "Select model file to use:",
133
+ choices: modelFiles.map((f) => ({ name: f, value: f })),
134
+ });
311
135
 
312
- const routeFiles = listRoutes();
313
- const modelFiles = listModels();
136
+ const modelName = path.basename(selectedModelFile, ".js");
137
+ const pascalName = toPascalCase(modelName);
138
+ const schemaFields = getModelSchema(modelName);
314
139
 
315
- console.log(chalk.yellow("Select the route file to modify:"));
316
- routeFiles.forEach((file, index) => {
317
- console.log(chalk.cyan(`${index + 1}. ${file}`));
318
- });
140
+ if (schemaFields.length === 0) {
141
+ showError("No fields found in the selected model.");
142
+ process.exit(1);
143
+ }
144
+
145
+ const useExpiry = await confirm({
146
+ message: "Use token expiry? (15m access + 7d refresh)",
147
+ default: false,
148
+ });
149
+
150
+ const selectedFields = await checkbox({
151
+ message: "Select login fields:",
152
+ choices: schemaFields.map((f) => ({ name: f, value: f })),
153
+ validate: (v) => (v.length > 0 ? true : "Select at least one field."),
154
+ });
319
155
 
320
- rl.question(
321
- chalk.yellow("Enter the number of the route file: "),
322
- (routeAnswer) => {
323
- const routeIndex = parseInt(routeAnswer, 10) - 1;
324
- if (routeIndex >= 0 && routeIndex < routeFiles.length) {
325
- const selectedRoute = routeFiles[routeIndex];
326
-
327
- console.log(chalk.yellow("Select the model file to use:"));
328
- modelFiles.forEach((file, index) => {
329
- console.log(chalk.cyan(`${index + 1}. ${file}`));
330
- });
331
-
332
- rl.question(
333
- chalk.yellow("Enter the number of the model file: "),
334
- (modelAnswer) => {
335
- const modelIndex = parseInt(modelAnswer, 10) - 1;
336
- if (modelIndex >= 0 && modelIndex < modelFiles.length) {
337
- const selectedModel = path.basename(modelFiles[modelIndex], ".js");
338
- const schemaFields = getModelSchema(selectedModel);
339
-
340
- rl.question(
341
- chalk.yellow(
342
- "Do you want to use expiry date for JWT token? (y/n): "
343
- ),
344
- (expiryAnswer) => {
345
- const useExpiry = expiryAnswer.trim().toLowerCase() === "y";
346
- promptSelectFields(schemaFields, (fields) => {
347
- addLoginTemplateToRoute(
348
- selectedRoute,
349
- fields,
350
- selectedModel,
351
- useExpiry
352
- );
353
-
354
- const jwtSecret = generateJwtSecret();
355
- const projectRoot = process.cwd();
356
- updateEnvFile(
357
- path.join(projectRoot, ".env.development"),
358
- jwtSecret
359
- );
360
- updateEnvFile(
361
- path.join(projectRoot, ".env.production"),
362
- jwtSecret
363
- );
364
- console.log(
365
- chalk.green(
366
- "JWT_SECRET has been added to .env.development and .env.production files."
367
- )
368
- );
369
-
370
- installDependencies();
371
- addAuthMiddleware();
372
- if (useExpiry) {
373
- addRefreshTokenRoute();
374
- }
375
- });
376
- }
377
- );
378
- } else {
379
- console.error(
380
- chalk.red("Invalid selection. Please choose a valid number.")
381
- );
382
- rl.close();
383
- process.exit(0);
384
- }
385
- }
386
- );
387
- } else {
388
- console.error(
389
- chalk.red("Invalid selection. Please choose a valid number.")
390
- );
391
- rl.close();
392
- process.exit(0);
156
+ console.log();
157
+
158
+ const routeFilePath = getRouteFilePath(selectedRoute);
159
+
160
+ addImportToRoute(
161
+ routeFilePath,
162
+ `import bcrypt from 'bcrypt';\nimport jwt from 'jsonwebtoken';\nimport ${pascalName} from '../models/${modelName}.js';`
163
+ );
164
+
165
+ const loginCode = `
166
+ router.post('/login', async (req, res) => {
167
+ const { ${selectedFields.join(", ")} } = req.body;
168
+ try {
169
+ const user = await ${pascalName}.findOne({ ${selectedFields[0]}: ${selectedFields[0]} });
170
+ if (!user) {
171
+ return res.status(404).json({ message: 'User not found' });
172
+ }
173
+ ${
174
+ selectedFields.includes("password")
175
+ ? `const isMatch = await bcrypt.compare(password, user.password);
176
+ if (!isMatch) {
177
+ return res.status(400).json({ message: 'Invalid credentials' });
178
+ }`
179
+ : ""
393
180
  }
181
+ const payload = { id: user._id };
182
+ ${
183
+ 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' });
186
+ res.json({ token, refreshToken });`
187
+ : `const token = jwt.sign(payload, process.env.JWT_SECRET);
188
+ res.json({ token });`
189
+ }
190
+ } catch (err) {
191
+ console.error(err);
192
+ res.status(500).json({ message: 'Server error' });
394
193
  }
395
- );
194
+ });`;
195
+
196
+ insertCodeIntoRoute(routeFilePath, loginCode);
197
+ showFileAction("updated", `src/routes/${selectedRoute}`);
198
+
199
+ const projectRoot = getProjectRoot();
200
+ const devSecret = crypto.randomBytes(32).toString("hex");
201
+ const prodSecret = crypto.randomBytes(32).toString("hex");
202
+ updateEnvFile(path.join(projectRoot, ".env.development"), "JWT_SECRET", devSecret);
203
+ showFileAction("updated", ".env.development");
204
+ updateEnvFile(path.join(projectRoot, ".env.production"), "JWT_SECRET", prodSecret);
205
+ showFileAction("updated", ".env.production");
206
+
207
+ await installDeps(["jsonwebtoken", "bcrypt"]);
208
+
209
+ addAuthMiddleware();
210
+
211
+ if (useExpiry) {
212
+ addRefreshTokenRoute();
213
+ }
214
+
215
+ const details = [
216
+ `Route: ${selectedRoute}`,
217
+ `Model: ${modelName}`,
218
+ `Fields: ${selectedFields.join(", ")}`,
219
+ useExpiry ? "JWT with 15m expiry + refresh token" : "JWT without expiry",
220
+ ];
221
+ showTaskDone("Login & auth configured", details);
222
+ }
223
+
224
+ main().catch((err) => {
225
+ if (err.name === "ExitPromptError") process.exit(0);
226
+ showError(err.message);
227
+ process.exit(1);
228
+ });