From 3de7bdaf0da4011eb3cebd736c08fb3e8f304df2 Mon Sep 17 00:00:00 2001 From: Aditya Yadav <217367477+adityayadav176@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:08:55 +0530 Subject: [PATCH 1/5] Add Backend Controller And RoutesFor Email Verification --- backend/.env.example | 2 + backend/package-lock.json | 10 +++ backend/package.json | 1 + backend/src/controllers/auth.controller.js | 78 ++++++++++++++++++++++ backend/src/models/user.model.js | 6 ++ backend/src/routes/auth.route.js | 4 +- backend/src/utils/nodemailer.js | 11 +++ 7 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 backend/src/utils/nodemailer.js diff --git a/backend/.env.example b/backend/.env.example index 7b62781..f9e24b6 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -9,3 +9,5 @@ GOOGLE_CLIENT_ID=your_google_client_id VAPID_PUBLIC_KEY=your_vapid_public_key VAPID_PRIVATE_KEY=your_vapid_private_key VAPID_SUBJECT=mailto:your@email.com +EMAIL=your_email@gmail.com +EMAIL_PASSWORD=your_google_email_password \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 7859583..ffdf3c6 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -20,6 +20,7 @@ "helmet": "^8.1.0", "jsonwebtoken": "^9.0.3", "mongoose": "^9.6.1", + "nodemailer": "^8.0.11", "socket.io": "^4.8.3", "web-push": "^3.6.7" }, @@ -1457,6 +1458,15 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/nodemailer": { + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.11.tgz", + "integrity": "sha512-nrO/pDAUKl+wXX+lx16tDLbnm0fW6sK/x8mgohaCpg+CdCEl482bD4tCuAZk2DyliruiNTIZxRCoWkDqJEnAiA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.14", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", diff --git a/backend/package.json b/backend/package.json index a71d36e..c60b3b8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,6 +23,7 @@ "helmet": "^8.1.0", "jsonwebtoken": "^9.0.3", "mongoose": "^9.6.1", + "nodemailer": "^8.0.11", "socket.io": "^4.8.3", "web-push": "^3.6.7" }, diff --git a/backend/src/controllers/auth.controller.js b/backend/src/controllers/auth.controller.js index 2688b5b..c2fd1ef 100644 --- a/backend/src/controllers/auth.controller.js +++ b/backend/src/controllers/auth.controller.js @@ -3,6 +3,7 @@ import { OAuth2Client } from "google-auth-library"; import User from "../models/user.model.js"; import { generateTokenAndSetCookie } from "../lib/utils.js"; import cloudinary from "../lib/cloudinary.js"; +import transporter from "../utils/nodemailer.js" let googleClient; @@ -279,4 +280,81 @@ export async function subscribeToPush(req, res) { console.error("subscribeToPush:", err.message); res.status(500).json({ message: "Could not save push subscription" }); } +} + +export const emailVerification = async(req, res) => { + try { + const {email} = req.body + + if(!email) { + return res.status(400).json({message: "Email Required"}); + } + const user = await User.findOne({email}); + + if (!user) { + return res.status(404).json({ + success: false, + message: "No account found with this email" + }); +} + + const otp = Math.floor(100000 + Math.random() * 900000).toString(); + + user.otp = otp; + user.otpExpires = Date.now() + 5 * 60 * 1000; + await user.save(); + + await transporter.sendMail({ + from: process.env.EMAIL, + to: email, + subject: "Email Verification OTP", + html: `

Email Verification

+

Your Otp Is :${otp}

+

This Otp Is Valid For 5 Minutes. Use This Otp TO Verify Your Account

