Skip to content

Commit

Permalink
Routes for OTP generation and verification
Browse files Browse the repository at this point in the history
  • Loading branch information
Dovakiin0 committed Sep 18, 2023
1 parent 60daa82 commit f7e3660
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 48 deletions.
177 changes: 129 additions & 48 deletions server/src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
@@ -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 });
});
Expand All @@ -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
Expand All @@ -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,
};
4 changes: 4 additions & 0 deletions server/src/helper/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// basic utility function to generate 6 digit OTP number
export function generateOTP(): number {
return Math.floor(100000 + Math.random() * 900000);
}
3 changes: 3 additions & 0 deletions server/src/models/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions server/src/routes/user.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
login,
logout,
registerUser,
verifyOTP,
generateOtpForUser,
} from "../controllers/user.controller";
import { isAuth } from "../middlewares/auth";

Expand All @@ -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);

Expand Down
9 changes: 9 additions & 0 deletions server/src/test/unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
hashPassword,
verifyJWT,
} from "../helper/jwt";
import { generateOTP } from "../helper/util";

describe("Unit Test", () => {
let password: string = "HelloWorld";
Expand Down Expand Up @@ -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);
});
});
});
3 changes: 3 additions & 0 deletions server/src/types/IUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>;
Expand Down

0 comments on commit f7e3660

Please sign in to comment.