Skip to content

Commit

Permalink
Merge pull request #282 from Sourabh782/forgotpassword
Browse files Browse the repository at this point in the history
Feat: Implemented otp based forgot password #263
  • Loading branch information
RamakrushnaBiswal authored Oct 15, 2024
2 parents 1106026 + f4b682f commit b6e8228
Show file tree
Hide file tree
Showing 10 changed files with 414 additions and 55 deletions.
42 changes: 42 additions & 0 deletions backend/config/nodemailer.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,45 @@ exports.sendReservationConfirmation = async (email, reservationDetails) => {
}
}
};

exports.sendVerificationMail = async (email, verificationCode) => {
const emailText = `
Dear Customer,
Please use this verification code for resetting your password. Here's your code':
RCode: ${verificationCode}
Thank you for choosing our service. We are happy to help you.
Best regards,
PlayCafe
`;

try {
await transporter.sendMail({
from: process.env.EMAIL_USER,
to: email,
subject: "Password Reset Verification Code",
text: emailText,
});
logger.info("Verification code sent successfully via email", {
email,
});
} catch (error) {
logger.error("Failed to send verification code email", {
error,
email,
});
if (error.code === "ECONNREFUSED") {
throw new Error(
"Failed to connect to email server. Please try again later.",
);
} else {
throw new Error(
`Failed to send verification email: ${error.message}`,
);
}
}

}
86 changes: 86 additions & 0 deletions backend/controller/forgotPassword.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/* eslint-disable no-unused-vars */
const bcrypt = require("bcrypt");
const { z } = require("zod");
const Customer = require("../models/customer.model");
const { sendVerificationMail } = require("../config/nodemailer");


// Define the schema
const forgotPasswordSchema = z.object({
email: z.string().email("Invalid email address"),
});

async function verifyEmail(req, res) {
// Validate the request body
const validation = forgotPasswordSchema.safeParse(req.body);

if (!validation.success) {
return res.status(400).json({ error: validation.error.errors });
}

const existingCustomer = await Customer.findOne({ email: req.body.email });
if (!existingCustomer) {
return res.status(404).json({ error: "Email is not registered" });
}

const verifyCode = Math.floor(100000 + Math.random() * 900000).toString();

existingCustomer.verificationCode = verifyCode;
await existingCustomer.save();

sendVerificationMail(req.body.email, verifyCode)

try {
res.status(201).json({ id: existingCustomer._id, success: true });
} catch (error) {
res.status(500).json({ error: "Internal server error" });
}
}

async function verifyOTP(req, res) {
try {
const { id, otp } = req.body;

const existingCustomer = await Customer.findOne({ _id: id });

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

if(existingCustomer.verificationCode !== otp){
return res.status(400).json({ error: "Invalid verification code", success: false });
}

existingCustomer.verificationCode = ""
await existingCustomer.save();

return res.status(200).json({ message: "verified", success: true})
} catch (error) {
return res.status(500).json({ message: "Internal server error", success: false})
}


}

async function resetPassword(req, res) {
try {
const {id, password} = req.body

const customer = await Customer.findOne({ _id: id });
if (!customer) {
return res.status(401).json({ error: "User not found" });
}
const hashedPassword = await bcrypt.hash(password, 10);
customer.password = hashedPassword;
await customer.save();
res.json({ message: "Password reset successful" });
} catch (error) {
res.status(501).json({ error: "Internal server error" });
}
}

