diff --git a/bin/configs.js b/bin/configs.js index 1947a84..1dd0223 100644 --- a/bin/configs.js +++ b/bin/configs.js @@ -153,4 +153,14 @@ export const templates = { name: "express_oauth_google", serverPort: "8080:8080", }, + express_auth: { + name: "express_auth", + isUrl: true, + needDB: true, + dbPort: "27017:27017", + dbName: "MongoDB", + serverPort: "8080:8080", + dbDockerImage: "mongo:latest", + dbDataPath: "/data/db", + }, }; diff --git a/templates/express_auth/.env b/templates/express_auth/.env new file mode 100644 index 0000000..721e343 --- /dev/null +++ b/templates/express_auth/.env @@ -0,0 +1,6 @@ +PORT=8080 +DB_HOST=127.0.0.1 +DB_PORT=27017 +DB_NAME=auth_db +JWT_SECRET=your_secret_jwt_key_here +JWT_EXPIRES_IN=1d diff --git a/templates/express_auth/config/authConfig.js b/templates/express_auth/config/authConfig.js new file mode 100644 index 0000000..db472f2 --- /dev/null +++ b/templates/express_auth/config/authConfig.js @@ -0,0 +1,15 @@ +export const authConfig = { + PORT: process.env.PORT || 8080, + DB: { + HOST: process.env.DB_HOST || "127.0.0.1", + PORT: process.env.DB_PORT || 27017, + NAME: process.env.DB_NAME || "auth_db", + }, + JWT: { + SECRET: process.env.JWT_SECRET || "default_secret", + EXPIRES_IN: process.env.JWT_EXPIRES_IN || "1d", + }, + router: { + AUTH_PREFIX: "/api/auth", + }, +}; diff --git a/templates/express_auth/connection/connection.js b/templates/express_auth/connection/connection.js new file mode 100644 index 0000000..e9f87a9 --- /dev/null +++ b/templates/express_auth/connection/connection.js @@ -0,0 +1,14 @@ +import mongoose from "mongoose"; +import { authConfig } from "../config/authConfig.js"; + +const databaseUrl = `mongodb://${authConfig.DB.HOST}:${authConfig.DB.PORT}/${authConfig.DB.NAME}`; + +export const connectDB = async () => { + try { + await mongoose.connect(databaseUrl); + console.log("MongoDB Connected Successfully."); + } catch (err) { + console.error(`MongoDB Connection Failed: ${err.message}`); + process.exit(1); + } +}; diff --git a/templates/express_auth/controller/authController.js b/templates/express_auth/controller/authController.js new file mode 100644 index 0000000..ca35b97 --- /dev/null +++ b/templates/express_auth/controller/authController.js @@ -0,0 +1,56 @@ +import jwt from "jsonwebtoken"; +import { User } from "../models/User.js"; +import { authConfig } from "../config/authConfig.js"; + +const generateToken = (id) => { + return jwt.sign({ id }, authConfig.JWT.SECRET, { + expiresIn: authConfig.JWT.EXPIRES_IN, + }); +}; + +export const registerUser = async (req, res) => { + const { name, email, password } = req.body; + + try { + const userExists = await User.findOne({ email }); + if (userExists) { + return res.status(400).json({ message: "User already exists" }); + } + + const user = await User.create({ name, email, password }); + + res.status(201).json({ + _id: user._id, + name: user.name, + email: user.email, + token: generateToken(user._id), + }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +export const loginUser = async (req, res) => { + const { email, password } = req.body; + + try { + const user = await User.findOne({ email }); + + if (user && (await user.comparePassword(password))) { + res.json({ + _id: user._id, + name: user.name, + email: user.email, + token: generateToken(user._id), + }); + } else { + res.status(401).json({ message: "Invalid email or password" }); + } + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +export const getUserProfile = async (req, res) => { + res.json(req.user); +}; diff --git a/templates/express_auth/index.js b/templates/express_auth/index.js new file mode 100644 index 0000000..8ed7a2e --- /dev/null +++ b/templates/express_auth/index.js @@ -0,0 +1,36 @@ +import express from "express"; +import cors from "cors"; + +import { authConfig } from "./config/authConfig.js"; +import { authRouter } from "./router/authRouter.js"; + +import { initLog } from "./logs/initLog.js"; + +import { connectDB } from "./connection/connection.js"; + +const app = express(); + +initLog(); + +app.use(cors()); +app.use(express.json()); + +app.use(authConfig.router.AUTH_PREFIX, authRouter); + +app.get("/", (req, res) => { + res.json({ message: "auth server is running" }); +}); + +const startServer = async () => { + await connectDB(); + app.listen(authConfig.PORT, () => { + console.info( + `Server is running on http://127.0.0.1:${authConfig.PORT}`, + ); + console.warn( + `Test registration at http://127.0.0.1:${authConfig.PORT}${authConfig.router.AUTH_PREFIX}/register`, + ); + }); +}; + +startServer(); diff --git a/templates/express_auth/logs/initLog.js b/templates/express_auth/logs/initLog.js new file mode 100644 index 0000000..75bdbfa --- /dev/null +++ b/templates/express_auth/logs/initLog.js @@ -0,0 +1,7 @@ +import { existsSync, mkdirSync } from "fs"; + +export const initLog = () => { + if (!existsSync("./logs")) { + mkdirSync("./logs"); + } +}; diff --git a/templates/express_auth/middleware/authMiddleware.js b/templates/express_auth/middleware/authMiddleware.js new file mode 100644 index 0000000..ade3f68 --- /dev/null +++ b/templates/express_auth/middleware/authMiddleware.js @@ -0,0 +1,26 @@ +import jwt from "jsonwebtoken"; +import { authConfig } from "../config/authConfig.js"; +import { User } from "../models/User.js"; + +export const protect = async (req, res, next) => { + let token; + + if ( + req.headers.authorization && + req.headers.authorization.startsWith("Bearer") + ) { + try { + token = req.headers.authorization.split(" ")[1]; + const decoded = jwt.verify(token, authConfig.JWT.SECRET); + + req.user = await User.findById(decoded.id).select("-password"); + next(); + } catch (error) { + res.status(401).json({ message: "Not authorized, token failed" }); + } + } + + if (!token) { + res.status(401).json({ message: "Not authorized, no token" }); + } +}; diff --git a/templates/express_auth/models/User.js b/templates/express_auth/models/User.js new file mode 100644 index 0000000..804894c --- /dev/null +++ b/templates/express_auth/models/User.js @@ -0,0 +1,35 @@ +import mongoose from "mongoose"; +import bcrypt from "bcryptjs"; + +const userSchema = new mongoose.Schema( + { + name: { + type: String, + required: true, + }, + email: { + type: String, + required: true, + unique: true, + lowercase: true, + }, + password: { + type: String, + required: true, + minlength: 6, + }, + }, + { timestamps: true }, +); + +userSchema.pre("save", async function (next) { + if (!this.isModified("password")) return next(); + this.password = await bcrypt.hash(this.password, 10); + next(); +}); + +userSchema.methods.comparePassword = async function (enteredPassword) { + return await bcrypt.compare(enteredPassword, this.password); +}; + +export const User = mongoose.model("User", userSchema); diff --git a/templates/express_auth/package.json b/templates/express_auth/package.json new file mode 100644 index 0000000..6200ab8 --- /dev/null +++ b/templates/express_auth/package.json @@ -0,0 +1,24 @@ +{ + "name": "express_auth", + "version": "1.0.0", + "description": "Express.js server with Custom JWT Authentication and MongoDB.", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js" + }, + "author": "quick_start_express", + "license": "ISC", + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.21.1", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.9.3" + }, + "devDependencies": { + "nodemon": "^3.1.9" + }, + "type": "module" +} diff --git a/templates/express_auth/router/authRouter.js b/templates/express_auth/router/authRouter.js new file mode 100644 index 0000000..22251de --- /dev/null +++ b/templates/express_auth/router/authRouter.js @@ -0,0 +1,16 @@ +import express from "express"; +import { + registerUser, + loginUser, + getUserProfile, +} from "../controller/authController.js"; +import { protect } from "../middleware/authMiddleware.js"; + +const router = express.Router(); + +router.post("/register", registerUser); +router.post("/login", loginUser); + +router.get("/profile", protect, getUserProfile); + +export { router as authRouter }; diff --git a/test/init.test.js b/test/init.test.js index 4303f54..6fe38bd 100644 --- a/test/init.test.js +++ b/test/init.test.js @@ -154,7 +154,7 @@ describe("normal init with default settings", () => { expect(hasNodemon()).toBe(true); expect(nodeModulesExist()).toBe(true); - }, 25000); + }, 60000); }); }); @@ -185,7 +185,7 @@ describe("init --remove-deps", () => { expect(hasNodemon()).toBe(true); expect(nodeModulesExist()).toBe(false); - }, 25000); + }, 60000); }); }); @@ -249,7 +249,7 @@ describe("init with custom template name without installing deps", () => { }, ); verifyPackageName(validName); - }, 25000); + }, 60000); test("valid template name: lowercase only", async () => { const validName = "validname"; @@ -260,7 +260,7 @@ describe("init with custom template name without installing deps", () => { }, ); verifyPackageName(validName); - }, 25000); + }, 60000); test("valid template name: URL friendly characters", async () => { const validName = "valid-name"; @@ -271,7 +271,7 @@ describe("init with custom template name without installing deps", () => { }, ); verifyPackageName(validName); - }, 25000); + }, 60000); // TODO: Add test for cases where `inquirer` prompts are used for this. }); @@ -304,7 +304,7 @@ describe("init without nodemon option without installing deps.", () => { "nodemon", ); } - }, 25000); + }, 60000); }); }); @@ -335,7 +335,7 @@ describe("init with docker-compose without cache service and db", () => { expect(nodeModulesExist()).toBe(false); verifyDockerFiles(); - }, 25000); + }, 60000); }); }); @@ -388,6 +388,6 @@ describe("init with docker-compose with cache service and db", () => { expect(nodeModulesExist()).toBe(false); verifyDockerFiles(); - }, 25000); + }, 60000); }); }); diff --git a/test/list.test.js b/test/list.test.js index c00aa19..5415d45 100644 --- a/test/list.test.js +++ b/test/list.test.js @@ -26,7 +26,8 @@ Available Templates: - express_mysql - express_pg_prisma - express_oauth_microsoft -- express_oauth_google\n`; +- express_oauth_google +- express_auth\n`; describe("List Command", () => { test("list", async () => {