diff --git a/backend/.env.sample b/backend/.env.sample index 9c8fcbd..71b3925 100644 --- a/backend/.env.sample +++ b/backend/.env.sample @@ -1,4 +1,6 @@ # sample env file with some imp info # PORT = 6352 -# MONGO_URL = YOUR_MONGODB_URL \ No newline at end of file +# MONGO_URL = YOUR_MONGODB_URL +# EMAIL="" +# EMAIL_PASSWORD="" //this is app password \ No newline at end of file diff --git a/backend/Schemas/User.Schema.js b/backend/Schemas/User.Schema.js index c827eba..9dfa46d 100644 --- a/backend/Schemas/User.Schema.js +++ b/backend/Schemas/User.Schema.js @@ -26,6 +26,12 @@ const UserSchema = new mongoose.Schema({ type: String, default: "3 days", }, + resetPasswordToken: { + type: String, + }, + resetPasswordExpires: { + type: Date, + }, }); const User = mongoose.model("User", UserSchema); diff --git a/backend/index.js b/backend/index.js index ebc7178..71bde03 100644 --- a/backend/index.js +++ b/backend/index.js @@ -7,6 +7,8 @@ import jwt from "jsonwebtoken"; import bcrypt from "bcrypt"; import User from "./Schemas/User.Schema.js"; import client from "./twilioConfig.js"; +import crypto from "crypto"; // Add this line +import nodemailer from "nodemailer"; // Add this line dotenv.config(); @@ -341,6 +343,95 @@ app.post("/login", async (req, res) => { res.status(500).send({ message: "Error logging in", error: error.message }); } }); +// Configure nodemailer for email sending +const transporter = nodemailer.createTransport({ + service: "Gmail", + auth: { + user: process.env.EMAIL, + pass: process.env.EMAIL_PASSWORD, + }, +}); + +// Route to handle forgot password +app.post("/forgot-password", 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" }); + } + + const token = crypto.randomBytes(20).toString("hex"); + + user.resetPasswordToken = token; + user.resetPasswordExpires = Date.now() + 3600000; // 1 hour + + await user.save(); + + const resetURL = `http://localhost:3000/user/reset-password/${token}`; + + const mailOptions = { + to: user.email, + from: process.env.EMAIL, + subject: "Password Reset", + text: `You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n + Please click on the following link, or paste this into your browser to complete the process:\n\n + ${resetURL}\n\n + If you did not request this, please ignore this email and your password will remain unchanged.\n`, + }; + + transporter.sendMail(mailOptions, (err, response) => { + if (err) { + console.error("There was an error: ", err); + res.status(500).json({ message: "Error sending email", error: err }); + } else { + res.status(200).json({ + success: true, + message: "Password reset email sent successfully", + }); + } + }); + } catch (error) { + res.status(500).json({ message: "Server error", error: error.message }); + } +}); + +// Route to handle reset password +app.post("/user/reset-password/:token", async (req, res) => { + const { token } = req.params; + const { password } = req.body; + + try { + const user = await User.findOne({ + resetPasswordToken: token, + resetPasswordExpires: { $gt: Date.now() }, + }); + + if (!user) { + return res.status(400).json({ message: "Password reset token is invalid or has expired." }); + } + + const hashedPassword = await bcrypt.hash(password, 10); + + user.password = hashedPassword; + user.resetPasswordToken = undefined; + user.resetPasswordExpires = undefined; + + await user.save(); + + res.status(200).json({ + success: true, + message: "Password reset successful!", + }); + } catch (error) { + res.status(500).json({ message: "Server error", error: error.message }); + } +}); + + + + // Start the server const server = app.listen(PORT, () => { diff --git a/backend/package-lock.json b/backend/package-lock.json index fc71943..a66a124 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,6 +15,7 @@ "express": "^4.19.2", "jsonwebtoken": "^9.0.2", "mongoose": "^8.4.0", + "nodemailer": "^6.9.14", "nodemon": "^3.1.1", "twilio": "^5.1.1" }, @@ -5519,6 +5520,14 @@ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, + "node_modules/nodemailer": { + "version": "6.9.14", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz", + "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.1.tgz", diff --git a/backend/package.json b/backend/package.json index b45c2fd..3dd18f8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,6 +21,7 @@ "express": "^4.19.2", "jsonwebtoken": "^9.0.2", "mongoose": "^8.4.0", + "nodemailer": "^6.9.14", "nodemon": "^3.1.1", "twilio": "^5.1.1" }, diff --git a/src/App.js b/src/App.js index e7014b4..6471c45 100644 --- a/src/App.js +++ b/src/App.js @@ -17,6 +17,8 @@ import AdminD from "./Dashboards/AdminD"; import Settings from "./pages/Settings"; import AccountSettings from "./settings/AccountSettings"; import NotFoundPage from "./pages/PNF"; +import ForgotPassword from "./pages/Forgotpassword.js"; +import ResetPassword from "./pages/ResetPassword.js"; const App = () => { return ( @@ -26,6 +28,8 @@ const App = () => { } /> } /> } /> + } /> + } /> }> } /> } /> diff --git a/src/pages/Forgotpassword.js b/src/pages/Forgotpassword.js new file mode 100644 index 0000000..2b9c53d --- /dev/null +++ b/src/pages/Forgotpassword.js @@ -0,0 +1,121 @@ +import React, { useState } from "react"; +import axios from "axios"; +import { toast, ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; +import Navbar from "../components/Navbar"; +import { RotatingLines } from "react-loader-spinner"; +import { useNavigate } from "react-router-dom"; + +const ForgotPassword = () => { +const navigate=useNavigate(); + const [email, setEmail] = useState(""); + const [errors, setErrors] = useState({}); + const [loading, setLoading] = useState(false); + + const validateEmail = (email) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + const newErrors = {}; + + if (!validateEmail(email)) { + newErrors.email = "Invalid email address."; + } + + setErrors(newErrors); + + if (Object.keys(newErrors).length === 0) { + setLoading(true); + try { + const response = await axios.post( + "https://smartserver-scbe.onrender.com/forgot-password", + { email } + ); + + if (response && response.data.success) { + toast.success("Password reset email sent!"); + navigate("/user/login") + + } + } catch (error) { + if (error.response) { + setErrors({ server: error.response.data.message }); + toast.error(error.response.data.message); + } else if (error.request) { + setErrors({ + server: "Server not responding. Please try again later.", + }); + toast.error("Server not responding. Please try again later."); + } else { + setErrors({ server: "An error occurred. Please try again." }); + toast.error("An error occurred. Please try again."); + } + } finally { + setLoading(false); + } + } + }; + + return ( + <> + + +
+
+
+ Logo +
+

