4bnode
Version:
A professional tool to generate a Node.js app by 4Brains Technologies
396 lines (346 loc) • 11.7 kB
JavaScript
#!/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);
}
}
);