4bnode 1.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.
package/add-api.js ADDED
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import readline from "readline";
6
+ import chalk from "chalk";
7
+
8
+ const rl = readline.createInterface({
9
+ input: process.stdin,
10
+ output: process.stdout,
11
+ });
12
+
13
+ const methods = ["get", "post", "put", "delete", "patch"];
14
+
15
+ function toCamelCase(str) {
16
+ return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
17
+ }
18
+
19
+ function checkDuplicateApi(endpointName) {
20
+ const projectRoot = process.cwd();
21
+ const apiDir = path.join(projectRoot, "src", "routes");
22
+ const apiFile = path.join(apiDir, `${endpointName}.js`);
23
+ return fs.existsSync(apiFile);
24
+ }
25
+
26
+ function checkDuplicateMethod(apiFile, method) {
27
+ const apiContent = fs.readFileSync(apiFile, "utf8");
28
+ const methodPattern = new RegExp(`router\\.${method}\\(`);
29
+ return methodPattern.test(apiContent);
30
+ }
31
+
32
+ function createApi(endpointName, method, isAuthorized, selectedPath) {
33
+ const projectRoot = process.cwd();
34
+ const apiDir = path.join(projectRoot, "src", "routes");
35
+ const apiFile = path.join(apiDir, `${endpointName}.js`);
36
+ const variableName = toCamelCase(endpointName);
37
+
38
+ if (!fs.existsSync(apiDir)) {
39
+ fs.mkdirSync(apiDir, { recursive: true });
40
+ }
41
+
42
+ if (fs.existsSync(apiFile) && checkDuplicateMethod(apiFile, method)) {
43
+ console.error(
44
+ chalk.red(
45
+ `API endpoint ${selectedPath} with method ${method.toUpperCase()} already exists. Choose a different method or endpoint name.`
46
+ )
47
+ );
48
+ rl.close();
49
+ return;
50
+ }
51
+
52
+ let apiContent;
53
+ const authMiddleware = isAuthorized ? "auth, " : "";
54
+ const methodContent = `
55
+ router.${method.toLowerCase()}('/', ${authMiddleware}async (req, res) => {
56
+ res.json({ msg: '${endpointName} ${method.toUpperCase()} endpoint works' });
57
+ });
58
+ `;
59
+
60
+ if (fs.existsSync(apiFile)) {
61
+ // Read existing content and append the new method if not already present
62
+ apiContent = fs.readFileSync(apiFile, "utf8");
63
+ if (!apiContent.includes(methodContent.trim())) {
64
+ const insertionIndex = apiContent.lastIndexOf("export default router;");
65
+ apiContent =
66
+ apiContent.slice(0, insertionIndex) +
67
+ methodContent.trim() +
68
+ "\n\n" +
69
+ apiContent.slice(insertionIndex);
70
+ }
71
+ } else {
72
+ // Create new content for the API file
73
+ apiContent = `
74
+ import express from 'express';
75
+ ${isAuthorized ? "import auth from '../middleware/auth.js';" : ""}
76
+ const router = express.Router();
77
+
78
+ router.${method.toLowerCase()}('/', ${authMiddleware}async (req, res) => {
79
+ res.json({ msg: '${endpointName} ${method.toUpperCase()} endpoint works' });
80
+ });
81
+
82
+ export default router;
83
+ `;
84
+ }
85
+
86
+ fs.writeFileSync(apiFile, apiContent.trim(), "utf8");
87
+
88
+ // Update index.js to include the new route
89
+ const indexPath = path.join(projectRoot, "index.js");
90
+ const routeImport = `import ${variableName} from './src/routes/${endpointName}.js';\n`;
91
+ const routeUse = `app.use('${selectedPath}', ${variableName});\n`;
92
+
93
+ let indexContent = fs.readFileSync(indexPath, "utf8");
94
+ if (!indexContent.includes(routeImport)) {
95
+ indexContent = routeImport + indexContent;
96
+ }
97
+
98
+ // Ensure there's no duplicate app.use entry
99
+ if (!indexContent.includes(routeUse)) {
100
+ // Check if `app.listen` or `server.listen` exists and replace accordingly
101
+ if (indexContent.includes("app.listen(port")) {
102
+ indexContent = indexContent.replace(
103
+ "app.listen(port",
104
+ routeUse + "app.listen(port"
105
+ );
106
+ } else if (indexContent.includes("server.listen(port")) {
107
+ indexContent = indexContent.replace(
108
+ "server.listen(port",
109
+ routeUse + "server.listen(port"
110
+ );
111
+ }
112
+ }
113
+
114
+ fs.writeFileSync(indexPath, indexContent, "utf8");
115
+
116
+ console.log(
117
+ chalk.green(
118
+ `API endpoint ${selectedPath} with method ${method.toUpperCase()} created/updated successfully.`
119
+ )
120
+ );
121
+ rl.close();
122
+ }
123
+
124
+ function exitWithError(message) {
125
+ console.error(chalk.red(message));
126
+ rl.close();
127
+ }
128
+
129
+ const apiPathOptions = [
130
+ (endpointName) => `/${endpointName}`,
131
+ (endpointName) => `/api/${endpointName}`,
132
+ ];
133
+
134
+ function promptApiPath(endpointName, callback) {
135
+ console.log(chalk.cyan("Choose API path:"));
136
+ apiPathOptions.forEach((option, index) => {
137
+ console.log(chalk.cyan(`${index + 1}. ${option(endpointName)}`));
138
+ });
139
+
140
+ rl.question(
141
+ chalk.yellow("Enter the number for the API path: "),
142
+ (pathAnswer) => {
143
+ const pathIndex = parseInt(pathAnswer, 10) - 1;
144
+ if (pathIndex >= 0 && pathIndex < apiPathOptions.length) {
145
+ const selectedPath = apiPathOptions[pathIndex](endpointName);
146
+ callback(selectedPath);
147
+ } else {
148
+ exitWithError("Invalid selection. Please choose a valid number.");
149
+ }
150
+ }
151
+ );
152
+ }
153
+
154
+ if (process.argv.length !== 3) {
155
+ exitWithError("Usage: add-api <endpointName>");
156
+ }
157
+
158
+ const endpointName = process.argv[2];
159
+
160
+ promptApiPath(endpointName, (selectedPath) => {
161
+ console.log(chalk.cyan("Choose HTTP method:"));
162
+ methods.forEach((method, index) => {
163
+ console.log(chalk.cyan(`${index + 1}. ${method}`));
164
+ });
165
+
166
+ rl.question(
167
+ chalk.yellow("Enter the number for the HTTP method: "),
168
+ (answer) => {
169
+ const methodIndex = parseInt(answer, 10) - 1;
170
+ if (methodIndex >= 0 && methodIndex < methods.length) {
171
+ const projectRoot = process.cwd();
172
+ const authFilePath = path.join(
173
+ projectRoot,
174
+ "src",
175
+ "middleware",
176
+ "auth.js"
177
+ );
178
+
179
+ if (fs.existsSync(authFilePath)) {
180
+ rl.question(
181
+ chalk.yellow("Should this API be authorized? (y/n): "),
182
+ (authAnswer) => {
183
+ const isAuthorized = authAnswer.trim().toLowerCase() === "y";
184
+ createApi(
185
+ endpointName,
186
+ methods[methodIndex],
187
+ isAuthorized,
188
+ selectedPath
189
+ );
190
+ }
191
+ );
192
+ } else {
193
+ createApi(endpointName, methods[methodIndex], false, selectedPath);
194
+ }
195
+ } else {
196
+ exitWithError("Invalid selection. Please choose a valid number.");
197
+ }
198
+ }
199
+ );
200
+ });
package/add-login.js ADDED
@@ -0,0 +1,395 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import readline from "readline";
6
+ 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
+ }
206
+
207
+ function addAuthMiddleware() {
208
+ const projectRoot = process.cwd();
209
+ const authFilePath = path.join(projectRoot, "src", "middleware", "auth.js");
210
+
211
+ if (fs.existsSync(authFilePath)) {
212
+ console.log(chalk.yellow("Auth middleware already exists."));
213
+ return;
214
+ }
215
+
216
+ const authMiddlewareContent = `
217
+ import jwt from 'jsonwebtoken';
218
+
219
+ const auth = (req, res, next) => {
220
+ const token = req.header('Authorization')?.replace('Bearer ', '');
221
+ if (!token) {
222
+ return res.status(401).json({ message: 'No token, authorization denied' });
223
+ }
224
+
225
+ try {
226
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
227
+ req.user = decoded;
228
+ next();
229
+ } catch (err) {
230
+ res.status(401).json({ message: 'Token is not valid' });
231
+ }
232
+ };
233
+
234
+ export default auth;
235
+ `;
236
+
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."));
242
+ }
243
+
244
+ function addRefreshTokenRoute() {
245
+ const projectRoot = process.cwd();
246
+ const routesDir = path.join(projectRoot, "src", "routes");
247
+ const refreshTokenFilePath = path.join(routesDir, "refresh-token.js");
248
+
249
+ if (fs.existsSync(refreshTokenFilePath)) {
250
+ console.log(chalk.yellow("Refresh token route already exists."));
251
+ return;
252
+ }
253
+
254
+ const refreshTokenContent = `
255
+ import express from 'express';
256
+ import jwt from 'jsonwebtoken';
257
+
258
+ const router = express.Router();
259
+
260
+ router.post('/', (req, res) => {
261
+ const { refreshToken } = req.body;
262
+ if (!refreshToken) {
263
+ return res.status(401).json({ message: 'Refresh token not provided' });
264
+ }
265
+
266
+ try {
267
+ const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET);
268
+ const payload = { id: decoded.id };
269
+ const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '15m' });
270
+ res.json({ token });
271
+ } catch (err) {
272
+ console.error(err);
273
+ res.status(401).json({ message: 'Invalid refresh token' });
274
+ }
275
+ });
276
+
277
+ export default router;
278
+ `;
279
+
280
+ fs.mkdirSync(routesDir, { recursive: true });
281
+ fs.writeFileSync(refreshTokenFilePath, refreshTokenContent.trim(), "utf8");
282
+
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`;
287
+
288
+ let indexContent = fs.readFileSync(indexPath, "utf8");
289
+ if (!indexContent.includes(routeImport)) {
290
+ indexContent = routeImport + indexContent;
291
+ }
292
+
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
+ }
305
+ }
306
+
307
+ fs.writeFileSync(indexPath, indexContent, "utf8");
308
+
309
+ console.log(chalk.green("Refresh token route created successfully."));
310
+ }
311
+
312
+ const routeFiles = listRoutes();
313
+ const modelFiles = listModels();
314
+
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
+ });
319
+
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);
393
+ }
394
+ }
395
+ );