From 4de8bf00c7d70e3c79707ccb7e073126082875df Mon Sep 17 00:00:00 2001 From: haseebzaki-07 Date: Sun, 3 Nov 2024 16:33:02 +0530 Subject: [PATCH] add email verification --- backend/controllers/authController.js | 92 +++++++++++++++---- backend/model/user.js | 3 + backend/routes/auth.js | 3 +- frontend/src/MainContent.jsx | 2 + frontend/src/components/EmailVerification.jsx | 79 ++++++++++++++++ frontend/src/components/SignUpPage.jsx | 2 +- 6 files changed, 159 insertions(+), 22 deletions(-) create mode 100644 frontend/src/components/EmailVerification.jsx diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index 8bbcec45..a55fab34 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -2,29 +2,81 @@ const User = require("../model/user"); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const nodemailer = require('nodemailer'); +const crypto = require('crypto'); exports.signupController = async (req, res) => { - try { - const { firstName, lastName, email, password, role } = req.body; - - const existingUser = await User.findOne({ email }); - if (existingUser) { - return res.status(400).json({ message: 'Email already exists' }); - } - - const newUser = new User({ firstName, lastName, email, password, role }); - await newUser.save(); - - // Include role in the token - const token = jwt.sign({ userId: newUser._id, role: newUser.role }, process.env.JWT_SECRET, { expiresIn: '1h' }); - - res.status(201).json({ message: 'User created', token }); - } catch (error) { - console.error("Signup error:", error); - res.status(500).json({ message: 'Signup failed' }); + try { + const { firstName, lastName, email, password, role } = req.body; + + const existingUser = await User.findOne({ email }); + if (existingUser) { + return res.status(400).json({ message: 'Email already exists' }); } -} + + const newUser = new User({ firstName, lastName, email, password, role }); + + // Generate OTP + const otp = crypto.randomInt(100000, 999999).toString(); + newUser.otp = otp; + newUser.otpExpires = Date.now() + 10 * 60 * 1000; // OTP expires in 10 minutes + + await newUser.save(); + + // Send OTP via email + const transporter = nodemailer.createTransport({ + service: 'Gmail', // Use a secure service + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, + }); + + const mailOptions = { + from: '"Agro-tech AI Support" ', + to: newUser.email, + subject: 'Verify your account - OTP', + text: `Hello ${firstName},\n\nYour OTP for account verification is: ${otp}\n\nThis OTP is valid for 10 minutes.`, + }; + + await transporter.sendMail(mailOptions); + + res.status(201).json({ message: 'User created. Please verify your email with the OTP sent.' }); + } catch (error) { + console.error("Signup error:", error); + res.status(500).json({ message: 'Signup failed' }); + } +}; + +exports.signupVerifyOtpController = async (req, res) => { + try { + const { email, otp } = req.body; + + const user = await User.findOne({ email }); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + if (user.isVerified) { + return res.status(400).json({ message: 'User is already verified' }); + } + + if (user.otp !== otp || user.otpExpires < Date.now()) { + return res.status(400).json({ message: 'Invalid or expired OTP' }); + } + + // Update user to verified + user.isVerified = true; + user.otp = undefined; // Clear OTP fields + user.otpExpires = undefined; + await user.save(); + + res.status(200).json({ message: 'Account verified successfully' }); + } catch (error) { + console.error("OTP Verification error:", error); + res.status(500).json({ message: 'OTP verification failed' }); + } +}; exports.signinController = async (req, res) => { try { @@ -93,7 +145,7 @@ exports.forgotPasswordController = async (req, res) => {

${otp}

If you did not request a password reset, please ignore this email.

Thank you,

-

The Rentalog Team

+

The AgroTech-AI Team

`, }; diff --git a/backend/model/user.js b/backend/model/user.js index c00c895c..d22fc53e 100644 --- a/backend/model/user.js +++ b/backend/model/user.js @@ -13,6 +13,9 @@ const userSchema = new mongoose.Schema({ }, resetPasswordOTP: { type: String }, resetPasswordExpires: { type: Date }, + isVerified: { type: Boolean, default: false }, + otp: { type: String }, + otpExpires: { type: Date }, }); // Hash password before saving diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 649b14ec..1982b263 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,10 +1,11 @@ const express = require('express'); const router = express.Router(); -const { signupController, signinController, forgotPasswordController, verifyOtpController, resetPasswordController } = require('../controllers/authController'); +const { signupController, signinController, forgotPasswordController, verifyOtpController, resetPasswordController, signupVerifyOtpController } = require('../controllers/authController'); // Signup Route router.post('/signup', signupController); +router.post('/verify-emailotp', signupVerifyOtpController); // Login Route router.post('/signin', signinController ); diff --git a/frontend/src/MainContent.jsx b/frontend/src/MainContent.jsx index d48e4c00..8bae6709 100644 --- a/frontend/src/MainContent.jsx +++ b/frontend/src/MainContent.jsx @@ -58,6 +58,7 @@ import Wishlist from './AgroShopAI/pages/Wishlist'; import ShopNavbar from './AgroShopAI/components/ShopNavbar'; import ShopProfile from './AgroShopAI/pages/Profile' import ForgotPasswordPage from './components/ForgotPassword'; +import AccountVerificationPage from './components/EmailVerification'; const MainContent = () => { UseScrollToTop(); const location = useLocation(); // Get the current route @@ -123,6 +124,7 @@ const MainContent = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/EmailVerification.jsx b/frontend/src/components/EmailVerification.jsx new file mode 100644 index 00000000..fe7cc3fa --- /dev/null +++ b/frontend/src/components/EmailVerification.jsx @@ -0,0 +1,79 @@ +import { useState } from "react"; +import axios from "axios"; +import { Navigate } from "react-router-dom"; +import { toast, ToastContainer } from "react-toastify"; +import loginImage from "../assets/LoginImage.png"; + +const AccountVerificationPage = () => { + const [email, setEmail] = useState(""); + const [otp, setOtp] = useState(""); + const [isVerified, setIsVerified] = useState(false); + + const handleVerifyAccount = async () => { + try { + await axios.post("https://agro-tech-ai-backend-teal.vercel.app/auth/verify-emailotp", { email, otp }); + toast.success("Account verified successfully. Redirecting to login..."); + setTimeout(() => setIsVerified(true), 2000); // Redirect after a short delay + } catch (error) { + toast.error(error.response?.data?.message || "Verification failed"); + } + }; + + if (isVerified) { + return ; + } + + return ( +
+ +
+
+ Account Verification Illustration +
+ +
+

+ Verify Your Account +

+

+ Enter your email and OTP to verify your account. +

+ +
+ + setEmail(e.target.value)} + className="w-full px-4 py-2 mt-1 rounded-md bg-purple-100 text-purple-800 focus:ring focus:ring-purple-400" + required + /> +
+ +
+ + setOtp(e.target.value)} + className="w-full px-4 py-2 mt-1 rounded-md bg-purple-100 text-purple-800 focus:ring focus:ring-purple-400" + required + /> +
+ + + +

+ Back to Login +

+
+
+
+ ); +}; + +export default AccountVerificationPage; diff --git a/frontend/src/components/SignUpPage.jsx b/frontend/src/components/SignUpPage.jsx index 85205f34..a81c0971 100644 --- a/frontend/src/components/SignUpPage.jsx +++ b/frontend/src/components/SignUpPage.jsx @@ -62,7 +62,7 @@ const SignUpPage = () => { } ); toast.success(response.data.message); - navigate("/login"); + navigate("/verify-email"); } catch (error) { toast.error( error.response?.data?.message || "Signup failed. Please try again."