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 Resend Email Verification and Delete Unverified Users after 24 Hours #436

Merged
merged 11 commits into from
Nov 9, 2024
2 changes: 1 addition & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ FRONT_END_URL=""
#email of tastytrail for sending verification email
EMAIL_USER =
EMAIL_PASS =
EMAIL_PROVIDER =
EMAIL_PROVIDER =
20 changes: 19 additions & 1 deletion backend/Controllers/UserController.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,23 @@ const deleteUserById = async (req, res) => {
});
}
};
const deleteUnverifiedUsers = async () => {
try {
const now = new Date();
const expirationTime = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 24 hours ago

const deletedUsers = await User.deleteMany({
isVerified: false,
createdAt: { $lt: expirationTime }
});

console.log(`Deleted ${deletedUsers.deletedCount} unverified accounts older than 24 hours`);
} catch (error) {
console.error('Error deleting unverified accounts:', error);
}
};




const UserController = {
Expand All @@ -403,7 +420,8 @@ const UserController = {
submitFeedback,
getAllFeedback,
getFeedbackByUserId,
deleteFeedbackById
deleteFeedbackById,
deleteUnverifiedUsers
};


Expand Down
75 changes: 54 additions & 21 deletions backend/Controllers/VerificationController.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,59 @@
import User from "../models/User.js";

import crypto from "crypto";
import { sendVerificationEmail } from "../Utils/emailUtils.js";
const verifyEmail = async (req, res) => {
const { token } = req.query;

try {
const user = await User.findOne({ verificationToken: token });
if (!user) {
return res.status(400).json({ message: 'Invalid or expired token' });
}

user.isVerified = true;
user.verificationToken = undefined; // Remove the token after verification
await user.save();

res.status(200).json({ message: 'Account successfully verified' });
} catch (error) {
res.status(500).json({ message: 'Error verifying email' });
const { token } = req.query;

try {
const user = await User.findOne({ verificationToken: token });
if (!user) {
return res.status(400).json({ message: "Invalid or expired token" });
}
};

const VerificationController = {
verifyEmail
};

export default VerificationController;
user.isVerified = true;
user.verificationToken = undefined; // Remove the token after verification
await user.save();

res.status(200).json({ message: "Account successfully verified" });
} catch (error) {
res.status(500).json({ message: "Error verifying email" });
}
};

const resendVerificationEmail = async (req, res) => {
const { email } = req.body;

try {
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: "Account is already verified" });
}

// Generate a new verification token and resend the email
const verificationToken = crypto.randomBytes(32).toString("hex");
user.verificationToken = verificationToken;

// Save the updated user and resend the email
await user.save();
await sendVerificationEmail(user.email, verificationToken, user.token, user._id);

res
.status(200)
.json({ message: "Verification email resent. Please check your inbox." });
} catch (error) {
res.status(500).json({ message: "Error resending verification email" });
}
};

const VerificationController = {
verifyEmail,
resendVerificationEmail,
};
export default VerificationController;


2 changes: 2 additions & 0 deletions backend/Utils/emailUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import nodemailer from 'nodemailer';

