├── .gitignore ├── README.md ├── package.json └── src ├── app.js ├── config └── db.js ├── domains ├── email_verification │ ├── controller.js │ ├── index.js │ └── routes.js ├── forgot_password │ ├── controller.js │ ├── index.js │ └── routes.js ├── otp │ ├── controller.js │ ├── index.js │ ├── model.js │ └── routes.js └── user │ ├── controller.js │ ├── index.js │ ├── model.js │ └── routes.js ├── index.js ├── middleware └── auth.js ├── routes └── index.js └── util ├── createToken.js ├── generateOTP.js ├── hashData.js └── sendEmail.js /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules/ 3 | public/images/ 4 | 5 | .env 6 | yarn.lock 7 | package-lock.json 8 | # .eslintrc.json 9 | note.text 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Node Auth Backend 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-node-auth-backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon src/index.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "bcrypt": "^5.1.1", 14 | "cors": "^2.8.5", 15 | "dotenv": "^16.3.1", 16 | "express": "^4.18.2", 17 | "jsonwebtoken": "^9.0.2", 18 | "mongoose": "^8.0.2", 19 | "nodemailer": "^6.9.7", 20 | "nodemon": "^3.0.1" 21 | } 22 | } -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | // mongodb 2 | require("./config/db"); 3 | 4 | const express = require("express"); 5 | const bodyParser = express.json; 6 | const cors = require("cors"); 7 | const routes = require("./routes/"); 8 | 9 | // create server app 10 | const app = express(); 11 | 12 | app.use(cors()); 13 | app.use(bodyParser()); 14 | app.use("/api/v1", routes); 15 | 16 | module.exports = app; 17 | -------------------------------------------------------------------------------- /src/config/db.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const mongoose = require("mongoose"); 3 | 4 | // uri 5 | const { MONGODB_URI } = process.env; 6 | 7 | const connectToDB = async () => { 8 | try { 9 | await mongoose.connect(MONGODB_URI, { 10 | useNewUrlParser: true, 11 | useUnifiedTopology: true, 12 | }); 13 | console.log("DB Connected"); 14 | } catch (error) { 15 | console.log(error); 16 | } 17 | } 18 | 19 | connectToDB(); 20 | -------------------------------------------------------------------------------- /src/domains/email_verification/controller.js: -------------------------------------------------------------------------------- 1 | const User = require("../user/model"); 2 | const { sendOTP, verifyOTP, deleteOTP } = require("../otp/controller"); 3 | 4 | const verifyUserEmail = async ({ email, otp }) => { 5 | try { 6 | const validIOTP = await verifyOTP({ email, otp }); 7 | if (!validIOTP) { 8 | throw Error("Invalid code passed. Check your inbox.") 9 | } 10 | // now update user record to show verified. 11 | await User.updateOne({ email }, { verified: true }); 12 | 13 | await deleteOTP(email); 14 | return; 15 | } catch (error) { 16 | throw error; 17 | } 18 | } 19 | 20 | const sendVerificationOTPEmail = async (email) => { 21 | try { 22 | // check if an account exits 23 | const exitingUser = await User.findOne({ email }); 24 | if (!exitingUser) { 25 | throw Error("There's no account for the provided email.") 26 | } 27 | 28 | const otpDetails = { 29 | email, 30 | subject: "Email Verification", 31 | message: "Verify your email with the code below.", 32 | duration: 1, 33 | }; 34 | const createdOTP = await sendOTP(otpDetails); 35 | return createdOTP; 36 | } catch (error) { 37 | throw error; 38 | } 39 | }; 40 | 41 | module.exports = { sendVerificationOTPEmail, verifyUserEmail }; 42 | -------------------------------------------------------------------------------- /src/domains/email_verification/index.js: -------------------------------------------------------------------------------- 1 | const routes = require("./routes"); 2 | 3 | module.exports = routes; 4 | -------------------------------------------------------------------------------- /src/domains/email_verification/routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const { sendVerificationOTPEmail, verifyUserEmail } = require("./controller") 4 | 5 | router.post("/verify", async (req, res) => { 6 | try { 7 | let { email, otp } = req.body; 8 | 9 | if (!(email && otp)) throw Error("Empty otp details are not allowed") 10 | 11 | await verifyUserEmail({ email, otp }); 12 | res.status(200).json({ email, verified: true }); 13 | } catch (error) { 14 | res.status(400).send(error.message); 15 | } 16 | }) 17 | 18 | // request new verification otp 19 | router.post("/", async (req, res) => { 20 | try { 21 | const { email } = req.body; 22 | if (!email) throw Error("An email is required"); 23 | 24 | const createdEmailVerificationOTP = await sendVerificationOTPEmail(email); 25 | res.status(200).json(createdEmailVerificationOTP); 26 | } catch (error) { 27 | res.status(400).send(error.message); 28 | } 29 | }) 30 | 31 | module.exports = router; 32 | -------------------------------------------------------------------------------- /src/domains/forgot_password/controller.js: -------------------------------------------------------------------------------- 1 | const User = require("./../user/model"); 2 | const { sendOTP, verifyOTP, deleteOTP } = require("./../otp/controller"); 3 | const { hashData } = require("./../../util/hashData"); 4 | 5 | const resetUserPassword = async ({ email, otp, newPassword }) => { 6 | try { 7 | const validIOTP = await verifyOTP({ email, otp }); 8 | if (!validIOTP) { 9 | throw Error("Invalid code passed. Check your inbox."); 10 | } 11 | 12 | // now update user record with new password. 13 | if (newPassword.length < 8) { 14 | throw Error("Password is too short!"); 15 | } 16 | const hashedNewPassword = await hashData(newPassword); 17 | await User.updateOne({ email }, { password: hashedNewPassword }); 18 | await deleteOTP(email); 19 | 20 | return; 21 | } catch (error) { 22 | throw error; 23 | } 24 | } 25 | 26 | const sendPasswordResetOTPEmail = async (email) => { 27 | try { 28 | // check if an account exists 29 | const existingUser = await User.findOne({ email }); 30 | if (!existingUser) { 31 | throw Error("There's no account for the provided email."); 32 | } 33 | 34 | if (!existingUser.verified) { 35 | throw Error("Email hasn't been verified yet. Check your inbox.") 36 | } 37 | 38 | const otpDetails = { 39 | email, 40 | subject: "Password Reset", 41 | message: "Enter the code below to reset your password.", 42 | duration: 1, 43 | } 44 | const createdOTP = await sendOTP(otpDetails); 45 | return createdOTP; 46 | } catch (error) { 47 | throw error; 48 | } 49 | }; 50 | 51 | module.exports = { sendPasswordResetOTPEmail, resetUserPassword }; 52 | -------------------------------------------------------------------------------- /src/domains/forgot_password/index.js: -------------------------------------------------------------------------------- 1 | const routes = require("./routes"); 2 | 3 | module.exports = routes; 4 | -------------------------------------------------------------------------------- /src/domains/forgot_password/routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const { sendPasswordResetOTPEmail, resetUserPassword } = require("./controller"); 4 | 5 | router.post("/reset", async (req, res) => { 6 | try { 7 | let { email, otp, newPassword } = req.body; 8 | if (!(email && otp && newPassword)) 9 | throw Error("Empty credentials are not allowed."); 10 | 11 | await resetUserPassword({ email, otp, newPassword }); 12 | res.status(200).json({ email, passwordreset: true }); 13 | } catch (error) { 14 | res.status(400).send(error.message); 15 | } 16 | }) 17 | 18 | // Password reset request 19 | router.post("/", async (req, res) => { 20 | try { 21 | const { email } = req.body; 22 | if (!email) throw Error("An email is required."); 23 | 24 | const createdPasswordResetOTP = await sendPasswordResetOTPEmail(email); 25 | res.status(200).json(createdPasswordResetOTP); 26 | } catch (error) { 27 | res.status(400).send(error.message); 28 | } 29 | }) 30 | 31 | module.exports = router; 32 | -------------------------------------------------------------------------------- /src/domains/otp/controller.js: -------------------------------------------------------------------------------- 1 | const OTP = require("./model"); 2 | const generateOTP = require("./../../util/generateOTP"); 3 | const sendEmail = require("./../../util/sendEmail"); 4 | const { hashData, verifyHashedData } = require("./../../util/hashData"); 5 | const { AUTH_EMAIL } = process.env; 6 | 7 | const verifyOTP = async ({ email, otp }) => { 8 | try { 9 | if (!(email && otp)) { 10 | throw Error("Provide values for email, otp"); 11 | } 12 | 13 | // ensure otp record exists 14 | const matchedOTPRecord = await OTP.findOne({ 15 | email, 16 | }); 17 | 18 | // Logging for debugging 19 | console.log("Matched OTP Record:", matchedOTPRecord); 20 | 21 | if (!matchedOTPRecord) { 22 | throw Error("No otp records found."); 23 | } 24 | 25 | const { expiresAt } = matchedOTPRecord; 26 | 27 | // checking for expired code 28 | if (expiresAt < Date.now()) { 29 | await OTP.deleteOne({ email }); 30 | throw Error("Code has expired. Request for a new one."); 31 | } 32 | 33 | // not expired yet, verify value 34 | const hashedOTP = matchedOTPRecord.otp; 35 | const validOTP = await verifyHashedData(otp, hashedOTP); 36 | return validOTP; 37 | } catch (error) { 38 | throw error; 39 | } 40 | } 41 | 42 | 43 | const sendOTP = async ({ email, subject, message, duration = 1 }) => { 44 | try { 45 | if (!(email && subject && message)) { 46 | throw Error("Provide values for email, subject, message"); 47 | } 48 | 49 | // clear any old record 50 | await OTP.deleteOne({ email }); 51 | 52 | // generate pin 53 | const generatedOTP = await generateOTP(); 54 | 55 | // send email 56 | const mailOptions = { 57 | from: AUTH_EMAIL, 58 | to: email, 59 | subject, 60 | html: `

