From 96efff85da17712d6097b2866f7cdbae05339186 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Mon, 29 Jul 2024 01:36:44 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5=20Reset=20password=20now=20working?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/App.tsx | 56 ++-- client/src/components/NavigationBar.tsx | 3 - client/src/components/SignInModule.tsx | 2 +- client/src/components/UpdateAccountModule.tsx | 32 +- client/src/pages/ResetPasswordPage.tsx | 142 ++++++++ server/connections/mailersend.js | 316 ++++++++++-------- server/models/User.js | 10 +- server/routes/users.js | 115 ++++++- server/security/generatePasswordResetToken.js | 16 + 9 files changed, 506 insertions(+), 186 deletions(-) create mode 100644 client/src/pages/ResetPasswordPage.tsx create mode 100644 server/security/generatePasswordResetToken.js diff --git a/client/src/App.tsx b/client/src/App.tsx index c78693c..7810864 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -20,42 +20,46 @@ import EditEventsPage from "./pages/EditEventsPage"; import DefaultLayout from "./layouts/default"; import AdministratorLayout from "./layouts/administrator"; import UsersManagement from "./pages/UsersManagement"; +import ResetPasswordPage from "./pages/ResetPasswordPage"; function App() { return ( {/* User Routes */} - }> - {/* General Routes */} - } /> - } path="signup" /> - } path="signin" /> - } path="springboard" /> - } path="manage-account" /> + + }> + {/* General Routes */} + } /> + } path="signup" /> + } path="signin" /> + } path="springboard" /> + } path="manage-account" /> - {/* Events Route */} - - } /> - + {/* Events Route */} + + } /> + - {/* Karang Guni Schedules Route */} - - } /> - + {/* Karang Guni Schedules Route */} + + } /> + - {/* Home Bill Contest Route */} - - } /> - } path="new-submission" /> - + {/* Home Bill Contest Route */} + + } /> + } path="new-submission" /> + - {/* Community Posts Route */} - - } /> - } path="create" /> - } path="post/:id" /> - } path="edit/:id" /> + {/* Community Posts Route */} + + } /> + } path="create" /> + } path="post/:id" /> + } path="edit/:id" /> + + } path="reset-password/:token" /> {/* Admin Routes */} diff --git a/client/src/components/NavigationBar.tsx b/client/src/components/NavigationBar.tsx index 8d030c2..ee1e2fc 100644 --- a/client/src/components/NavigationBar.tsx +++ b/client/src/components/NavigationBar.tsx @@ -36,9 +36,6 @@ export default function NavigationBar() { ); setUserInformation(value); }) - .catch(() => { - navigate("/signin"); - }) .finally(() => { setDoneLoading(true); }); diff --git a/client/src/components/SignInModule.tsx b/client/src/components/SignInModule.tsx index b95536c..ecc7bb1 100644 --- a/client/src/components/SignInModule.tsx +++ b/client/src/components/SignInModule.tsx @@ -45,7 +45,7 @@ export default function SignInModule() { if (value.accountType == 2) { navigate("/admin"); } else { - navigate("/springboard"); + window.location.reload(); } }); }) diff --git a/client/src/components/UpdateAccountModule.tsx b/client/src/components/UpdateAccountModule.tsx index c0abe2a..afd5887 100644 --- a/client/src/components/UpdateAccountModule.tsx +++ b/client/src/components/UpdateAccountModule.tsx @@ -18,7 +18,7 @@ import { Form, Formik } from "formik"; import NextUIFormikInput from "./NextUIFormikInput"; import { useNavigate } from "react-router-dom"; import UserProfilePicture from "./UserProfilePicture"; -import { popErrorToast } from "../utilities"; +import { popErrorToast, popToast } from "../utilities"; import instance from "../security/http"; export default function UpdateAccountModule() { @@ -105,6 +105,25 @@ export default function UpdateAccountModule() { }); }; + const sendResetPasswordEmail = () => { + instance + .put( + `${ + config.serverAddress + }/users/request-reset-password/${encodeURIComponent( + userInformation.email + )}` + ) + .then(() => { + console.log("Email sent successfully"); + popToast("Email sent to your mailbox!", 1); + }) + .catch((error) => { + console.error("Failed to send email:", error); + popErrorToast("Failed to send email: " + error); + }); + }; + return (
{userInformation && ( @@ -221,16 +240,7 @@ export default function UpdateAccountModule() { color="danger" variant="light" onPress={() => { - instance - .put( - `${config.serverAddress}/connections/send-reset-password-email/${userInformation.id}` - ) - .then(() => { - console.log("Email sent successfully"); - }) - .catch((error) => { - console.error("Failed to send email:", error); - }); + sendResetPasswordEmail(); }} > Reset your password diff --git a/client/src/pages/ResetPasswordPage.tsx b/client/src/pages/ResetPasswordPage.tsx new file mode 100644 index 0000000..3fe21f3 --- /dev/null +++ b/client/src/pages/ResetPasswordPage.tsx @@ -0,0 +1,142 @@ +import { useParams, useNavigate } from "react-router-dom"; +import instance from "../security/http"; +import config from "../config"; +import { useEffect, useState } from "react"; +import { Button, Card, CircularProgress } from "@nextui-org/react"; +import EcoconnectFullLogo from "../components/EcoconnectFullLogo"; +import NextUIFormikInput from "../components/NextUIFormikInput"; +import { Formik, Form } from "formik"; +import * as Yup from "yup"; +import axios from "axios"; +import { popErrorToast, popToast } from "../utilities"; + +const validationSchema = Yup.object({ + password: Yup.string() + .trim() + .max(69, "Password must be at most 69 characters") + .matches( + /^(?=.*[a-zA-Z])(?=.*[0-9]).{1,}$/, + "Password must contain both letters and numbers" + ) + .required("Password is required"), + confirmPassword: Yup.string() + .oneOf([Yup.ref("password"), undefined], "Passwords must match") + .required("Confirm Password is required"), +}); + +export default function ResetPasswordPage() { + const { token } = useParams(); + const navigate = useNavigate(); + + const [validationResult, setValidationResult] = useState(false); + const [pageLoading, setPageLoading] = useState(true); + + const validateToken = () => { + instance + .get(`${config.serverAddress}/users/reset-password/${token}`) + .then(() => { + setValidationResult(true); + }) + .catch(() => { + setValidationResult(false); + }) + .finally(() => { + setPageLoading(false); + }); + }; + + useEffect(() => { + validateToken(); + }, []); + + const handleSubmit = (values: any): void => { + console.log("submitting"); + instance + .post(`${config.serverAddress}/users/reset-password`, { + token, + password: values.password, + }) + .then(() => { + popToast("Success!", 1); + navigate("/signin"); + }) + .catch((error) => { + popErrorToast(error); + }); + }; + + return ( +
+
+ {pageLoading && ( +
+ +
+ )} + {!pageLoading && ( +
+ + {validationResult && ( +
+
+

Password Reset

+

Enter a new password below.

+
+ + {({ isValid, dirty }) => ( +
+ + + + + )} +
+
+ )} + {!validationResult && ( +
+
+

+ Reset portal has been closed. +

+

Please request for a password reset again.

+
+
+ )} +
+
+ +

·

+

+ © Copyright {new Date().getFullYear()}. All rights reserved. +

+
+
+ )} +
+
+ ); +} diff --git a/server/connections/mailersend.js b/server/connections/mailersend.js index 12d0d4b..847f631 100644 --- a/server/connections/mailersend.js +++ b/server/connections/mailersend.js @@ -49,170 +49,200 @@ async function sendEmail(recipientEmail, title, content) { } } -async function sendPasswordResetEmail(email, firstName) { +const ecoconnectEmailLogoUrl = + "https://onedrive.live.com/download?resid=FDC8D8692E9A43C0%21425747&authkey=%21ADT2uhKbMIG4iqw&width=1310&height=212"; + +async function sendPasswordResetEmail(email, firstName, resetToken) { + let dateTimeNow = new Date().toLocaleString(); let emailContent = ` - + ecoconnect Reset Password
- ecoconnect logo -

Greetings, ${firstName}!

-

We have received your request to reset the password.
Click the button below to do so.

- RESET PASSWORD -

If you have not made a request to reset the password, feel free to ignore this email.

-

Best regards,
ecoconnect administrators

+ ecoconnect logo +

+ Greetings, ${firstName}! +

+

+ We have received your request to reset the password.
Click the + button below to do so. +

+
+ RESET PASSWORD +

This reset portal is opened on ${dateTimeNow}, for 60 minutes.

+

+ If you have not made a request to reset the password, feel free to + ignore this email. +

+
+ +

+ Best regards,
ecoconnect administrators +

-
+
- ecoconnect logo + ecoconnect logo

· Connecting neighbourhoods together

+ `; await sendEmail( email, diff --git a/server/models/User.js b/server/models/User.js index 8fdeef0..284233e 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -43,13 +43,21 @@ module.exports = (sequelize) => { accountType: { type: DataTypes.TINYINT(2), allowNull: false, - defaultValue: 0 + defaultValue: 0, }, isArchived: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false, }, + resetPasswordToken: { + type: DataTypes.STRING(64), + allowNull: true, + }, + resetPasswordExpireTime: { + type: DataTypes.DATE, + allowNull: true, + }, }, { tableName: "users", diff --git a/server/routes/users.js b/server/routes/users.js index 349aa17..31e24e0 100644 --- a/server/routes/users.js +++ b/server/routes/users.js @@ -1,6 +1,6 @@ const express = require("express"); const yup = require("yup"); -const { Op } = require("sequelize"); +const { Op, Sequelize } = require("sequelize"); const { User } = require("../models"); const { validateToken } = require("../middlewares/auth"); const argon2 = require("argon2"); @@ -8,6 +8,10 @@ const router = express.Router(); const { sign } = require("jsonwebtoken"); const multer = require("multer"); const sharp = require("sharp"); +const { sendPasswordResetEmail } = require("../connections/mailersend"); +const { + generatePasswordResetToken, +} = require("../security/generatePasswordResetToken"); require("dotenv").config(); @@ -333,4 +337,113 @@ router.put( } ); +router.put("/request-reset-password/:email", async (req, res) => { + let email = req.params.email; + + try { + console.log(email); + let user = await User.findOne({ + where: { email: email }, + }); + + if (!user) { + res.sendStatus(404); + return; + } + + if (user.isArchived) { + res.status(400).json({ + message: `ERR_ACC_IS_ARCHIVED`, + }); + } else { + const token = await generatePasswordResetToken(); + let num = await User.update( + { + resetPasswordToken: token, + resetPasswordExpireTime: Date.now() + 3600000, + }, + { + where: { id: user.id }, + } + ); + + if (num == 1) { + res.json({ + message: "User was updated successfully.", + }); + } else { + res.status(400).json({ + message: `Something went wrong setting token for user with id ${id}.`, + }); + } + await sendPasswordResetEmail(user.email, user.firstName, token); + + res.status(200).json({ message: "Email sent successfully" }); + return; + } + } catch (error) { + // Silence. + } +}); + +router.post("/reset-password", async (req, res) => { + let token = req.body.token; + let newPassword = req.body.password; + + try { + resetPassword(token, newPassword).then(() => { + res.sendStatus(200); + }); + } catch (err) { + res.status(400).json({ errors: err.errors }); + } +}); + +router.get("/reset-password/:token", async (req, res) => { + let token = req.params.token; + try { + let user = await validateResetPasswordToken(token); + + if (!user) { + console.log("no"); + res.sendStatus(404); + return; + } else { + console.log("yes"); + res.sendStatus(200); + } + } catch { + res.status(401); + } +}); + +async function validateResetPasswordToken(token) { + const user = await User.findOne({ + where: { + resetPasswordToken: token, + resetPasswordExpireTime: { [Sequelize.Op.gt]: Date.now() }, + }, + }); + + if (!user) { + return undefined; + } else { + return user; + } +} + +async function resetPassword(token, newPassword) { + const user = await validateResetPasswordToken(token); + + if (!user) { + return undefined; + } + + const hashedPassword = await argon2.hash(newPassword); + user.password = hashedPassword; + user.resetPasswordToken = null; + user.resetPasswordExpires = null; + await user.save(); +} + module.exports = router; diff --git a/server/security/generatePasswordResetToken.js b/server/security/generatePasswordResetToken.js new file mode 100644 index 0000000..6351c62 --- /dev/null +++ b/server/security/generatePasswordResetToken.js @@ -0,0 +1,16 @@ +const crypto = require("crypto"); + +function generatePasswordResetToken() { + return new Promise((resolve, reject) => { + crypto.randomBytes(32, (err, buffer) => { + if (err) { + reject(err); + } else { + const token = buffer.toString("hex"); + resolve(token); + } + }); + }); +} + +module.exports = { generatePasswordResetToken };