module.exports = {
verifyEmail,
verifyOTP,
resetPassword,
};
4 changes: 4 additions & 0 deletions backend/models/customer.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ const customerSchema = new Schema(
message: (props) => `${props.value} is not a valid email address!`,
},
},
verificationCode: {
type: String,
default: ""
},
role: {
type: String,
default: "customer",
Expand Down
15 changes: 15 additions & 0 deletions backend/routes/forgotRouter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const express = require("express");
const {
verifyEmail,
verifyOTP,
resetPassword,
} = require("../controller/forgotPassword.controller");

const router = express.Router();
require("dotenv").config();

router.post("/verify-email", verifyEmail);
router.post("/verify-otp", verifyOTP);
router.post("/resetpassword", resetPassword);

module.exports = router;
3 changes: 2 additions & 1 deletion backend/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ try {
}

router.get("/", (req, res) => {
res.json({
return res.json({
message: "Welcome to the restaurant API!",
version: "1.0.0",
endpoints: {
Expand All @@ -46,5 +46,6 @@ router.use("/feedback", feedbackRouter);
router.use("/user", require("./customerRouter"));
router.use("/reservation", require("./reservationRouter"));
router.use("/newsletter", require("./newsletterRoute"));
router.use("/forgot", require("./forgotRouter"))

module.exports = router;
111 changes: 111 additions & 0 deletions frontend/src/components/Pages/EmailVerify.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Link, useNavigate } from 'react-router-dom';
import photo from '../../assets/login.png';
import React, { useState } from 'react';
import { message } from 'antd';

const EmailVerify = () => {
const API_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3000';
const navigate = useNavigate(); // Use useNavigate for navigation
const [email, setEmail] = useState("")

const handleChange = (e) => {
setEmail(e.target.value);
};

const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

// Helper function for email validation
const isValidEmail = (email) => {
// Basic email regex, consider using a more robust solution in production
return /\S+@\S+\.\S+/.test(email);
};

const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
setError(null);

// Add input validation // Basic validation examples
if (!isValidEmail(email)) {
setError('Please enter a valid email address');
return;
}

try {
const response = await fetch(`${API_URL}/api/forgot/verify-email`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email
}),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || 'Reset password failed');
}
console.log(result);


// Display success message and navigate to login
message.success('Password reset successfully! Please log in.');
navigate(`/verifyotp/${result.id}`);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};

return (
<div className="w-screen h-screen flex items-center justify-center pt-10">
<img src={photo} alt="login" loading="lazy" className=" w-3/4 absolute" />
<form
onSubmit={(e) => handleSubmit(e)}
className="form z-10 p-16 bg-lightblue flex flex-col items-start justify-center gap-5 rounded-lg border-2 border-black shadow-[4px_4px_0px_0px_black] bg-[#f1e9dc]"
>
<span className="block text-[#666] font-semibold text-2xl ">
Hey User,
</span>
<div className="title text-[#323232] font-black text-7xl mb-6">
Reset Your<br></br> Password
<br />
</div>

{error && (
<div className="w-full p-2 bg-red-100 text-red-700 border border-red-400 rounded-md">
{error}
</div>
)}

<input
className="input w-full h-10 rounded-md border-2 border-black bg-beige shadow-[4px_4px_0px_0px_black] text-[15px] font-semibold text-[#323232] p-2.5 focus:outline-none focus:border-[#2d8cf0] placeholder-[#666] placeholder-opacity-80"
name="email"
placeholder="Email"
type="email"
aria-required="true"
autoComplete="email"
onChange={(e) => handleChange(e)}
/>

<h3 className="flex items-center justify-between w-full">
<span className="block text-[#666] font-semibold text-xl transform hover:scale-110 hover:-translate-y-1 hover:text-green-500 transition">
<Link to={'/login'}>Go Back To Login Page</Link>
</span>
</h3>

<button
type="submit"
className="button-confirm mx-auto mt-12 px-4 w-30 h-10 rounded-md border-2 border-black bg-beige shadow-[4px_4px_0px_0px_black] text-[17px] font-semibold text-[#323232] cursor-pointer active:shadow-none active:translate-x-[3px] active:translate-y-[3px]"
disabled={isLoading}
>
{isLoading ? 'Submitting...' : 'Let’s go →'}
</button>
</form>
</div>
);
};

export default EmailVerify;
7 changes: 6 additions & 1 deletion frontend/src/components/Pages/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const Login = () => {
<img src={photo} alt="login" loading="lazy" className=" w-3/4 absolute" />
<form
onSubmit={(e) => handleSubmit(e)}
className="form z-10 p-16 bg-lightblue flex flex-col items-start justify-center gap-5 rounded-lg border-2 border-black shadow-[4px_4px_0px_0px_black] bg-[#f1e9dc]"
className="form z-10 p-16 bg-lightblue flex flex-col items-start justify-center gap-4 rounded-lg border-2 border-black shadow-[4px_4px_0px_0px_black] bg-[#f1e9dc]"
>
<div className="title text-[#323232] font-black text-7xl mb-6">
Welcome,
Expand Down Expand Up @@ -93,6 +93,11 @@ const Login = () => {
{hidden ? <FaEyeSlash/> : <FaEye/>}
</button>
</div>

<div className="transform hover:text-red-500 transition">
<Link to={'/email-verify'}>Forgot Password?</Link>
</div>

<h3 className="flex items-center justify-between w-full">
Dont have an account?
<span className="block text-[#666] font-semibold text-xl transform hover:scale-110 hover:-translate-y-1 hover:text-green-500 transition">
Expand Down
Loading

0 comments on commit b6e8228

Please sign in to comment.