From 876f96e7a80c6f168f98807f8ea9da9296a7d881 Mon Sep 17 00:00:00 2001 From: Vinay Anand Lodhi Date: Thu, 24 Oct 2024 17:05:42 +0530 Subject: [PATCH] forgot password feature added backend and frontend --- backend/.env.sample | 9 + backend/controllers/submitFeedback.js | 106 +++++++++++ backend/index.js | 2 + backend/models/User.js | 8 + backend/package-lock.json | 10 ++ backend/package.json | 1 + backend/routes/stationRoutes.js | 6 +- backend/utils/emailUtils.js | 28 +++ backend/utils/otputils.js | 16 ++ frontend/src/Pages/PasswordRecovery.jsx | 222 +++++++++++++++++++++--- 10 files changed, 384 insertions(+), 24 deletions(-) create mode 100644 backend/.env.sample create mode 100644 backend/utils/emailUtils.js create mode 100644 backend/utils/otputils.js diff --git a/backend/.env.sample b/backend/.env.sample new file mode 100644 index 0000000..3d377ae --- /dev/null +++ b/backend/.env.sample @@ -0,0 +1,9 @@ +EMAIL_USER=your_gmail +#your email +EMAIL_USER=your_gmail + +# To create a passkey on the phone or computer you’re on: + +# 1. Go to https://myaccount.google.com/signinoptions/passkeys. +# 2. Tap Create a passkey and then Continue.(You'll be required to unlock your device.) +# 3. A 16 character passkey is generated which you can use in below. \ No newline at end of file diff --git a/backend/controllers/submitFeedback.js b/backend/controllers/submitFeedback.js index 5062e6d..43654f4 100644 --- a/backend/controllers/submitFeedback.js +++ b/backend/controllers/submitFeedback.js @@ -1,4 +1,9 @@ +import nodemailer from 'nodemailer'; +import User from '../models/User.js'; +import { generateOTP , verifyOTP} from '../utils/otputils.js'; // Import the OTP generation function +import { sendOTPEmail } from '../utils/emailUtils.js'; // Import the email sender utility if separated into a different file +import { hashPassword } from '../utils/authFunctions.js'; // Controller to handle user feedback submission export const submitFeedback = async (req, res) => { @@ -21,3 +26,104 @@ export const submitFeedback = async (req, res) => { return res.status(error.statusCode || 500).json({ message: error.message || 'An error occurred while submitting feedback' }); } }; + +export const sendOTPToEmail = async (req, res) => { + try { + const { email } = req.body; + + // Check if email is provided + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + + // Find user by email + const user = await User.findOne({ email }); + + if (!user) { + return res.status(404).json({ error: 'User does not exist' }); + } + + // Generate OTP and set expiry (10 minutes from now) + const otp = generateOTP(); + const otpExpiry = new Date(Date.now() + 10 * 60 * 1000); // OTP expires in 10 minutes + + // Store the OTP and expiry in the user's document + user.otp = otp; + user.otpExpiry = otpExpiry; + await user.save(); + + // Send OTP to user's email using the utility + await sendOTPEmail(email, otp); + + res.status(200).json({ message: 'OTP sent to email' }); + } catch (error) { + console.error('Error sending OTP:', error); + return res.status(500).json({ error: error.message || 'Internal Server Error' }); + } +}; + +export const verifyOTPController = async (req, res) => { + try { + const { email, otp } = req.body; + + // Check if email and OTP are provided + if (!email || !otp) { + return res.status(400).json({ error: 'Email and OTP are required' }); + } + + // Find user by email + const user = await User.findOne({ email }); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + // Verify the OTP and check expiration + const isValid = verifyOTP(user, otp); + + if (!isValid) { + return res.status(400).json({ error: 'Invalid or expired OTP' }); + } + + // Clear OTP after successful verification (optional, but recommended) + user.otp = null; + user.otpExpiry = null; + await user.save(); + + // OTP is valid + res.status(200).json({ message: 'OTP verified successfully' }); + } catch (error) { + console.error('Error verifying OTP:', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}; + +export const resetPassword = async (req, res) => { + try { + const { email, newPassword } = req.body; + + // Check if both email and new password are provided + if (!email || !newPassword) { + return res.status(400).json({ error: 'Email and new password are required' }); + } + + // Find the user by email + const user = await User.findOne({ email }); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + // Hash the new password + const hashedPassword = await hashPassword(newPassword); + + // Update the user's password in the database + user.password = hashedPassword; + await user.save(); + + res.status(200).json({ message: 'Password reset successfully' }); + } catch (error) { + console.error('Error resetting password:', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}; diff --git a/backend/index.js b/backend/index.js index 3ecc689..a871fca 100644 --- a/backend/index.js +++ b/backend/index.js @@ -34,6 +34,8 @@ app.use("/auth", authRoutes); app.use("/api", authRoutes); app.use("/station", stationRoutes); + + app.get("/", (req, res) => { res.send("Working..."); }); diff --git a/backend/models/User.js b/backend/models/User.js index 05ac800..ca95f98 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -32,6 +32,14 @@ const userSchema = new Schema({ type: String, trim: true, default: '', + }, + otp: { + type: String, // The OTP code + required: false, + }, + otpExpiry: { + type: Date, // The OTP expiry timestamp + required: false, } }, { timestamps: true diff --git a/backend/package-lock.json b/backend/package-lock.json index 67ee812..54dbbc5 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -19,6 +19,7 @@ "jsonwebtoken": "^9.0.2", "mongoose": "^8.7.0", "node": "^22.8.0", + "nodemailer": "^6.9.15", "nodemon": "^3.1.7", "socket.io": "^4.8.0" } @@ -2305,6 +2306,15 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/nodemailer": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz", + "integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", diff --git a/backend/package.json b/backend/package.json index ff8eb97..7004140 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,7 @@ "jsonwebtoken": "^9.0.2", "mongoose": "^8.7.0", "node": "^22.8.0", + "nodemailer": "^6.9.15", "nodemon": "^3.1.7", "socket.io": "^4.8.0" }, diff --git a/backend/routes/stationRoutes.js b/backend/routes/stationRoutes.js index 1629b20..3ea5ecc 100644 --- a/backend/routes/stationRoutes.js +++ b/backend/routes/stationRoutes.js @@ -2,7 +2,7 @@ import express from 'express'; import { getCloakroomBookingsByStation, getCoolieBookingsByStation, getStationBookings, getWheelchairBookingsByStation } from '../controllers/stationBookingsController.js'; import { createStation, getAllStations } from '../controllers/StationController.js'; - +import { sendOTPToEmail ,verifyOTPController, resetPassword} from '../controllers/submitFeedback.js'; const router = express.Router(); // Route to fetch all bookings for a station @@ -15,5 +15,7 @@ router.post('/', createStation); router.get('/:id/coolies', getCoolieBookingsByStation); router.get('/:id/wheelchairs', getWheelchairBookingsByStation); router.get('/:id/cloakrooms', getCloakroomBookingsByStation); - +router.post('/send-otp', sendOTPToEmail); +router.post('/verify-otp', verifyOTPController); +router.post('/reset-password', resetPassword); export default router; diff --git a/backend/utils/emailUtils.js b/backend/utils/emailUtils.js new file mode 100644 index 0000000..906e3b8 --- /dev/null +++ b/backend/utils/emailUtils.js @@ -0,0 +1,28 @@ +import nodemailer from 'nodemailer'; + +// Function to send OTP via email +export const sendOTPEmail = async (email, otp) => { + try { + // Create a transporter using SMTP settings (e.g., Gmail) + const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: process.env.EMAIL_USER, // Your email address + pass: process.env.EMAIL_PASS // Your email password or app-specific password + } + }); + + const mailOptions = { + from: process.env.EMAIL_USER, + to: email, + subject: 'Your Password Reset OTP', + text: `Your OTP for password reset is ${otp}. It will expire in 10 minutes.` + }; + + // Send the email + await transporter.sendMail(mailOptions); + } catch (error) { + console.error('Error sending email:', error); + throw new Error('Failed to send OTP email'); + } +}; diff --git a/backend/utils/otputils.js b/backend/utils/otputils.js new file mode 100644 index 0000000..f9e6348 --- /dev/null +++ b/backend/utils/otputils.js @@ -0,0 +1,16 @@ +// Generate a 6-digit OTP +export const generateOTP = () => { + return Math.floor(100000 + Math.random() * 900000).toString(); + }; + + // Check if OTP is valid (both matching and not expired) + export const verifyOTP = (user, submittedOtp) => { + return user.otp === submittedOtp && user.otpExpiry > Date.now(); + }; + + // Clear OTP after it's used + export const clearOTP = async (user) => { + user.otp = null; + user.otpExpiry = null; + await user.save(); + }; diff --git a/frontend/src/Pages/PasswordRecovery.jsx b/frontend/src/Pages/PasswordRecovery.jsx index 8a7e1fc..beb1dae 100644 --- a/frontend/src/Pages/PasswordRecovery.jsx +++ b/frontend/src/Pages/PasswordRecovery.jsx @@ -3,15 +3,114 @@ import { useNavigate } from "react-router-dom"; const PasswordRecovery = () => { const [email, setEmail] = useState(""); + const [otp, setOtp] = useState(""); // For OTP input + const [newPassword, setNewPassword] = useState(""); // For new password input + const [confirmPassword, setConfirmPassword] = useState(""); // For confirm password input + const [step, setStep] = useState(1); // For handling the steps (1: Email, 2: OTP, 3: Password Reset) + const [otpDisabled, setOtpDisabled] = useState(false); // For disabling OTP input after submission const [message, setMessage] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); const navigate = useNavigate(); + // Step 1: Handle sending OTP to email const handlePasswordRecovery = async (e) => { e.preventDefault(); - // Implement password recovery logic here - setMessage( - "If an account with that email exists, a password recovery link has been sent." - ); + setMessage(""); + setError(""); + setLoading(true); + + try { + const response = await fetch("http://localhost:5000/station/send-otp", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email }), + }); + + const data = await response.json(); + + if (response.ok) { + setMessage("OTP has been sent to your email."); + setStep(2); // Move to OTP input step + } else { + setError(data.error || "Failed to send OTP. Please try again."); + } + } catch (error) { + setError("An error occurred. Please try again later."); + } finally { + setLoading(false); + } + }; + + // Step 2: Handle OTP verification + const handleVerifyOTP = async (e) => { + e.preventDefault(); + setMessage(""); + setError(""); + setLoading(true); + + try { + const response = await fetch("http://localhost:5000/station/verify-otp", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, otp }), + }); + + const data = await response.json(); + + if (response.ok) { + setMessage("OTP verified successfully. You can now reset your password."); + setStep(3); // Move to password reset step + setOtpDisabled(true); // Disable OTP input after successful verification + } else { + setError(data.error || "Invalid OTP. Please try again."); + } + } catch (error) { + setError("An error occurred. Please try again later."); + } finally { + setLoading(false); + } + }; + + // Step 3: Handle new password submission + const handlePasswordReset = async (e) => { + e.preventDefault(); + setMessage(""); + setError(""); + setLoading(true); + + if (newPassword !== confirmPassword) { + setError("Passwords do not match."); + setLoading(false); + return; + } + + try { + const response = await fetch("http://localhost:5000/station/reset-password", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, newPassword }), + }); + + const data = await response.json(); + + if (response.ok) { + setMessage("Password reset successfully. You can now log in."); + navigate("/login"); + } else { + setError(data.error || "Failed to reset password. Please try again."); + } + } catch (error) { + setError("An error occurred. Please try again later."); + } finally { + setLoading(false); + } }; const handleBackToLogin = () => { @@ -19,44 +118,123 @@ const PasswordRecovery = () => { }; return ( -
-
-

+
+
+

Password Recovery

+ + {/* Success Message */} {message && ( -

+

{message}

)} -
-
-