${message}

${generatedOTP}

This code expires in ${duration} hour(s)`, 61 | }; 62 | await sendEmail(mailOptions) 63 | 64 | // save otp record 65 | const hashedOTP = await hashData(generatedOTP); 66 | const newOTP = await new OTP({ 67 | email, 68 | otp: hashedOTP, 69 | createdAt: Date.now(), 70 | expiresAt: Date.now() + 3600000 * +duration, 71 | }); 72 | 73 | 74 | const createdOTPRecord = await newOTP.save(); 75 | return createdOTPRecord; 76 | } catch (error) { 77 | throw error; 78 | } 79 | }; 80 | 81 | const deleteOTP = async (email) => { 82 | try { 83 | await OTP.deleteOne({ email }); 84 | } catch (error) { 85 | throw error; 86 | } 87 | } 88 | 89 | module.exports = { sendOTP, verifyOTP, deleteOTP }; 90 | -------------------------------------------------------------------------------- /src/domains/otp/index.js: -------------------------------------------------------------------------------- 1 | const routes = require("./routes"); 2 | 3 | module.exports = routes; 4 | -------------------------------------------------------------------------------- /src/domains/otp/model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const OTPSchema = new Schema({ 5 | email: { type: String, unique: true }, 6 | otp: String, 7 | createdAt: Date, 8 | expiresAt: Date, 9 | }); 10 | 11 | const OTP = mongoose.model("OTP", OTPSchema); 12 | 13 | module.exports = OTP; 14 | -------------------------------------------------------------------------------- /src/domains/otp/routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const { sendOTP, verifyOTP } = require("./controller"); 4 | 5 | router.post("/verify", async (req, res) => { 6 | try { 7 | let { email, otp } = req.body; 8 | 9 | const validOTP = await verifyOTP({ email, otp }); 10 | res.status(200).json({ valid: validOTP }); 11 | } catch (error) { 12 | res.status(400).send(error.message) 13 | } 14 | }) 15 | 16 | // request new verification otp 17 | router.post("/", async (req, res) => { 18 | try { 19 | const { email, subject, message, duration } = req.body 20 | 21 | const createdOTP = await sendOTP({ 22 | email, 23 | subject, 24 | message, 25 | duration, 26 | }) 27 | res.status(200).json(createdOTP); 28 | } catch (error) { 29 | res.status(400).send(error.message); 30 | } 31 | }); 32 | 33 | module.exports = router; 34 | -------------------------------------------------------------------------------- /src/domains/user/controller.js: -------------------------------------------------------------------------------- 1 | const User = require("./model") 2 | const { hashData, verifyHashedData } = require("./../../util/hashData"); 3 | const createToken = require("./../../util/createToken"); 4 | 5 | const authenticateUser = async (data) => { 6 | try { 7 | const { email, password } = data; 8 | 9 | const fetchedUser = await User.findOne({ email }); 10 | 11 | if (!fetchedUser) { 12 | throw Error("Invalid crediatials entered!"); 13 | } 14 | if (!fetchedUser.verified) { 15 | throw Error("Email hasn't been verified yet. Check your inbox."); 16 | } 17 | 18 | const hashedPassword = fetchedUser.password; 19 | const passwordMatch = await verifyHashedData(password, hashedPassword); 20 | 21 | if (!passwordMatch) { 22 | throw Error("Invalid password entered!"); 23 | } 24 | 25 | // create user token 26 | const tokenData = { userId: fetchedUser._id, email }; 27 | const token = await createToken(tokenData); 28 | 29 | // assign user token 30 | fetchedUser.token = token 31 | return fetchedUser; 32 | } catch (error) { 33 | throw error; 34 | } 35 | } 36 | 37 | const createNewUser = async (data) => { 38 | try { 39 | const { name, email, password } = data; 40 | 41 | // Checking if user already exits 42 | const existingUser = await User.findOne({ email }); 43 | 44 | if (existingUser) { 45 | throw Error("User with the provided email already exists"); 46 | } 47 | 48 | // hash password 49 | const hashedPassword = await hashData(password) 50 | const newUser = new User({ 51 | name, 52 | email, 53 | password: hashedPassword, 54 | }) 55 | // save user 56 | const createdUser = await newUser.save(); 57 | return createdUser; 58 | } catch (error) { 59 | throw error; 60 | } 61 | }; 62 | 63 | module.exports = { createNewUser, authenticateUser }; 64 | -------------------------------------------------------------------------------- /src/domains/user/index.js: -------------------------------------------------------------------------------- 1 | const routes = require("./routes"); 2 | 3 | module.exports = routes; -------------------------------------------------------------------------------- /src/domains/user/model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const UserSchema = new Schema({ 5 | name: String, 6 | email: { type: String, unique: true }, 7 | password: String, 8 | token: String, 9 | verified: { type: Boolean, default: false }, 10 | }); 11 | 12 | const User = mongoose.model("User", UserSchema); 13 | 14 | module.exports = User; -------------------------------------------------------------------------------- /src/domains/user/routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const { createNewUser, authenticateUser } = require("./controller"); 4 | const auth = require("./../../middleware/auth"); 5 | const { sendVerificationOTPEmail } = require("./../email_verification/controller"); 6 | 7 | // procted route 8 | router.get("/private_data", auth, (req, res) => { 9 | res 10 | .status(200) 11 | .send(`You're in the private territory of ${req.currentUser.email}`); 12 | }) 13 | 14 | // Signin 15 | router.post("/", async (req, res) => { 16 | try { 17 | let { email, password } = req.body; 18 | email = email.trim(); 19 | password = password.trim(); 20 | 21 | if (!(email && password)) { 22 | throw Error("Empty credentials supplied!") 23 | } 24 | 25 | const authenticatedUser = await authenticateUser({ email, password }); 26 | 27 | res.status(200).json(authenticatedUser); 28 | } catch (error) { 29 | res.status(400).send(error.message); 30 | } 31 | }) 32 | 33 | // Signup 34 | router.post("/signup", async (req, res) => { 35 | try { 36 | let { name, email, password } = req.body 37 | name = name.trim(); 38 | email = email.trim(); 39 | password = password.trim(); 40 | 41 | if (!(name && email && password)) { 42 | throw Error("Empty input fields!"); 43 | } else if (!/^[a-zA-Z]*$/.test(name)) { 44 | throw Error("Invalid name entered"); 45 | } else if (!/^[\w=\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(email)) { 46 | throw Error("Invalid email entered"); 47 | } else if (password.length < 8) { 48 | throw Error("Password is too short!"); 49 | } else { 50 | // good credentials, create new user 51 | const newUser = await createNewUser({ 52 | name, 53 | email, 54 | password, 55 | }); 56 | await sendVerificationOTPEmail(email); 57 | res.status(200).json(newUser); 58 | } 59 | } catch (error) { 60 | res.status(400).send(error.message); 61 | } 62 | }); 63 | 64 | module.exports = router; 65 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const app = require("./app"); 2 | const { PORT } = process.env; 3 | 4 | const startApp = () => { 5 | app.listen(PORT, () => { 6 | console.log(`Auth Backend running on port ${PORT}`); 7 | }); 8 | }; 9 | 10 | startApp(); -------------------------------------------------------------------------------- /src/middleware/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | 3 | const { TOKEN_KEY } = process.env; 4 | 5 | const verifyToken = async (req, res, next) => { 6 | const token = 7 | req.body.token || req.query.token || req.headers["x-access-token"]; 8 | 9 | // check for provided token 10 | if (!token) { 11 | return res.status(403).send("An authentication token is required"); 12 | } 13 | 14 | // verify token 15 | try { 16 | const decodedToken = await jwt.verify(token, TOKEN_KEY); 17 | req.currentUser = decodedToken; 18 | } catch (error) { 19 | return res.status(401).send("Invalid Token provided"); 20 | } 21 | 22 | // proceed with request 23 | return next(); 24 | }; 25 | 26 | module.exports = verifyToken; -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | const userRoutes = require("../domains/user"); 5 | const OTPRoutes = require("../domains/otp"); 6 | const EmailVerificationRoutes = require("./../domains/email_verification"); 7 | const ForgotPasswordRoutes = require("./../domains/forgot_password"); 8 | 9 | router.use("/user", userRoutes); 10 | router.use("/otp", OTPRoutes); 11 | router.use("/email_verification", EmailVerificationRoutes); 12 | router.use("/forgot_password", ForgotPasswordRoutes); 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /src/util/createToken.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | 3 | const { TOKEN_KEY, TOKEN_EXPIRY } = process.env 4 | 5 | const createToken = async ( 6 | tokenData, 7 | tokenKey = TOKEN_KEY, 8 | expiresIn = TOKEN_EXPIRY 9 | ) => { 10 | try { 11 | const token = await jwt.sign(tokenData, tokenKey, { 12 | expiresIn, 13 | }); 14 | return token 15 | } catch (error) { 16 | throw error; 17 | } 18 | } 19 | 20 | module.exports = createToken; 21 | -------------------------------------------------------------------------------- /src/util/generateOTP.js: -------------------------------------------------------------------------------- 1 | const generateOTP = async () => { 2 | try { 3 | return (otp = `${Math.floor(1000 + Math.random() * 9000)}`); 4 | } catch (error) { 5 | throw error; 6 | } 7 | }; 8 | 9 | module.exports = generateOTP; 10 | -------------------------------------------------------------------------------- /src/util/hashData.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require("bcrypt"); 2 | 3 | const hashData = async (data, saltRounds = 10) => { 4 | try { 5 | const hashedData = await bcrypt.hash(data, saltRounds); 6 | return hashedData; 7 | } catch (error) { 8 | throw error; 9 | } 10 | } 11 | 12 | const verifyHashedData = async (unhashed, hashed) => { 13 | try { 14 | const match = await bcrypt.compare(unhashed, hashed); 15 | return match; 16 | } catch (error) { 17 | throw error 18 | } 19 | } 20 | 21 | module.exports = { hashData, verifyHashedData }; -------------------------------------------------------------------------------- /src/util/sendEmail.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require("nodemailer"); 2 | 3 | const { AUTH_EMAIL, AUTH_PASS } = process.env; 4 | let transporter = nodemailer.createTransport({ 5 | service: "gmail", 6 | auth: { 7 | user: AUTH_EMAIL, 8 | pass: AUTH_PASS, 9 | }, 10 | }); 11 | 12 | // test transporter 13 | transporter.verify((error, success) => { 14 | if (error) { 15 | console.log(error); 16 | } else { 17 | console.log("Ready for messages"); 18 | console.log(success); 19 | } 20 | }); 21 | 22 | const sendEmail = async (mailOptions) => { 23 | try { 24 | await transporter.sendMail(mailOptions); 25 | return; 26 | } catch (error) { 27 | throw error; 28 | } 29 | }; 30 | 31 | module.exports = sendEmail; 32 | --------------------------------------------------------------------------------