Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added OTP verification for signup #32

Merged
merged 10 commits into from
May 31, 2024
289 changes: 289 additions & 0 deletions backend/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^6.9.13",
"nodemon": "^3.1.1",
"prisma": "^5.14.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
Expand Down
1 change: 1 addition & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ model User {
username String
email String
passwordHash String
otp Int?
posts Post[] @relation("authorPosts")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Expand Down
93 changes: 93 additions & 0 deletions backend/src/helpers/sendMail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// import nodemailer from 'nodemailer';
// @ts-nocheck
import nodemailer from "nodemailer";
// nodemailer = require('nodemailer');

export const sendVerificationEmail = async (email: string, otp: number) => {
// console.log("Email: ", process.env.EMAIL_USER, process.env.EMAIL_PASS)
let transporter = nodemailer.createTransport({
service: 'Gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});

let info = await transporter.sendMail({
from: '"Style Share" <yourapp@example.com>',
to: email,
subject: "Email Verification",
text: `Your OTP for email verification is ${otp}`,
// This html is a simple html template for the email body
html: `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OTP Verification</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.container {
width: 100%;
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
.header {
background-color: #4CAF50;
color: white;
padding: 10px 0;
text-align: center;
border-radius: 8px 8px 0 0;
}
.content {
margin: 20px 0;
text-align: center;
}
.otp {
font-size: 24px;
font-weight: bold;
margin: 20px 0;
color: #4CAF50;
}
.footer {
text-align: center;
color: #888888;
font-size: 12px;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>OTP Verification</h1>
</div>
<div class="content">
<p>Hello,</p>
<p>Thank you for signing up. To complete your registration, please use the following OTP (One Time Password) to verify your email address:</p>
<div class="otp">${otp}</div>
<p>This OTP is valid for 10 minutes. Please do not share it with anyone.</p>
</div>
<div class="footer">
<p>If you did not request this OTP, please ignore this email.</p>
<p>Thank you!</p>
</div>
</div>
</body>
</html>

`
});

// console.log("Message sent: %s", info.messageId);
};
1 change: 1 addition & 0 deletions backend/src/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const authMiddleware = (req:UserAuthRequest, res:Response, next: NextFunction) =
}

req.userId = decodedValue.id;
// console.log("IN JWT:'",req.userId)
next();
}

Expand Down
163 changes: 143 additions & 20 deletions backend/src/routes/user/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { signinBodySchema, signupBodySchema } from "./zodSchema";
import { createHash, validatePassword } from "../../helpers/hash";
import { createJWT } from "../../helpers/jwt";
import { UserAuthRequest } from "../../helpers/types";
import crypto from "crypto";
import { sendVerificationEmail } from "../../helpers/sendMail";
import { z } from "zod";

export const userSignupController = async (req: Request, res: Response) => {
try {
Expand Down Expand Up @@ -36,47 +39,157 @@ export const userSignupController = async (req: Request, res: Response) => {
});

if (existingUser) {
return res.status(411).json({
error: {
message: "Username or email already in use.",
if (!existingUser.otp) {
return res.status(411).json({
error: {
message: "Username or email already in use.",
},
});

}
}



const passwordHash = await createHash(data.password);

const otp = crypto.randomInt(100000, 999999); // Generate a 6-digit OTP

let user;
if(!existingUser){
user = await prisma.user.create({
data: {
email: data.email,
passwordHash,
username: data.username,
// @ts-ignore
otp,
},
select: {
id: true,
email: true,
username: true,
},
});
}
else if(existingUser && existingUser.otp){
user = await prisma.user.update({
where: {
id: existingUser.id
},
data: {
otp: otp,
passwordHash,
username: data.username,
},
select: {
id: true,
email: true,
username: true,
}
})
}


// Send OTP to user's email
await sendVerificationEmail(user!.email, otp);

const passwordHash = await createHash(data.password);


const user = await prisma.user.create({
data: {
email: data.email,
passwordHash,
username: data.username,
res.status(201).json({
message: "User created Successfully.",
user,
});
} catch (error) {
console.log(error)
return res.status(500).json({
error: {
message: "An unexpected exception occurred! Brooo",
display: error
},
});
}
};

// import prisma from "../../db";

const otpVerificationSchema = z.object({
userId: z.string(),
otp: z.number(),
username: z.string(),
});

export const verifyOtpController = async (req: Request, res: Response) => {
try {
const payload = req.body;
const result = otpVerificationSchema.safeParse(payload);

if (!result.success) {
const formattedError: any = {};
result.error.errors.forEach((e) => {
formattedError[e.path[0]] = e.message;
});
return res.status(400).json({
error: { ...formattedError, message: "Validation error." },
});
}

const { userId, otp, username } = result.data;

const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
username: true,
otp: true,
createdAt: true,
},
});

if (!user) {
return res.status(404).json({
error: { message: "User not found." },
});
}

if (user.otp !== otp) {
return res.status(400).json({
error: { message: "Invalid OTP." },
});
}

const otpAge = Date.now() - new Date(user.createdAt).getTime();
const otpExpiry = 10 * 60 * 1000; // 10 minutes

if (otpAge > otpExpiry) {
return res.status(400).json({
error: { message: "OTP has expired." },
});
}
const token = createJWT({
id: user.id,
username: user.username,
id: user!.id,
username: username,
});
await prisma.user.update({
where: { id: userId },
data: {
otp: null,
},
});

res.status(201).json({
message: "User created Successfully.",
user,
token: token,
return res.status(200).json({
message: "Email verified successfully.",
token,
});
} catch (error) {
console.error("OTP verification error:", error);
return res.status(500).json({
error: {
message: "An unexpected exception occurred!",
},
error: { message: "An unexpected error occurred." },
});
}
};



export const userSigninController = async (req: Request, res: Response) => {
try {
const payload = req.body;
Expand All @@ -99,6 +212,7 @@ export const userSigninController = async (req: Request, res: Response) => {
email: data.email,
},
select: {
otp: true,
id: true,
username: true,
email: true,
Expand All @@ -114,6 +228,15 @@ export const userSigninController = async (req: Request, res: Response) => {
});
}

if (user.otp) {
console.log("Email really not verified")
return res.status(411).json({
error: {
message: "Email not verified",
},
});
}

const matchPassword = await validatePassword(
data.password,
user.passwordHash
Expand Down
4 changes: 3 additions & 1 deletion backend/src/routes/user/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Router } from "express";
import { userProfileController, userSigninController, userSignupController } from "./controller";
import { userProfileController, userSigninController, userSignupController, verifyOtpController } from "./controller";
import authMiddleware from "../../middleware/auth"

const userRouter = Router();
Expand All @@ -8,6 +8,8 @@ userRouter.post('/signup', userSignupController)

userRouter.post('/signin', userSigninController)

userRouter.post('/verify', verifyOtpController)

userRouter.get('/me', authMiddleware, userProfileController);

export default userRouter;
4 changes: 2 additions & 2 deletions backend/src/routes/user/zodSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ export const signupBodySchema = zod.object({
.string()
.min(5, { message: "Username too short!" })
.max(30, { message: "Username too long!" }),
email: zod.string().email().max(30, { message: "Email too long!" }),
email: zod.string().email().max(80, { message: "Email too long!" }),
password: zod
.string()
.min(8, { message: "Password too short!" })
.max(30, { message: "Password too long!" }),
});

export const signinBodySchema = zod.object({
email: zod.string().email().max(30, { message: "Email too long!" }),
email: zod.string().email().max(80, { message: "Email too long!" }),
password: zod
.string()
.min(8, { message: "Password too short!" })
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import { RecoilRoot } from "recoil";
import NonAuthenticatedRoute from "./components/NonAuthenticatedRoute";
import AuthenticatedRoute from "./components/AuthenticatedRoute";
import Profile from "./pages/Profile";
// @ts-expect-error
import OTP from "./pages/Otp.jsx";
import React from "react";
import Loader from "./components/Loader";
// import axios from "axios";
// axios.defaults.baseURL = "http://localhost:3001/";
import axios from "axios";
axios.defaults.baseURL = "http://localhost:3001/";

function App() {
return (
Expand All @@ -27,6 +29,7 @@ function App() {
<div className="min-h-[80vh]">
<Routes>
<Route path="/app" element={<Home />} />
<Route path="/app/otp" element={<OTP/>} />
<Route path="/app/posts/:id" element={<Post />} />
<Route path="/app/posts" element={<Posts />} />
<Route
Expand Down
Loading