` + }); + + res.status(200).json({message: "Otp Send Successfully"}); + } catch (error) { + console.error("EmailVerification Error :", error.message); + res.status(500).json({message: "Failed To Verified Email"}); + } +} + +export const verifyOtp = async(req, res) => { + try { + const {email, otp} = req.body; + + if(!email) { + return res.status(400).json({message: "Email Required"}); + } + + if(!otp) { + return res.status(400).json({message: "Otp Required"}); + } + + const user = await User.findOne({email}); + + if(!user) { + return res.status(404).json({message: "User Not Found"}); + } + + if(user.otp !== otp) { + return res.status(400).json({message: "Invalid Otp"}); + } + + if(user.otpExpires < Date.now()) { + return res.status(400).json({message: "Otp Expired"}); + } + + user.isVerified = true; + user.otp = null; + user.otpExpires = null; + + await user.save(); + + res.status(200).json({message: "Email Verified"}); + } catch (error) { + res.status(500).json({message: "Error While Verified Email Account", error}); + console.log(error); + } } \ No newline at end of file diff --git a/backend/src/models/user.model.js b/backend/src/models/user.model.js index 46935a6..11b7a7c 100644 --- a/backend/src/models/user.model.js +++ b/backend/src/models/user.model.js @@ -33,10 +33,16 @@ const userSchema = new mongoose.Schema({ type: mongoose.Schema.Types.Mixed, default: null, }, + isVerified: { + type: Boolean, + default: false + }, lastSeen: { type: Date, default: Date.now, }, + otp: String, + otpExpires: Date }, { timestamps: true }); userSchema.index({ name: "text" }); diff --git a/backend/src/routes/auth.route.js b/backend/src/routes/auth.route.js index 3db8be9..8d3e65f 100644 --- a/backend/src/routes/auth.route.js +++ b/backend/src/routes/auth.route.js @@ -1,7 +1,7 @@ import express from "express"; import protectRoute from "../middleware/auth.middleware.js"; import { validateSignup, validateLogin } from "../middleware/validate.js"; -import { signup, login, logout, googleAuth, updateProfile, updateProfilePicture, checkAuth, subscribeToPush } from "../controllers/auth.controller.js"; +import { signup, login, logout, googleAuth, updateProfile, updateProfilePicture, checkAuth, subscribeToPush, emailVerification, verifyOtp } from "../controllers/auth.controller.js"; const router = express.Router(); @@ -13,5 +13,7 @@ router.put("/update-profile", protectRoute, updateProfile); router.put("/update-profile-picture", protectRoute, updateProfilePicture); router.get("/check", protectRoute, checkAuth); router.post("/push-subscribe", protectRoute, subscribeToPush); +router.post("/send-verification-email-otp", emailVerification); +router.post("/Verified-account", verifyOtp); export default router; \ No newline at end of file diff --git a/backend/src/utils/nodemailer.js b/backend/src/utils/nodemailer.js new file mode 100644 index 0000000..9977241 --- /dev/null +++ b/backend/src/utils/nodemailer.js @@ -0,0 +1,11 @@ +import nodemailer from 'nodemailer' + +const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: process.env.EMAIL, + pass: process.env.EMAIL_PASSWORD + } +}); + +export default transporter \ No newline at end of file From 2d9f3c12042df7779020be138ca9ae455a9efd68 Mon Sep 17 00:00:00 2001 From: Aditya Yadav <217367477+adityayadav176@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:15:09 +0530 Subject: [PATCH 2/5] Add SendVerifyEmailOtp And VErifyEmail Page In Frontend --- frontend/pages/SendVerifyEmailOtp.jsx | 70 +++++++++++++++ frontend/pages/VerifyEmail.jsx | 117 ++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 frontend/pages/SendVerifyEmailOtp.jsx create mode 100644 frontend/pages/VerifyEmail.jsx diff --git a/frontend/pages/SendVerifyEmailOtp.jsx b/frontend/pages/SendVerifyEmailOtp.jsx new file mode 100644 index 0000000..fb6b3c3 --- /dev/null +++ b/frontend/pages/SendVerifyEmailOtp.jsx @@ -0,0 +1,70 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import axios from "axios"; +import React from 'react' +import toast from "react-hot-toast" + +function SendVerifyEmailOtp() { + const [email, setEmail] = useState(""); + const [loading, setLoading] = useState(false); + + const navigate = useNavigate(); + + const handleSendOtp = async (e) => { + e.preventDefault(); + + try { + setLoading(true); + + const res = await axios.post( + "http://localhost:5001/api/auth/send-verification-email-otp", + { email } + ); + + toast.success(res.data.message); + + localStorage.setItem("verifyEmail", email); + + navigate("/verify-email"); + } catch (error) { + toast.error( + error.response?.data?.message || + "Failed to send OTP" + ); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ +

+ Verify Your Email +

+ +
+ setEmail(e.target.value)} + required + /> + + +
+
+
+ ); +} + +export default SendVerifyEmailOtp diff --git a/frontend/pages/VerifyEmail.jsx b/frontend/pages/VerifyEmail.jsx new file mode 100644 index 0000000..1ec80cf --- /dev/null +++ b/frontend/pages/VerifyEmail.jsx @@ -0,0 +1,117 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import axios from "axios"; +import toast from "react-hot-toast" + +const VerifyEmail = () => { + const navigate = useNavigate(); + + const [otp, setOtp] = useState(""); + const [loading, setLoading] = useState(false); + + const email = localStorage.getItem("verifyEmail"); + + const handleVerifyOtp = async (e) => { + e.preventDefault(); + + if (otp.length !== 6) { + return toast.error("Please enter a valid 6-digit OTP"); + } + + try { + setLoading(true); + + const res = await axios.post( + "http://localhost:5001/api/auth/Verified-account", + { + email, + otp, + } + ); + + toast.success(res.data.message); + + localStorage.removeItem("verifyEmail"); + + navigate("/login"); + } catch (error) { + toast.error( + error.response?.data?.message || + "OTP verification failed" + ); + } finally { + setLoading(false); + } + }; + + const handleResendOtp = async () => { + try { + const res = await axios.post( + "http://localhost:5001/api/auth/send-verification-email-otp", + { + email, + } + ); + + toast.success(res.data.message); + } catch (error) { + toast.error( + error.response?.data?.message || + "Failed to resend OTP" + ) + } + }; + + return ( +
+
+ +

+ Verify Email +

+ +

+ Enter the OTP sent to +

+ +

+ {email} +

+ +
+ + setOtp( + e.target.value + .replace(/\D/g, "") + .slice(0, 6) + ) + } + placeholder="Enter 6-digit OTP" + className="input input-bordered w-full text-center text-xl tracking-[10px]" + maxLength={6} + /> + + +
+ + +
+
+ ); +}; + +export default VerifyEmail; \ No newline at end of file From 363fcb8b667fab4e88605ed05e1e6cb27489d708 Mon Sep 17 00:00:00 2001 From: Aditya Yadav <217367477+adityayadav176@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:35:42 +0530 Subject: [PATCH 3/5] Refactor Whole Code And Change Controller Login With Autenticate User --- backend/src/controllers/auth.controller.js | 144 +++++++++++---------- frontend/pages/SendVerifyEmailOtp.jsx | 99 ++++++-------- frontend/pages/VerifyEmail.jsx | 29 ++--- 3 files changed, 123 insertions(+), 149 deletions(-) diff --git a/backend/src/controllers/auth.controller.js b/backend/src/controllers/auth.controller.js index c2fd1ef..38cbab6 100644 --- a/backend/src/controllers/auth.controller.js +++ b/backend/src/controllers/auth.controller.js @@ -282,79 +282,87 @@ export async function subscribeToPush(req, res) { } } -export const emailVerification = async(req, res) => { - try { - const {email} = req.body - - if(!email) { - return res.status(400).json({message: "Email Required"}); - } - const user = await User.findOne({email}); +export const emailVerification = async (req, res) => { + try { + const user = await User.findById(req.user._id); + + if (!user) { + return res.status(404).json({ + message: "User not found", + }); + } + + if (user.isVerified) { + return res.status(400).json({ + message: "Email already verified", + }); + } - if (!user) { - return res.status(404).json({ - success: false, - message: "No account found with this email" + const otp = Math.floor( + 100000 + Math.random() * 900000 + ).toString(); + + user.otp = otp; + user.otpExpires = Date.now() + 5 * 60 * 1000; + + await user.save(); + + await transporter.sendMail({ + from: process.env.EMAIL, + to: user.email, + subject: "Email Verification OTP", + html: `