export const sendVerificationEmail = async (email, token, jwt, user_id) => {
const transporter = nodemailer.createTransport({

service: process.env.EMAIL_PROVIDER, // Use your email provider here

auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
Expand Down
4 changes: 3 additions & 1 deletion backend/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ const userSchema = new mongoose.Schema({
verificationToken: {
type: String
},
});

}, {timestamps : true});


const User = mongoose.model("User", userSchema);

Expand Down
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"axios": "^1.7.7",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"cron": "^3.1.9",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"helmet": "^8.0.0",
Expand Down
1 change: 1 addition & 0 deletions backend/routes/web.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ router.get("/recipe/getcomments/:recipeId", RecipeController.getComments);
// Signup ROUTES
router.post("/signup", UserController.Signup);
router.get("/verify-account", VerificationController.verifyEmail);
router.post("/reverify-account", VerificationController.resendVerificationEmail);

router.post("/login", UserController.Login);
router.post("/submitFeedback", UserController.submitFeedback);
Expand Down
39 changes: 28 additions & 11 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import dotenv from "dotenv";
import router from "./routes/web.js";
import mongoose from "mongoose";
import { rateLimit } from "./middleware/rateLimit.js";
import UserController from "./Controllers/UserController.js";
import { CronJob} from "cron";
import client from "prom-client";
dotenv.config();

Expand All @@ -21,7 +23,7 @@ app.use(helmet());
const allowedOrigins = [
"https://delightful-daifuku-a9f6ea.netlify.app",
/https:\/\/deploy-preview-\d+--delightful-daifuku-a9f6ea\.netlify\.app/,

];


Expand Down Expand Up @@ -88,17 +90,32 @@ app.get("/metrics", async (_, res) => {
// Database Connection and server
try {
await mongoose
.connect(process.env.DATABASE, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log("Successfully Connected To MongoDB Server!");

app.listen(port, () => {
console.log(`The server is running at ${process.env.PORT}`);
});
.connect(process.env.DATABASE, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log("Successfully Connected To MongoDB Server!");

app.listen(port, () => {
console.log(`The server is running at ${process.env.PORT}`);
});
});
} catch (error) {
console.log(error);
}



const job = new CronJob('0 0 * * *', async () => { // Changed for testing every minute
console.log('User Cleanup Job triggered');
try {
await UserController.deleteUnverifiedUsers();
} catch (error) {
console.error('Error running scheduled task:', error);
}
});
console.log("Scheduler started: Unverified users cleanup task will run every day at midnight.")

job.start()

3 changes: 3 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import ResetPassword from "./Pages/ResetPassword.jsx";
import ForgotPassword from "./Pages/ForgotPassword.jsx";
import RecipeSuggestions from "./Pages/RecipeSuggestions.jsx";
import EmailVerification from "./Pages/EmailVerification.jsx"
import ResendVerificationPage from "./Pages/ResendVerification.jsx";

import UserProfile from "./Pages/Profile.jsx";

function App() {
Expand Down Expand Up @@ -66,6 +68,7 @@ function App() {
<Route path="/verify-email" element={<EmailVerification/>}></Route>
<Route path="/forgot_password" element={<ForgotPassword />} />
<Route path="/reset_password/:token" element={<ResetPassword />} />
<Route path="/reverify-email" element={<ResendVerificationPage />} />
<Route
path="/recipes"
element={<Recipes key={"recipes"} type="" />}
Expand Down
1 change: 0 additions & 1 deletion frontend/src/Pages/EmailVerification.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ const AccountVerificationPage = () => {
const userData = await axios.post(`${backendURL}/api/user/fetch`, {
id: userId,
});
console.log(userData);
setTimeout(() => {
setIsVerified(true);
navigator(`/user/${userData.data._id}`, {
Expand Down
123 changes: 123 additions & 0 deletions frontend/src/Pages/ResendVerification.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { useState, useEffect } from "react";
import axios from "axios";
import { useLocation } from "react-router-dom";
import { toast, ToastContainer } from "react-toastify";

const ResendVerificationPage = () => {
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState(null);
const [timeRemaining, setTimeRemaining] = useState(120); // Initial countdown timer
const [canResend, setCanResend] = useState(false);
const location = useLocation();
const backendURL = import.meta.env.VITE_BACKEND_URL;

// Extract the email from URL query parameters
const queryParams = new URLSearchParams(location.search);
const email = queryParams.get("email");

useEffect(() => {
if (!email) {
setError("Invalid request. No email provided.");
}

// Start the countdown timer when the component mounts
const timer = setInterval(() => {
setTimeRemaining((prevTime) => {
if (prevTime <= 1) {
clearInterval(timer);
setCanResend(true); // Enable resend button when timer reaches zero
return 0;
}
return prevTime - 1;
});
}, 1000);

return () => clearInterval(timer); // Cleanup interval when component unmounts
}, [email]);

const resendVerificationEmail = async () => {
setLoading(true);
setError(null);
setSuccess(false); // Reset success state before starting the resend

try {
const response = await axios.post(`${backendURL}/api/reverify-account`, {
email: email,
});

if (response.status === 200) {
setSuccess(true);
setCanResend(false); // Disable resend button again
setTimeRemaining(120); // Reset timer after successful resend

// Trigger the toast only once here
toast.success("Verification email sent successfully.");

// Restart the countdown timer after resetting timeRemaining
const timer = setInterval(() => {
setTimeRemaining((prevTime) => {
if (prevTime <= 1) {
clearInterval(timer);
setCanResend(true); // Enable resend button when timer reaches zero
return 0;
}
return prevTime - 1;
});
}, 1000);

return () => clearInterval(timer); // Cleanup the interval when the component unmounts or timer resets
} else {
setError("Failed to resend verification email. Please try again.");
}
} catch (error) {
setError(error.response?.data?.message || "Failed to resend verification email.");
} finally {
setLoading(false);
}
};

const formattedTime = `${Math.floor(timeRemaining / 60)}:${String(timeRemaining % 60).padStart(2, "0")}`;

return (
<div className="min-h-screen flex items-center justify-center bg-white text-red-700 mt-10">
<ToastContainer position="top-center" autoClose={5000} hideProgressBar newestOnTop />
<div className="w-full max-w-lg bg-white shadow-xl rounded-xl p-10">
<h2 className="text-3xl font-semibold text-center text-red-700 mb-6">Verify Your Email</h2>

<p className="text-center text-gray-600 mb-8">
A verification email has been sent to <strong>{email}</strong>. Please check your inbox.
</p>

{error && <p className="text-center text-red-700 mb-4">{error}</p>}
{success && (
<p className="text-center text-green-600 mb-4">
Verification email sent! Please check your inbox.
</p>
)}

<button
className={`w-full py-3 mt-6 rounded-md text-white font-semibold ${
canResend ? "bg-red-700 hover:bg-red-800" : "bg-gray-300 cursor-not-allowed"
}`}
onClick={resendVerificationEmail}
disabled={!canResend || loading}
>
{loading ? "Sending..." : "Resend Verification Email"}
</button>

{!canResend && (
<p className="text-center text-gray-600 mt-4">
Please wait {formattedTime} before requesting again.
</p>
)}

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

export default ResendVerificationPage;
6 changes: 4 additions & 2 deletions frontend/src/Pages/Signup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ const Signup = () => {
e.preventDefault();
setSubmitting(true)
let submitable = true;
console.log(form)

Object.values(error).forEach((val) => {
if (val) {
submitable = false;
Expand All @@ -105,7 +105,9 @@ const Signup = () => {
.then((res) => {
if (res.data.success) {
setSubmitting(false)
toast.success("Please check your Email to verify your account");

navigator(`/reverify-email?email=${form.email}`)


}
})
Expand Down