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

Add email verification #751

Merged
merged 1 commit into from
Nov 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 72 additions & 20 deletions backend/controllers/authController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" <support@agrotechai.com>',
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 {
Expand Down Expand Up @@ -93,7 +145,7 @@ exports.forgotPasswordController = async (req, res) => {
<h2>${otp}</h2>
<p>If you did not request a password reset, please ignore this email.</p>
<p>Thank you,</p>
<p>The Rentalog Team</p>
<p>The AgroTech-AI Team</p>
`,
};

Expand Down
3 changes: 3 additions & 0 deletions backend/model/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion backend/routes/auth.js
Original file line number Diff line number Diff line change
@@ -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 );
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/MainContent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -123,6 +124,7 @@ const MainContent = () => {
<Route path="/login" element={<LoginPage />} />
<Route path="/profile" element={<Profile/>} />
<Route path="/signup" element={<SignUpPage />} />
<Route path="/verify-email" element={<AccountVerificationPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
<Route path="/terms" element={<TermsAndConditions />} />
<Route path="/cookie-policy" element={<CookiePolicy />} />
Expand Down
79 changes: 79 additions & 0 deletions frontend/src/components/EmailVerification.jsx
Original file line number Diff line number Diff line change
@@ -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 <Navigate to="/login" replace />;
}

return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-r from-purple-400 to-pink-500 mt-10">
<ToastContainer position="top-center" autoClose={5000} hideProgressBar newestOnTop />
<div className="w-full max-w-4xl grid grid-cols-1 md:grid-cols-2 bg-white shadow-lg rounded-lg overflow-hidden">
<div className="hidden md:block">
<img src={loginImage} alt="Account Verification Illustration" className="h-full w-full object-cover" />
</div>

<div className="p-10 flex flex-col justify-center">
<h2 className="text-4xl font-bold text-center text-purple-600 mb-4">
Verify Your Account
</h2>
<p className="text-center text-gray-600 mb-8">
Enter your email and OTP to verify your account.
</p>

<div>
<label htmlFor="email" className="block text-sm font-medium text-purple-600">Email</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => 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
/>
</div>

<div className="mt-6">
<label htmlFor="otp" className="block text-sm font-medium text-purple-600">OTP</label>
<input
type="text"
id="otp"
value={otp}
onChange={(e) => 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
/>
</div>

<button onClick={handleVerifyAccount} className="w-full mt-6 py-2 bg-gradient-to-r from-purple-500 to-pink-500 text-white rounded-md font-bold">
Verify Account
</button>

<p className="text-center text-sm mt-4">
<a href="/login" className="text-purple-500 hover:underline">Back to Login</a>
</p>
</div>
</div>
</div>
);
};

export default AccountVerificationPage;
2 changes: 1 addition & 1 deletion frontend/src/components/SignUpPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
Loading