diff --git a/back-end/.env b/back-end/.env index 8e776ec..0ae91f9 100644 --- a/back-end/.env +++ b/back-end/.env @@ -3,3 +3,5 @@ SESSION_SECRET = 'myKey123!' JWT_SECRET = 'myKey123!' CLIENT_URL = "http://localhost:5173" MONGODB_URI = "mongodb+srv://yz5835:mlDMBoYStK63fpWj@bakerdb.imjpooa.mongodb.net/bakerdb" +EMAIL_USER='goodoldmap@gmail.com' +EMAIL_PASS='Goodoldmap123' diff --git a/back-end/src/app.mjs b/back-end/src/app.mjs index 28b3959..1478771 100644 --- a/back-end/src/app.mjs +++ b/back-end/src/app.mjs @@ -107,7 +107,7 @@ const passwordValidationRules = [ // Account routes app.patch("/changeusername", usernameValidationRules, changeusernameRouter); //Finished app.patch("/resetemail", emailValidationRules, resetemailRouter); //Finished -app.post("/forgetpassword", forgetpasswordRouter); +app.post("/forget", forgetpasswordRouter); app.patch("/resetpassword", passwordValidationRules, resetpasswordRouter); //Finished app.delete("/delaccount", delaccountRouter); //Finished diff --git a/back-end/src/routes/forgetpasswordRouter.mjs b/back-end/src/routes/forgetpasswordRouter.mjs index 44be965..918f707 100644 --- a/back-end/src/routes/forgetpasswordRouter.mjs +++ b/back-end/src/routes/forgetpasswordRouter.mjs @@ -1,44 +1,39 @@ +//to-do add recovery page, now we only send token to client's email import User from '../models/User.mjs'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; -import sendRecoveryEmail from './sendEmail.mjs'; // Your email sending function +import sendRecoveryEmail from './sendEmail.mjs'; const forgetpasswordRouter = async (req, res) => { const { email } = req.body; + console.log("Received forgot password request for email:", email); + try { const user = await User.findOne({ email: email.toLowerCase() }); + console.log("User found:", user); if (!user) { + console.log("User not found for email:", email); return res.status(404).json({ message: "User not found." }); } - // Generate a token for password reset const resetToken = jwt.sign({ id: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: "1h" }); - - // Store the token in the database (you'll need to implement this logic) - // E.g., user.resetToken = resetToken; await user.save(); - // Send the token to the user's email + // Store the reset token in the user's record in the database + user.resetToken = resetToken; + await user.save(); + await sendRecoveryEmail(email, resetToken); + console.log("Password reset email sent to:", email); return res.status(200).json({ message: "Password reset email sent." }); } catch (error) { - console.error(error); + console.error("Error in /forgotpassword route:", error); return res.status(500).json({ message: "Internal server error." }); } + + }; export default forgetpasswordRouter; - -// const forgetpasswordRouter = async (req, res) => { -// // req.body: email, userID -// // TODO: compare email to see if that match, -// // if match, send recovery data to email -// try { -// return res.sendStatus(200) -// } catch (error) { -// } -// } - -// export default forgetpasswordRouter \ No newline at end of file diff --git a/back-end/src/routes/sendEmail.mjs b/back-end/src/routes/sendEmail.mjs index 42ec673..9d880c0 100644 --- a/back-end/src/routes/sendEmail.mjs +++ b/back-end/src/routes/sendEmail.mjs @@ -1,19 +1,18 @@ import nodemailer from 'nodemailer'; const sendRecoveryEmail = async (email, token) => { - // Create a transporter object using the default SMTP transport let transporter = nodemailer.createTransport({ - host: 'smtp.example.com', + host: 'smtp.gmail.com', // Gmail SMTP server port: 587, secure: false, // true for 465, false for other ports auth: { - user: 'your-email@example.com', // Your email - pass: 'your-password', // Your email password or app-specific password + user: 'goodoldmap@gmail.com', + pass: 'ebro wnec snzf fcqt', }, }); let mailOptions = { - from: '"Your App Name" ', // Sender address + from: '"The GoodOldMap" ', // Your verified sender email address to: email, // Receiver email subject: 'Password Recovery', // Subject line text: `Please use the following token to recover your password in 1h: ${token}`, // Plain text body diff --git a/back-end/tests/routes/testsendemail.mjs b/back-end/tests/routes/testsendemail.mjs new file mode 100644 index 0000000..ee47d33 --- /dev/null +++ b/back-end/tests/routes/testsendemail.mjs @@ -0,0 +1,32 @@ +import nodemailer from 'nodemailer'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const testEmailSending = async () => { + let transporter = nodemailer.createTransport({ + host: 'smtp.gmail.com', + port: 587, + secure: false, + auth: { + user: 'goodoldmap@gmail.com', + pass: 'ebro wnec snzf fcqt', // Use environment variables to keep this secure + }, + }); + + let mailOptions = { + from: `"The GoodOldMap" <${process.env.EMAIL_USER}>`, + to: 'rm5592@nyu.edu', // Replace with your test recipient email + subject: 'Test Email', + text: 'This is a test email from my Node.js application.', + }; + + try { + let info = await transporter.sendMail(mailOptions); + console.log('Message sent: %s', info.messageId); + } catch (error) { + console.error('Error sending email:', error); + } +}; + +testEmailSending(); diff --git a/front-end/src/pages/Account/Account.jsx b/front-end/src/pages/Account/Account.jsx index d4826ce..8b05f32 100644 --- a/front-end/src/pages/Account/Account.jsx +++ b/front-end/src/pages/Account/Account.jsx @@ -117,22 +117,6 @@ const AccountEdit = (props) => { } } - // route /forgetpassword - // TODO: user id - const sentForgetPwEmail = async (evt) => { - try { - const requestData = getFormData() - requestData["userID"] = "1234" - const response = await axiosProvider.post( - "/forgetpassword", - requestData, - ) - closePopup() - } catch (error) { - const errorMessage = error?.requestMessage || error.response?.data?.message || 'Change failed, please try again.'; - setMessage(errorMessage); - } - } // Finished: route /resetpassword const confirmResetPassword = async (evt) => { @@ -207,13 +191,6 @@ const AccountEdit = (props) => { buttons: [{value:"Discard", handleClick: closePopup}, {value:"Confirm", handleClick: confirmResetEmail}], }, - "forgotPassword": { - link: "Forget Password", - title: "Forget Password", - inputs: [{id:"email", name:"email", type:"text", placeholder:"email"}], - buttons: [{value:"Discard", handleClick: closePopup}, - {value: "Send Email", handleClick: sentForgetPwEmail}], - }, "changePassword": { link: "Change Password", title: "Change Password", diff --git a/front-end/src/pages/Account/popupContent.jsx b/front-end/src/pages/Account/popupContent.jsx index 4e148e6..5d6cf71 100644 --- a/front-end/src/pages/Account/popupContent.jsx +++ b/front-end/src/pages/Account/popupContent.jsx @@ -2,34 +2,33 @@ import { FormInputsPopup } from "../../components/form/formInput"; import { FormBtns } from "../../components/form/formBtn"; import { forwardRef } from "react"; - const PopupContent = forwardRef((props, ref) => { - // props: title(str), inputs(array of object), buttons:(array of object) - // optional: message + // Function to stop propagation for clicks inside the popup content + const handleContentClick = (e) => { + e.stopPropagation(); // Prevents clicks within the content from closing the popup + }; + return ( - // dark background + // Overlay: Closes the popup when clicked
- {/* white popup container: */} -
- + + {/* Content Area: Does not propagate clicks to the overlay */} +
- {/* content area */}

{props?.title}

{props?.message}

-
+
-
{/* Adjust button positioning as needed */} +
-
); }); - -export default PopupContent \ No newline at end of file +export default PopupContent; diff --git a/front-end/src/pages/Authenticate/Login.jsx b/front-end/src/pages/Authenticate/Login.jsx index de68f50..965a61c 100644 --- a/front-end/src/pages/Authenticate/Login.jsx +++ b/front-end/src/pages/Authenticate/Login.jsx @@ -1,73 +1,100 @@ -import { useState } from 'react' -import AuthHeader from './authHeader' -import PageLink from '../../components/common/pageLink' -import { FormInputs } from '../../components/form/formInput' -import FormBtn from '../../components/form/formBtn' -import { useNavigate } from 'react-router-dom' -import { useRef } from 'react' -import axiosProvider from '../../util/api/axios' +import { useState, useRef } from 'react'; +import AuthHeader from './authHeader'; +import PopupContent from '../Account/popupContent'; +import PageLink from '../../components/common/pageLink'; +import { FormInputs } from '../../components/form/formInput'; +import FormBtn from '../../components/form/formBtn'; +import { useNavigate } from 'react-router-dom'; +import axiosProvider from '../../util/api/axios'; const Login = () => { - const [message, setMessage] = useState("") - const fields = ["email", "password"] + const [loginMessage, setLoginMessage] = useState(""); + const [popupMessage, setPopupMessage] = useState(""); + const fields = ["email", "password"]; const formRef = useRef(null); + const popupFormRef = useRef(null); + const [currentActionData, setCurrentActionData] = useState(null); const navigate = useNavigate(); - // click to login - const handleClick = async (evt) => { - evt.preventDefault(); - - const formData = new FormData(formRef.current) - const email = formData.get('email'); - const password = formData.get('password'); - - const loginData = { - email, - password - }; + const closePopup = () => { + setCurrentActionData(null); + setPopupMessage(""); + }; - const postOptions = { - headers: { - 'Content-Type': 'application/json' - } + const handleLogin = async (evt) => { + evt.preventDefault(); // Prevents the default form submission behavior + + const formData = new FormData(formRef.current); + const email = formData.get('email'); // Extracts the email from the form data + const password = formData.get('password'); // Extracts the password from the form data + + // Checks if both email and password fields are filled + if (!email || !password) { + setLoginMessage("Please fill in all input slots"); + return; } - + try { - // Handle incomplete data - if (!loginData.email || !loginData.password) throw {requestMessage: "Please fill in all input slots"} - const response = await axiosProvider.post( - "/login", - loginData, - postOptions - ) - - localStorage.setItem('token', response.data.accessToken); // Store the token + // Sends a POST request to the login endpoint with email and password + const response = await axiosProvider.post("/login", { email, password }); + + // Stores the token and user data in localStorage upon successful login + localStorage.setItem('token', response.data.accessToken); const userData = { uuid: response.data.user.uuid, name: response.data.user.name, email: response.data.user.email }; - localStorage.setItem('user', JSON.stringify(userData)) + localStorage.setItem('user', JSON.stringify(userData)); + + setLoginMessage("Login successful!"); // Sets a success message + navigate("/"); // Navigates to the home page or dashboard + } catch (error) { + // Sets an error message if the login process fails + setLoginMessage(error.message || 'Login failed, please try again.'); + } + }; + - setMessage("Login successful!"); - navigate("/") + const handleForgotPassword = async (evt) => { + const email = popupFormRef.current?.email?.value; + if (!email) { + evt.preventDefault() + setPopupMessage("Please enter your email."); + return; } - catch(error){ - const errorMessage = error.requestMessage || error.response?.data?.message || 'Login failed, please try again.'; - setMessage(errorMessage); + try { + await axiosProvider.post("http://localhost:3000/forget", { email }); + setPopupMessage("Reset password link sent to your email."); + } catch (error) { + setPopupMessage(error.message || 'Error sending reset password link.'); } - } - - // if user click guest visit redirect to main map + }; + + const forgotPasswordData = { + title: "Forgot Password", + inputs: [{id:"email", name:"email", type:"email", placeholder:"Enter your email"}], + buttons: [ + {value: "Discard", handleClick: closePopup}, + {value: "Submit", handleClick: handleForgotPassword} + ] + }; + const handleGuest = (evt) => { - evt.preventDefault() - // pop up alert: confirm guest visit - const continueGuest = window.confirm('Guest visit will not save your data, continue?'); + evt.preventDefault(); + const continueGuest = window.confirm('Guest visit will not save your data, continue?'); if (continueGuest) { - navigate("/", { state: { from: location.pathname } }); + navigate("/"); } - } - + }; + + + const handlePopupFormSubmit = (evt) => { + evt.preventDefault(); + if (currentActionData && currentActionData.buttons[0]) { + currentActionData.buttons[0].handleClick(); + } + }; return( //
@@ -90,20 +117,35 @@ const Login = () => { } `} - -
+ +
- - + +
- - + +
+ setCurrentActionData(forgotPasswordData)} style={{ cursor: 'pointer' }}> + Forgot Password + +
+ {currentActionData && +
e.stopPropagation()}> + + + }
- ) -} + ); +}; -export default Login \ No newline at end of file +export default Login; \ No newline at end of file