diff --git a/backend/config/nodemailer.js b/backend/config/nodemailer.js index 255f0030..eb354cbc 100644 --- a/backend/config/nodemailer.js +++ b/backend/config/nodemailer.js @@ -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}`, + ); + } + } + +} \ No newline at end of file diff --git a/backend/controller/forgotPassword.controller.js b/backend/controller/forgotPassword.controller.js new file mode 100644 index 00000000..61b025a1 --- /dev/null +++ b/backend/controller/forgotPassword.controller.js @@ -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, +}; diff --git a/backend/models/customer.model.js b/backend/models/customer.model.js index 7b196dd7..cd446f41 100644 --- a/backend/models/customer.model.js +++ b/backend/models/customer.model.js @@ -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", diff --git a/backend/routes/forgotRouter.js b/backend/routes/forgotRouter.js new file mode 100644 index 00000000..14024474 --- /dev/null +++ b/backend/routes/forgotRouter.js @@ -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; diff --git a/backend/routes/index.js b/backend/routes/index.js index 3893069a..63bcbbfb 100644 --- a/backend/routes/index.js +++ b/backend/routes/index.js @@ -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: { @@ -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; diff --git a/frontend/src/components/Pages/EmailVerify.jsx b/frontend/src/components/Pages/EmailVerify.jsx new file mode 100644 index 00000000..00a952ab --- /dev/null +++ b/frontend/src/components/Pages/EmailVerify.jsx @@ -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 ( +
+ login +
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]" + > + + Hey User, + +
+ Reset Your

Password +
+
+ + {error && ( +
+ {error} +
+ )} + + handleChange(e)} + /> + +

+ + Go Back To Login Page + +

+ + +
+
+ ); +}; + +export default EmailVerify; diff --git a/frontend/src/components/Pages/Login.jsx b/frontend/src/components/Pages/Login.jsx index c8b9fd59..537c8f6b 100644 --- a/frontend/src/components/Pages/Login.jsx +++ b/frontend/src/components/Pages/Login.jsx @@ -61,7 +61,7 @@ const Login = () => { login
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]" >
Welcome, @@ -93,6 +93,11 @@ const Login = () => { {hidden ? : }
+ +
+ Forgot Password? +
+

Dont have an account? diff --git a/frontend/src/components/Pages/ResetPassword.jsx b/frontend/src/components/Pages/ResetPassword.jsx index 9174d075..b277292f 100644 --- a/frontend/src/components/Pages/ResetPassword.jsx +++ b/frontend/src/components/Pages/ResetPassword.jsx @@ -1,16 +1,18 @@ -import { Link, useNavigate } from 'react-router-dom'; +import { Link, useNavigate, useParams } from 'react-router-dom'; import photo from '../../assets/login.png'; import React, { useState } from 'react'; import { message } from 'antd'; +import { FaEye } from "react-icons/fa"; +import { FaEyeSlash } from "react-icons/fa6"; const ResetPassword = () => { const API_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3000'; const navigate = useNavigate(); // Use useNavigate for navigation - const [data, setData] = useState({ - email: '', - password: '', - confirmPassword: '', - }); + const { id } = useParams(); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [hidden, setHidden] = useState(true) + const [confHidden, setConfHidden] = useState(true) const handleChange = (e) => { setData({ ...data, [e.target.name]: e.target.value }); @@ -19,29 +21,18 @@ const ResetPassword = () => { 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(data.email)) { - setError('Please enter a valid email address'); - return; - } - - if (data.password.length < 8) { + if (password.length < 8) { setError('Password must be at least 8 characters long'); return; } - const passwordMatch = data.password === data.confirmPassword; + const passwordMatch = password === confirmPassword; if (!passwordMatch) { setError('Passwords do not match'); setIsLoading(false); @@ -49,12 +40,15 @@ const ResetPassword = () => { } try { - const response = await fetch(`${API_URL}/api/user/reset-password`, { + const response = await fetch(`${API_URL}/api/forgot/resetpassword`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(data), + body: JSON.stringify({ + id: id, + password: password + }), }); const result = await response.json(); if (!response.ok) { @@ -82,7 +76,7 @@ const ResetPassword = () => { Hey User,
- Reset Your

Password + Enter your

New Password
@@ -92,36 +86,39 @@ const ResetPassword = () => { )} - handleChange(e)} - /> - - handleChange(e)} - /> - handleChange(e)} - /> +
+ setPassword(e.target.value)} + /> + +
-

- - Go Back To Login Page - -

+
+ setConfirmPassword(e.target.value)} + value={confirmPassword} + /> + +
+
+ + ); +}; + +export default VerifyOtp; diff --git a/frontend/src/router/index.jsx b/frontend/src/router/index.jsx index 712ee45a..994e6334 100644 --- a/frontend/src/router/index.jsx +++ b/frontend/src/router/index.jsx @@ -18,6 +18,8 @@ import Signup from '../components/Pages/Signup'; import Login from '../components/Pages/Login'; import ResetPassword from '../components/Pages/ResetPassword'; import Admin from '../components/Pages/Admin'; +import VerifyOtp from '../components/Pages/VerifyOtp'; +import EmailVerify from '../components/Pages/EmailVerify'; const router = createBrowserRouter( createRoutesFromElements( @@ -32,8 +34,10 @@ const router = createBrowserRouter( } /> } /> } /> - } /> + } /> } /> + } /> + } /> ) );