From f7e3660dcaa8879eed8441ad209634058c27d972 Mon Sep 17 00:00:00 2001 From: dovakiin0 Date: Mon, 18 Sep 2023 17:00:56 +0545 Subject: [PATCH] Routes for OTP generation and verification --- server/src/controllers/user.controller.ts | 177 ++++++++++++++++------ server/src/helper/util.ts | 4 + server/src/models/User.ts | 3 + server/src/routes/user.routes.ts | 4 + server/src/test/unit.test.ts | 9 ++ server/src/types/IUser.ts | 3 + 6 files changed, 152 insertions(+), 48 deletions(-) create mode 100644 server/src/helper/util.ts diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts index fee6e4d..2dc786f 100644 --- a/server/src/controllers/user.controller.ts +++ b/server/src/controllers/user.controller.ts @@ -1,15 +1,17 @@ -import type { NextFunction, Response } from "express"; +import type { Response } from "express"; import User from "../models/User"; import asyncHandler from "express-async-handler"; import { generateJWT } from "../helper/jwt"; import { IRequest } from "../types/IRequest"; +import { generateOTP } from "../helper/util"; +import { sendEmail } from "../helper/mailer"; /* @Desc Get all users @Route /api/auth @Method GET */ -const getAll = asyncHandler(async (req: IRequest, res: Response) => { +const getAll = asyncHandler(async (_: IRequest, res: Response) => { const users = await User.find({}).select("-password"); res.status(200).json({ count: users.length, users }); }); @@ -28,40 +30,43 @@ const getMe = asyncHandler(async (req: IRequest, res: Response) => { @Route /api/auth/ @Method POST */ -const login = asyncHandler( - async (req: IRequest, res: Response, next: NextFunction) => { - const { email, password } = req.body; - const user = await User.findOne({ email }); +const login = asyncHandler(async (req: IRequest, res: Response) => { + const { email, password } = req.body; + const user = await User.findOne({ email }); - if (!user) { - res.status(401); - throw new Error("Email or password is incorrect"); - } + if (!user) { + res.status(401); + throw new Error("Email or password is incorrect"); + } - if (await user.comparePassword(password)) { - const expireDate = 7 * 24 * 60 * 60 * 1000; // 7 days - - res - .status(200) - .cookie("access_token", generateJWT(user._id), { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - expires: new Date(Date.now() + expireDate), - }) - .json({ - success: true, - user: { - id: user._id, - email: user.email, - username: user.username, - }, - }); - } else { - res.status(401); - throw new Error("Email or password incorrect"); - } - }, -); + if (!user.isVerified) { + res.status(400); + throw new Error("Please verify your email"); + } + + if (await user.comparePassword(password)) { + const expireDate = 7 * 24 * 60 * 60 * 1000; // 7 days + + res + .status(200) + .cookie("access_token", generateJWT(user._id), { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + expires: new Date(Date.now() + expireDate), + }) + .json({ + success: true, + user: { + id: user._id, + email: user.email, + username: user.username, + }, + }); + } else { + res.status(401); + throw new Error("Email or password incorrect"); + } +}); /* @Desc Register new User @@ -85,27 +90,103 @@ const registerUser = asyncHandler(async (req: IRequest, res: Response) => { await user.save(); - const expireDate = 7 * 24 * 60 * 60 * 1000; // 7 days + res.status(201).json({ + success: true, + user: { + _id: user._id, + }, + }); +}); - res - .status(201) - .cookie("access_token", generateJWT(user._id), { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - expires: new Date(Date.now() + expireDate), - }) - .json({ - success: true, - user: { - email: user.email, - fullName: user.username, +/* + @Desc Generates new otp for the user + @Route /api/auth/otp + @Method POST +*/ +const generateOtpForUser = asyncHandler( + async (req: IRequest, res: Response) => { + const { id } = req.body; + + const user = await User.findById({ id }); + if (!user) { + res.status(400); + throw new Error("User does not exists"); + } + + const otpExpirationTimeStamp = new Date(); + otpExpirationTimeStamp.setMinutes(otpExpirationTimeStamp.getMinutes() + 15); // 15 minutes + + const generatedOTP = generateOTP(); // generate a new OTP + + user.otp = generatedOTP; + user.otpExpiration = otpExpirationTimeStamp; + + await user.save(); + + // send OTP to the user email address + sendEmail({ + to: user.email, + subject: "Verification Code", + template: "main", + context: { + username: user.username, + otp: generatedOTP.toString(), }, }); + + res.status(400).json({ + success: true, + message: "OTP has been sent to your email address", + }); + }, +); + +/* + @Desc Gets OTP to verify user account + @Route /api/auth/otp/verify + @Method POST +*/ +const verifyOTP = asyncHandler(async (req: IRequest, res: Response) => { + const { otp, id } = req.body; + const user = await User.findById({ id }); + + if (!user || !user.otp || !user.otpExpiration) { + res.status(400); + throw new Error("User does not exists"); // User not found or OTP information missing + } + + const currentTimeStamp = new Date(); + + if (user.otp !== otp && currentTimeStamp > user.otpExpiration) { + res.status(400); + throw new Error("Your otp has been expired"); + } + + user.isVerified = true; + user.otp = null; + user.otpExpiration = null; + + res + .status(200) + .json({ success: true, message: "Account verified, Please login!" }); }); +/* + @Desc Clears the cookie and logs out the user + @Route /api/auth/logout + @Method POST +*/ const logout = asyncHandler(async (req: IRequest, res: Response) => { res.clearCookie("access_token"); res.status(200).json({ success: true, message: "Logged out successfully" }); }); -export { registerUser, login, getAll, logout, getMe }; +export { + registerUser, + login, + getAll, + logout, + getMe, + verifyOTP, + generateOtpForUser, +}; diff --git a/server/src/helper/util.ts b/server/src/helper/util.ts new file mode 100644 index 0000000..1236c22 --- /dev/null +++ b/server/src/helper/util.ts @@ -0,0 +1,4 @@ +// basic utility function to generate 6 digit OTP number +export function generateOTP(): number { + return Math.floor(100000 + Math.random() * 900000); +} diff --git a/server/src/models/User.ts b/server/src/models/User.ts index ed2bbf2..23432a2 100644 --- a/server/src/models/User.ts +++ b/server/src/models/User.ts @@ -7,6 +7,9 @@ const UserSchema = new mongoose.Schema( username: { type: String, required: true }, email: { type: String, unique: true, required: true }, password: { type: String, required: true }, + isVerified: { type: Boolean, default: false }, + otp: { type: Number }, + otpExpiration: { type: Date }, }, { timestamps: true, diff --git a/server/src/routes/user.routes.ts b/server/src/routes/user.routes.ts index ed6e120..95dc426 100644 --- a/server/src/routes/user.routes.ts +++ b/server/src/routes/user.routes.ts @@ -5,6 +5,8 @@ import { login, logout, registerUser, + verifyOTP, + generateOtpForUser, } from "../controllers/user.controller"; import { isAuth } from "../middlewares/auth"; @@ -13,6 +15,8 @@ const router = Router(); router.get("/", getAll); router.get("/@me", isAuth, getMe); router.post("/", login); +router.post("/otp", generateOtpForUser); +router.post("/otp/verify", verifyOTP); router.post("/register", registerUser); router.post("/logout", isAuth, logout); diff --git a/server/src/test/unit.test.ts b/server/src/test/unit.test.ts index cc1bc28..65ce0bd 100644 --- a/server/src/test/unit.test.ts +++ b/server/src/test/unit.test.ts @@ -5,6 +5,7 @@ import { hashPassword, verifyJWT, } from "../helper/jwt"; +import { generateOTP } from "../helper/util"; describe("Unit Test", () => { let password: string = "HelloWorld"; @@ -40,4 +41,12 @@ describe("Unit Test", () => { expect(typeof compare).toBe("boolean"); }); }); + + describe("Generate OTP", () => { + it("Should generate 6 digit number", () => { + let otp = generateOTP(); + expect(typeof otp).toBe("number"); + expect(otp.toString().length).toBe(6); + }); + }); }); diff --git a/server/src/types/IUser.ts b/server/src/types/IUser.ts index 98d8c28..390d9a3 100644 --- a/server/src/types/IUser.ts +++ b/server/src/types/IUser.ts @@ -5,6 +5,9 @@ export interface IUser extends mongoose.Document { email: string; password: string; token?: string; + otp: number | null; + otpExpiration: Date | null; + isVerified: boolean; createdAt: Date; updatedAt: Date; comparePassword(password: string): Promise;