Email Verification

+

Your OTP is: ${otp}

`, + }); + + res.status(200).json({ + message: "OTP sent successfully", }); -} - const otp = Math.floor(100000 + Math.random() * 900000).toString(); + } catch (error) { + res.status(500).json({ + message: "Failed to send OTP", + }); + } +}; - user.otp = otp; - user.otpExpires = Date.now() + 5 * 60 * 1000; - await user.save(); +export const verifyOtp = async (req, res) => { + try { + const { otp } = req.body; - await transporter.sendMail({ - from: process.env.EMAIL, - to: email, - subject: "Email Verification OTP", - html: `

Email Verification

-

Your Otp Is :${otp}

-

This Otp Is Valid For 5 Minutes. Use This Otp TO Verify Your Account

` - }); + const user = await User.findById(req.user._id); - res.status(200).json({message: "Otp Send Successfully"}); - } catch (error) { - console.error("EmailVerification Error :", error.message); - res.status(500).json({message: "Failed To Verified Email"}); + if (!user) { + return res.status(404).json({ + message: "User not found", + }); } -} -export const verifyOtp = async(req, res) => { - try { - const {email, otp} = req.body; - - if(!email) { - return res.status(400).json({message: "Email Required"}); - } - - if(!otp) { - return res.status(400).json({message: "Otp Required"}); - } - - const user = await User.findOne({email}); - - if(!user) { - return res.status(404).json({message: "User Not Found"}); - } - - if(user.otp !== otp) { - return res.status(400).json({message: "Invalid Otp"}); - } - - if(user.otpExpires < Date.now()) { - return res.status(400).json({message: "Otp Expired"}); - } - - user.isVerified = true; - user.otp = null; - user.otpExpires = null; - - await user.save(); - - res.status(200).json({message: "Email Verified"}); - } catch (error) { - res.status(500).json({message: "Error While Verified Email Account", error}); - console.log(error); - } -} \ No newline at end of file + if (user.otp !== otp) { + return res.status(400).json({ + message: "Invalid OTP", + }); + } + + if (user.otpExpires < Date.now()) { + return res.status(400).json({ + message: "OTP expired", + }); + } + + user.isVerified = true; + user.otp = null; + user.otpExpires = null; + + await user.save(); + + res.status(200).json({ + message: "Email verified successfully", + }); + + } catch (error) { + res.status(500).json({ + message: "Verification failed", + }); + } +}; \ No newline at end of file diff --git a/frontend/pages/SendVerifyEmailOtp.jsx b/frontend/pages/SendVerifyEmailOtp.jsx index fb6b3c3..3a16d86 100644 --- a/frontend/pages/SendVerifyEmailOtp.jsx +++ b/frontend/pages/SendVerifyEmailOtp.jsx @@ -1,70 +1,47 @@ import { useState } from "react"; -import { useNavigate } from "react-router-dom"; import axios from "axios"; -import React from 'react' -import toast from "react-hot-toast" +import toast from "react-hot-toast"; +import { useNavigate } from "react-router-dom"; function SendVerifyEmailOtp() { - const [email, setEmail] = useState(""); - const [loading, setLoading] = useState(false); - - const navigate = useNavigate(); - - const handleSendOtp = async (e) => { - e.preventDefault(); - - try { - setLoading(true); - - const res = await axios.post( - "http://localhost:5001/api/auth/send-verification-email-otp", - { email } - ); - - toast.success(res.data.message); - - localStorage.setItem("verifyEmail", email); - - navigate("/verify-email"); - } catch (error) { - toast.error( - error.response?.data?.message || - "Failed to send OTP" - ); - } finally { - setLoading(false); - } - }; - - return ( -
-
- -

- Verify Your Email -

+const [loading, setLoading] = useState(false); +const navigate = useNavigate(); + +const handleSendOtp = async () => { +try { +setLoading(true); + const res = await axios.post( + "http://localhost:5001/api/auth/send-verification-email-otp", + {}, + { withCredentials: true } + ); -
- setEmail(e.target.value)} - required - /> + toast.success(res.data.message); - -
-
-
+ navigate("/verify-email"); +} catch (error) { + toast.error( + error.response?.data?.message || + "Failed to send OTP" ); +} finally { + setLoading(false); +} + +}; + +return (

+Verify Your Email

+ +
+
+); } -export default SendVerifyEmailOtp +export default SendVerifyEmailOtp; diff --git a/frontend/pages/VerifyEmail.jsx b/frontend/pages/VerifyEmail.jsx index 1ec80cf..98201aa 100644 --- a/frontend/pages/VerifyEmail.jsx +++ b/frontend/pages/VerifyEmail.jsx @@ -9,8 +9,6 @@ const VerifyEmail = () => { const [otp, setOtp] = useState(""); const [loading, setLoading] = useState(false); - const email = localStorage.getItem("verifyEmail"); - const handleVerifyOtp = async (e) => { e.preventDefault(); @@ -22,18 +20,14 @@ const VerifyEmail = () => { setLoading(true); const res = await axios.post( - "http://localhost:5001/api/auth/Verified-account", - { - email, - otp, - } - ); + "http://localhost:5001/api/auth/Verified-account", + { otp }, + { withCredentials: true } +); toast.success(res.data.message); - localStorage.removeItem("verifyEmail"); - - navigate("/login"); + navigate("/"); } catch (error) { toast.error( error.response?.data?.message || @@ -47,11 +41,10 @@ const VerifyEmail = () => { const handleResendOtp = async () => { try { const res = await axios.post( - "http://localhost:5001/api/auth/send-verification-email-otp", - { - email, - } - ); + "http://localhost:5001/api/auth/send-verification-email-otp", + {}, + { withCredentials: true } +); toast.success(res.data.message); } catch (error) { @@ -74,10 +67,6 @@ const VerifyEmail = () => { Enter the OTP sent to

-

- {email} -

-
Date: Thu, 11 Jun 2026 12:37:53 +0530 Subject: [PATCH 4/5] Change Routes To Protected Routes --- backend/src/routes/auth.route.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/routes/auth.route.js b/backend/src/routes/auth.route.js index 8d3e65f..2dc89a9 100644 --- a/backend/src/routes/auth.route.js +++ b/backend/src/routes/auth.route.js @@ -13,7 +13,7 @@ router.put("/update-profile", protectRoute, updateProfile); router.put("/update-profile-picture", protectRoute, updateProfilePicture); router.get("/check", protectRoute, checkAuth); router.post("/push-subscribe", protectRoute, subscribeToPush); -router.post("/send-verification-email-otp", emailVerification); -router.post("/Verified-account", verifyOtp); +router.post("/send-verification-email-otp",protectRoute, emailVerification); +router.post("/Verified-account",protectRoute, verifyOtp); export default router; \ No newline at end of file From c28a760fd7e5049b03dfe1627d712b83470c7031 Mon Sep 17 00:00:00 2001 From: Aditya Yadav <217367477+adityayadav176@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:33:36 +0530 Subject: [PATCH 5/5] Completed Verified Email Functionality --- backend/src/controllers/auth.controller.js | 5 +++-- frontend/pages/ProfilePage.jsx | 11 +++++++++++ frontend/src/App.jsx | 4 ++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/backend/src/controllers/auth.controller.js b/backend/src/controllers/auth.controller.js index 38cbab6..6949273 100644 --- a/backend/src/controllers/auth.controller.js +++ b/backend/src/controllers/auth.controller.js @@ -284,7 +284,7 @@ export async function subscribeToPush(req, res) { export const emailVerification = async (req, res) => { try { - const user = await User.findById(req.user._id); + const user = await User.findById(req.userId); if (!user) { return res.status(404).json({ @@ -320,6 +320,7 @@ export const emailVerification = async (req, res) => { }); } catch (error) { + console.log(error); res.status(500).json({ message: "Failed to send OTP", }); @@ -330,7 +331,7 @@ export const verifyOtp = async (req, res) => { try { const { otp } = req.body; - const user = await User.findById(req.user._id); + const user = await User.findById(req.userId); if (!user) { return res.status(404).json({ diff --git a/frontend/pages/ProfilePage.jsx b/frontend/pages/ProfilePage.jsx index 75157f2..fb1d707 100644 --- a/frontend/pages/ProfilePage.jsx +++ b/frontend/pages/ProfilePage.jsx @@ -2,6 +2,7 @@ import { useState } from "react" import { Camera, Pencil } from "lucide-react" import toast from "react-hot-toast" import useAuthStore from "../src/store/useAuthStore" +import { useNavigate } from "react-router-dom"; export default function ProfilePage() { const { authUser: user, updateProfile, updateProfilePicture, isLoading } = useAuthStore() @@ -11,6 +12,8 @@ export default function ProfilePage() { const [previewImage, setPreviewImage] = useState(user?.profilePicture || null) const [selectedFile, setSelectedFile] = useState(null) const [isEditing, setIsEditing] = useState(false) + const [isEmailOtp, SetIsEmailOtp] = useState(false); + const navigate = useNavigate(); const handleFileChange = (e) => { const file = e.target.files[0] @@ -105,6 +108,14 @@ export default function ProfilePage() { Edit ) : null} + {!user?.isVerified && ( + +)} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 669ab27..a8811e3 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -12,6 +12,8 @@ import SavedMessagesPage from "../pages/SavedMessagesPage" import CallHandler from "../components/CallHandler" import useAuthStore from "./store/useAuthStore" import useCallStore from "./store/useCallStore" +import VerifyEmail from "../pages/VerifyEmail" +import SendVerifyEmailOtp from "../pages/SendVerifyEmailOtp"; const App = () => { const { authUser, checkAuth, isCheckingAuth } = useAuthStore() @@ -49,6 +51,8 @@ const App = () => { : } /> : } /> : } /> + : } /> + : } />