+ Forgot Password +

+

+ Enter your email to receive a password reset link +

+
+
+ + setEmail(e.target.value)} + className="w-full px-4 py-2 mt-2 border rounded-md focus:outline-none focus:ring-1 focus:ring-green-600" + placeholder="Email" + /> + {errors.email && ( +

{errors.email}

+ )} +
+ {errors.server && ( +

{errors.server}

+ )} + +
+
+
+ + ); +}; + +export default ForgotPassword; diff --git a/src/pages/Login.js b/src/pages/Login.js index 039d436..55bff59 100644 --- a/src/pages/Login.js +++ b/src/pages/Login.js @@ -171,13 +171,19 @@ const Login = () => { "Login" )} -
+
Sign Up + + Forgot Password? +
diff --git a/src/pages/ResetPassword.js b/src/pages/ResetPassword.js new file mode 100644 index 0000000..9d3b462 --- /dev/null +++ b/src/pages/ResetPassword.js @@ -0,0 +1,137 @@ +import React, { useState } from "react"; +import axios from "axios"; +import { toast, ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; +import Navbar from "../components/Navbar"; +import { RotatingLines } from "react-loader-spinner"; +import { useNavigate, useParams } from "react-router-dom"; + +const ResetPassword = () => { + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [errors, setErrors] = useState({}); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + const { token } = useParams(); + + const validatePassword = (password) => { + return password.length >= 6; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + const newErrors = {}; + + if (!validatePassword(password)) { + newErrors.password = "Password must be at least 6 characters."; + } + + if (password !== confirmPassword) { + newErrors.confirmPassword = "Passwords do not match."; + } + + setErrors(newErrors); + + if (Object.keys(newErrors).length === 0) { + setLoading(true); + try { + const response = await axios.post( + `https://smartserver-scbe.onrender.com/user/reset-password/${token}`, + { password } + ); + + if (response && response.data.success) { + toast.success("Password reset successful!"); + navigate("/user/login"); + } + } catch (error) { + if (error.response) { + setErrors({ server: error.response.data.message }); + toast.error(error.response.data.message); + } else if (error.request) { + setErrors({ + server: "Server not responding. Please try again later.", + }); + toast.error("Server not responding. Please try again later."); + } else { + setErrors({ server: "An error occurred. Please try again." }); + toast.error("An error occurred. Please try again."); + } + } finally { + setLoading(false); + } + } + }; + + return ( + <> + + +
+
+
+ Logo +
+

+ Reset Password +

+
+
+ + setPassword(e.target.value)} + className="w-full px-4 py-2 mt-2 border rounded-md focus:outline-none focus:ring-1 focus:ring-green-600" + placeholder="New Password" + /> + {errors.password && ( +

{errors.password}

+ )} +
+
+ + setConfirmPassword(e.target.value)} + className="w-full px-4 py-2 mt-2 border rounded-md focus:outline-none focus:ring-1 focus:ring-green-600" + placeholder="Confirm Password" + /> + {errors.confirmPassword && ( +

+ {errors.confirmPassword} +

+ )} +
+ {errors.server && ( +

{errors.server}

+ )} + +
+
+
+ + ); +}; + +export default ResetPassword;