UNPKG

4bnode

Version:

A professional tool to generate a Node.js app by 4Brains Technologies

396 lines (346 loc) 11.7 kB
#!/usr/bin/env node import fs from "fs"; import path from "path"; import readline from "readline"; import crypto from "crypto"; import { execSync } from "child_process"; import chalk from "chalk"; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); function listRoutes() { const projectRoot = process.cwd(); const routesDir = path.join(projectRoot, "src", "routes"); if (!fs.existsSync(routesDir)) { console.error(chalk.red("No routes directory found in src/routes")); process.exit(0); } const routeFiles = fs .readdirSync(routesDir) .filter((file) => file.endsWith(".js")); if (routeFiles.length === 0) { console.error(chalk.red("No route files found in src/routes")); process.exit(0); } return routeFiles; } function listModels() { const projectRoot = process.cwd(); const modelsDir = path.join(projectRoot, "src", "models"); if (!fs.existsSync(modelsDir)) { console.error(chalk.red("No models directory found in src/models")); process.exit(0); } const modelFiles = fs .readdirSync(modelsDir) .filter((file) => file.endsWith(".js")); if (modelFiles.length === 0) { console.error(chalk.red("No model files found in src/models")); process.exit(0); } return modelFiles; } function getModelSchema(modelName) { const projectRoot = process.cwd(); const modelFilePath = path.join( projectRoot, "src", "models", modelName + ".js" ); const modelFileContent = fs.readFileSync(modelFilePath, "utf8"); const schemaRegex = /new mongoose\.Schema\(\s*\{([\s\S]*?)\}\s*\)/m; const match = modelFileContent.match(schemaRegex); if (match) { try { const schemaContent = match[1]; const fieldRegex = /(\w+):\s*\{[\s\S]*?\}/g; const schemaKeys = []; let fieldMatch; while ((fieldMatch = fieldRegex.exec(schemaContent)) !== null) { schemaKeys.push(fieldMatch[1]); } return schemaKeys; } catch (error) { console.error(chalk.red("Error parsing schema: "), error); process.exit(0); } } else { console.error(chalk.red("Schema not found in model file.")); process.exit(0); } } function promptSelectFields(fields, callback) { console.log( chalk.yellow( "Select the fields to include in the data insertion (space-separated list of numbers):" ) ); fields.forEach((field, index) => { console.log(chalk.cyan(`${index + 1}. ${field}`)); }); rl.question( chalk.yellow("Enter the numbers of the fields: "), (fieldAnswer) => { const selectedIndexes = fieldAnswer .split(" ") .map((index) => parseInt(index.trim(), 10) - 1); const selectedFields = selectedIndexes .map((index) => fields[index]) .filter((field) => field !== undefined); callback(selectedFields); rl.close(); } ); } function generateJwtSecret() { return crypto.randomBytes(32).toString("hex"); } function updateEnvFile(filePath, jwtSecret) { const jwtSecretLine = `JWT_SECRET=${jwtSecret}\n`; if (fs.existsSync(filePath)) { let envContent = fs.readFileSync(filePath, "utf8"); if (envContent.includes("JWT_SECRET=")) { envContent = envContent.replace(/JWT_SECRET=.*/, jwtSecretLine.trim()); } else { envContent = envContent.trim() + "\n" + jwtSecretLine; } fs.writeFileSync(filePath, envContent, "utf8"); } else { fs.writeFileSync(filePath, jwtSecretLine, "utf8"); } } function installDependencies() { console.log(chalk.green("Installing dependencies: jsonwebtoken, bcrypt...")); execSync("npm install jsonwebtoken bcrypt", { stdio: "inherit" }); } function addLoginTemplateToRoute(routeFile, fields, modelName, useExpiry) { const projectRoot = process.cwd(); const routeFilePath = path.join(projectRoot, "src", "routes", routeFile); const pascalModelName = modelName.charAt(0).toUpperCase() + modelName.slice(1); const loginTemplateImport = ` import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import ${pascalModelName} from '../models/${modelName}.js'; `; const loginTemplateFunction = ` // Login logic const { ${fields.join(", ")} } = req.body; try { const user = await ${pascalModelName}.findOne({ ${fields[0]}: ${fields[0]} }); if (!user) { return res.status(404).json({ message: 'User not found' }); } ${ fields.includes("password") ? ` const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { return res.status(400).json({ message: 'Invalid credentials' }); }` : "" } const payload = { id: user._id }; ${ useExpiry ? ` const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '15m' }); const refreshToken = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '7d' }); res.json({ token, refreshToken });` : ` const token = jwt.sign(payload, process.env.JWT_SECRET); res.json({ token });` } } catch (err) { console.error(err); res.status(500).json({ message: 'Server error' }); } `; if (!fs.existsSync(routeFilePath)) { console.error( chalk.red(`The specified route file does not exist: ${routeFilePath}`) ); process.exit(0); } let routeContent = fs.readFileSync(routeFilePath, "utf8"); if (!routeContent.includes("import bcrypt")) { routeContent = loginTemplateImport + routeContent; } const lastCloseIndex = routeContent.lastIndexOf("});"); if (lastCloseIndex !== -1) { const updatedContent = [ routeContent.slice(0, lastCloseIndex), loginTemplateFunction, routeContent.slice(lastCloseIndex), ].join("\n"); routeContent = updatedContent; } else { routeContent += `\n${loginTemplateFunction}\n`; } fs.writeFileSync(routeFilePath, routeContent, "utf8"); console.log(chalk.green(`Login template added to ${routeFile}`)); } function addAuthMiddleware() { const projectRoot = process.cwd(); const authFilePath = path.join(projectRoot, "src", "middleware", "auth.js"); if (fs.existsSync(authFilePath)) { console.log(chalk.yellow("Auth middleware already exists.")); return; } const authMiddlewareContent = ` import jwt from 'jsonwebtoken'; const auth = (req, res, next) => { const token = req.header('Authorization')?.replace('Bearer ', ''); if (!token) { return res.status(401).json({ message: 'No token, authorization denied' }); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = decoded; next(); } catch (err) { res.status(401).json({ message: 'Token is not valid' }); } }; export default auth; `; fs.mkdirSync(path.join(projectRoot, "src", "middleware"), { recursive: true, }); fs.writeFileSync(authFilePath, authMiddlewareContent.trim(), "utf8"); console.log(chalk.green("Auth middleware created successfully.")); } function addRefreshTokenRoute() { const projectRoot = process.cwd(); const routesDir = path.join(projectRoot, "src", "routes"); const refreshTokenFilePath = path.join(routesDir, "refresh-token.js"); if (fs.existsSync(refreshTokenFilePath)) { console.log(chalk.yellow("Refresh token route already exists.")); return; } const refreshTokenContent = ` import express from 'express'; import jwt from 'jsonwebtoken'; const router = express.Router(); router.post('/', (req, res) => { const { refreshToken } = req.body; if (!refreshToken) { return res.status(401).json({ message: 'Refresh token not provided' }); } try { const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET); const payload = { id: decoded.id }; const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '15m' }); res.json({ token }); } catch (err) { console.error(err); res.status(401).json({ message: 'Invalid refresh token' }); } }); export default router; `; fs.mkdirSync(routesDir, { recursive: true }); fs.writeFileSync(refreshTokenFilePath, refreshTokenContent.trim(), "utf8"); // Update index.js to include the new route const indexPath = path.join(projectRoot, "index.js"); const routeImport = `import refreshToken from './src/routes/refresh-token.js';\n`; const routeUse = `app.use('/refresh-token', refreshToken);\n`; let indexContent = fs.readFileSync(indexPath, "utf8"); if (!indexContent.includes(routeImport)) { indexContent = routeImport + indexContent; } if (!indexContent.includes(routeUse)) { if (indexContent.includes("app.listen(port")) { indexContent = indexContent.replace( "app.listen(port", routeUse + "app.listen(port" ); } else if (indexContent.includes("server.listen(port")) { indexContent = indexContent.replace( "server.listen(port", routeUse + "server.listen(port" ); } } fs.writeFileSync(indexPath, indexContent, "utf8"); console.log(chalk.green("Refresh token route created successfully.")); } const routeFiles = listRoutes(); const modelFiles = listModels(); console.log(chalk.yellow("Select the route file to modify:")); routeFiles.forEach((file, index) => { console.log(chalk.cyan(`${index + 1}. ${file}`)); }); rl.question( chalk.yellow("Enter the number of the route file: "), (routeAnswer) => { const routeIndex = parseInt(routeAnswer, 10) - 1; if (routeIndex >= 0 && routeIndex < routeFiles.length) { const selectedRoute = routeFiles[routeIndex]; console.log(chalk.yellow("Select the model file to use:")); modelFiles.forEach((file, index) => { console.log(chalk.cyan(`${index + 1}. ${file}`)); }); rl.question( chalk.yellow("Enter the number of the model file: "), (modelAnswer) => { const modelIndex = parseInt(modelAnswer, 10) - 1; if (modelIndex >= 0 && modelIndex < modelFiles.length) { const selectedModel = path.basename(modelFiles[modelIndex], ".js"); const schemaFields = getModelSchema(selectedModel); rl.question( chalk.yellow( "Do you want to use expiry date for JWT token? (y/n): " ), (expiryAnswer) => { const useExpiry = expiryAnswer.trim().toLowerCase() === "y"; promptSelectFields(schemaFields, (fields) => { addLoginTemplateToRoute( selectedRoute, fields, selectedModel, useExpiry ); const jwtSecret = generateJwtSecret(); const projectRoot = process.cwd(); updateEnvFile( path.join(projectRoot, ".env.development"), jwtSecret ); updateEnvFile( path.join(projectRoot, ".env.production"), jwtSecret ); console.log( chalk.green( "JWT_SECRET has been added to .env.development and .env.production files." ) ); installDependencies(); addAuthMiddleware(); if (useExpiry) { addRefreshTokenRoute(); } }); } ); } else { console.error( chalk.red("Invalid selection. Please choose a valid number.") ); rl.close(); process.exit(0); } } ); } else { console.error( chalk.red("Invalid selection. Please choose a valid number.") ); rl.close(); process.exit(0); } } );