From 456f3ae658551aad2ff3bdcc3329ca78dda243bf Mon Sep 17 00:00:00 2001 From: Merlte Date: Mon, 22 Jul 2024 20:24:32 +1000 Subject: [PATCH 01/11] feat: backend functions (WIP) --- backend/database/user.js | 95 +++++++++++++++++++++++++++++++++++++++- backend/index.js | 2 + 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/backend/database/user.js b/backend/database/user.js index 8ca2660ee..cf58a5cc0 100644 --- a/backend/database/user.js +++ b/backend/database/user.js @@ -21,8 +21,8 @@ const userInfo = async (req, res) => { try { const query = ` - SELECT email, zid, firstname, lastname, role, year, mentor - FROM users + SELECT email, zid, firstname, lastname, role, year, mentor + FROM users WHERE zid = $1 `; const values = [zid]; @@ -45,6 +45,97 @@ const userInfo = async (req, res) => { } }; +// NOTE: re-using admin's /invite implementation and database +const forgotPassword = async (req, res) => { + const email = req.email; + + // Check if email is valid + const data = await db.query(`SELECT * FROM users WHERE email = $1;`, [email]); + const arr = data.rows; + if (arr.length === 0) { + return res.status(400).json({ message: `Email doesn't exist. Please register first.` }); + } + + // NOTE: re-using database because of tight deadlines + const token = base64.encode(uuidv4() + email); + const date = new Date().toLocaleString(); + const query = ` + INSERT INTO invitation_tokens (token, used, created_at) + VALUES ($1, $2, $3) + `; + + const params = [token, false, date]; + await db.query(query, params); + sendForgotPasswordEmail(email, token); + res.status(200).send("Reset password email sent successfully"); +}; + +// Function to send forgot password email +const sendForgotPasswordEmail = (email, token) => { + const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: process.env.EMAIL, + pass: process.env.EMAIL_PASSWORD, + }, + }); + + // TODO: Change link when deployed + const link = `https://empowerment.unswwit.com/user/reset-password?token=${token}`; + + const mailOptions = { + from: process.env.EMAIL, + to: email, + subject: "UNSW WIT Empowerment Program Reset Password", + text: `Hi there!,\n\nWe received a request to reset the password for ${email}.\n\nClick this link to enter a new password: ${link}.\n\nIf you did NOT expect to receive this message, please notify the admins immediately!.`, + }; + + transporter.sendMail(mailOptions, (error, info) => { + if (error) { + console.error("Error sending email:", error); + } else { + console.log("Email sent:", info.response); + } + }); +}; + +const resetPassword = async (req, res) => { + const { email, password } = req.body; + const token = req.query.token; + + if (token === "undefined") { + return res.status(400).send({ message: "Invalid/expired token" }); + } + + // Check valid reset token (using invitation_tokens database) + const tokenResult = await db.query(`SELECT * FROM invitation_tokens WHERE token = $1 AND used = $2`, [token, false]); + + // console.log(tokenResult); + if (tokenResult.rows.length === 0) { + return res.status(400).send({ message: "Invalid/expired token" }); + } + + const decoded = base64.decode(token); + + if (!decoded.endsWith(email)) { + return res.status(400).send({ message: "Invalid token/email" }); + } + + // Update password + const hashedPassword = await bcrypt.hash(password, 10); + const params = [email, hashedPassword]; + const q = "UPDATE users SET password = $2 WHERE email = $1"; + await db.query(q, params); + + // Mark the token as used + await db.query("UPDATE invitation_tokens SET used = $1 WHERE token = $2", [true, token]); + + return res.status(200).json({ message: "Password reset successfully! Please proceed to login." }); +}; + + module.exports = { userInfo, + forgotPassword, + resetPassword, }; diff --git a/backend/index.js b/backend/index.js index 7e06a6470..c6a5bdd4b 100644 --- a/backend/index.js +++ b/backend/index.js @@ -19,6 +19,8 @@ app.use(cors()); // -------- User --------// app.post("/user/register", auth.registerUser); app.post("/user/login", auth.loginUser); +app.post("/user/forgot-password", user.forgotPassword); +app.post("/user/reset-password", user.resetPassword); app.get("/user/profile", user.userInfo); // -------- Mentee --------// From 1d7874768ed33e24d8ad5ae74fdb458bce8cb5b4 Mon Sep 17 00:00:00 2001 From: Merlte Date: Mon, 22 Jul 2024 20:33:32 +1000 Subject: [PATCH 02/11] feat: forgot password button (WIP) --- frontend/pages/user/login.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/pages/user/login.tsx b/frontend/pages/user/login.tsx index cc785b7e8..f35efc515 100644 --- a/frontend/pages/user/login.tsx +++ b/frontend/pages/user/login.tsx @@ -87,6 +87,9 @@ export default function Login() { placeholder="Enter your password" /> +
+ Forgot your password? +

{error &&

{error}

} diff --git a/frontend/styles/User.module.css b/frontend/styles/User.module.css index c1bb7cb23..0f8f2870a 100644 --- a/frontend/styles/User.module.css +++ b/frontend/styles/User.module.css @@ -259,6 +259,15 @@ color: red; } +/* --- forgot password style --- */ +.forgot_password_container { + text-align: right; +} +.forgot_password_container p{ + cursor: pointer; + font-size: small; +} + /* -- Admin Tools --- */ /* Form Style */ .form { From b90bd5ffc5b06c97a26270f1c8db044f33e77421 Mon Sep 17 00:00:00 2001 From: Bianca Zhang Date: Sat, 27 Jul 2024 15:17:29 +1000 Subject: [PATCH 04/11] forgot password modal --- frontend/pages/api/user.ts | 19 ++++++ frontend/pages/user/fogot-password.tsx | 88 ++++++++++++++++++++++++++ frontend/pages/user/login.tsx | 4 +- 3 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 frontend/pages/user/fogot-password.tsx diff --git a/frontend/pages/api/user.ts b/frontend/pages/api/user.ts index 3aaeeaaa8..4992ebef7 100644 --- a/frontend/pages/api/user.ts +++ b/frontend/pages/api/user.ts @@ -42,6 +42,25 @@ export async function doLogin( } } +export async function doForgotPassword( + req: any, + setMessage: Dispatch>, + setError: Dispatch> +) { + const res = await fetch(`${apiUrl}/user/forgot-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(req), + }); + const data = await res.json(); + + if (res.ok) { + setMessage(data.message); + } else { + setError(data.message); + } +} + export async function getUserProfile() { const res = await fetch(`${apiUrl}/user/profile`, { method: 'GET', diff --git a/frontend/pages/user/fogot-password.tsx b/frontend/pages/user/fogot-password.tsx new file mode 100644 index 000000000..8a6221fd3 --- /dev/null +++ b/frontend/pages/user/fogot-password.tsx @@ -0,0 +1,88 @@ +import React, { FormEvent, useEffect, useState } from 'react'; +import { Montserrat } from 'next/font/google'; + +import styles from '../../styles/User.module.css'; +import { doForgotPassword } from '../api/user'; +// import { useRouter } from 'next/router'; +import LoadingOverlay from '../../components/LoadingOverlay'; + +const montserrat = Montserrat({ subsets: ['latin'] }); + +export default function Forgot_password() { + // const router = useRouter(); + + const [isLoading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [message, setMessage] = useState(null); + + const handleForgotPassword = (e: FormEvent) => { + setLoading(true); + e.preventDefault(); + + const reqData = { + email: e.currentTarget.userEmail.value, + } + + doForgotPassword(reqData, setMessage, setError).finally(() => setLoading(false)); + }; + + const initForgotPassword = async () => { + setLoading(false); + }; + + useEffect(() => { + setLoading(true); + initForgotPassword(); + }, []); + + return ( +
+
+
+ +
+
+

Reset your password

+
+ {/* guide: https://mattermost.com/blog/add-google-and-github-login-to-next-js-app-with-nextauth/ */} +
+ {/*
+
+ OR +
+
*/} +
+
+ + +
+
+ {error &&

{error}

} + {message &&

{message}

} + +
+
+
+
+ woman engineers +
+
+
+
+ + +
+
+ ); +} diff --git a/frontend/pages/user/login.tsx b/frontend/pages/user/login.tsx index 46fdcddfc..a1ae695aa 100644 --- a/frontend/pages/user/login.tsx +++ b/frontend/pages/user/login.tsx @@ -35,11 +35,11 @@ export default function Login() { }; const handleResetPassword = () => { - router.push('/user/reset-password'); + router.push('/user/forgot-password'); }; useEffect(() => { - setLoading(false); + setLoading(true); initLogin(); }, []); From 033805110288ebe0622ca9bf6f499e9a4a76e4f7 Mon Sep 17 00:00:00 2001 From: Bianca Zhang Date: Sat, 27 Jul 2024 15:39:56 +1000 Subject: [PATCH 05/11] forgot password modal with backend --- frontend/pages/user/fogot-password.tsx | 2 +- frontend/pages/user/login.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/pages/user/fogot-password.tsx b/frontend/pages/user/fogot-password.tsx index 8a6221fd3..ef4afba2d 100644 --- a/frontend/pages/user/fogot-password.tsx +++ b/frontend/pages/user/fogot-password.tsx @@ -51,7 +51,7 @@ export default function Forgot_password() { OR
*/} -
+
{ - setLoading(true); + setLoading(false); initLogin(); }, []); From 94090b653cd4b75fde51d19565042082ae3c55a0 Mon Sep 17 00:00:00 2001 From: Bianca Zhang Date: Sat, 27 Jul 2024 15:40:33 +1000 Subject: [PATCH 06/11] merge in helen backend code --- backend/database/user.js | 54 ++++++++++++++++++++++++++++++++++++++++ backend/index.js | 1 + 2 files changed, 55 insertions(+) diff --git a/backend/database/user.js b/backend/database/user.js index 8ca2660ee..5ad0392c0 100644 --- a/backend/database/user.js +++ b/backend/database/user.js @@ -45,6 +45,60 @@ const userInfo = async (req, res) => { } }; +const forgotPassword = async (req, res) => { + const email = req.email; + + // Check if email is valid + const data = await db.query(`SELECT * FROM users WHERE email = $1;`, [email]); + const arr = data.rows; + if (arr.length === 0) { + return res.status(400).json({ message: `Email doesn't exist. Please register first.` }); + } + + // NOTE: re-using database because of tight deadlines + const token = base64.encode(uuidv4() + email); + const date = new Date().toLocaleString(); + const query = ` + INSERT INTO invitation_tokens (token, used, created_at) + VALUES ($1, $2, $3) + `; + + const params = [token, false, date]; + await db.query(query, params); + sendForgotPasswordEmail(email, token); + res.status(200).send("Reset password email sent successfully"); +}; + +// Function to send forgot password email +const sendForgotPasswordEmail = (email, token) => { + const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: process.env.EMAIL, + pass: process.env.EMAIL_PASSWORD, + }, + }); + + // TODO: Change link when deployed + const link = `https://empowerment.unswwit.com/user/reset-password?token=${token}`; + + const mailOptions = { + from: process.env.EMAIL, + to: email, + subject: "UNSW WIT Empowerment Program Reset Password", + text: `Hi there!,\n\nWe received a request to reset the password for ${email}.\n\nClick this link to enter a new password: ${link}.\n\nIf you did NOT expect to receive this message, please notify the admins immediately!.`, + }; + + transporter.sendMail(mailOptions, (error, info) => { + if (error) { + console.error("Error sending email:", error); + } else { + console.log("Email sent:", info.response); + } + }); +}; + module.exports = { userInfo, + forgotPassword, }; diff --git a/backend/index.js b/backend/index.js index 7e06a6470..f9177dfc6 100644 --- a/backend/index.js +++ b/backend/index.js @@ -20,6 +20,7 @@ app.use(cors()); app.post("/user/register", auth.registerUser); app.post("/user/login", auth.loginUser); app.get("/user/profile", user.userInfo); +app.post("/user/forgot-password", user.forgotPassword); // -------- Mentee --------// app.post("/mentee/request-hours", mentee.requestHours); From 22cd0f5892fe7798fdd3e78a5da5968b634ff709 Mon Sep 17 00:00:00 2001 From: Bianca Zhang Date: Sat, 27 Jul 2024 23:24:44 +1000 Subject: [PATCH 07/11] frontend working, need to test with backend --- .../pages/user/{fogot-password.tsx => forgot-password.tsx} | 4 +--- frontend/pages/user/login.tsx | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) rename frontend/pages/user/{fogot-password.tsx => forgot-password.tsx} (95%) diff --git a/frontend/pages/user/fogot-password.tsx b/frontend/pages/user/forgot-password.tsx similarity index 95% rename from frontend/pages/user/fogot-password.tsx rename to frontend/pages/user/forgot-password.tsx index ef4afba2d..d1108450f 100644 --- a/frontend/pages/user/fogot-password.tsx +++ b/frontend/pages/user/forgot-password.tsx @@ -3,13 +3,11 @@ import { Montserrat } from 'next/font/google'; import styles from '../../styles/User.module.css'; import { doForgotPassword } from '../api/user'; -// import { useRouter } from 'next/router'; import LoadingOverlay from '../../components/LoadingOverlay'; const montserrat = Montserrat({ subsets: ['latin'] }); export default function Forgot_password() { - // const router = useRouter(); const [isLoading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -42,7 +40,7 @@ export default function Forgot_password() {
-

Reset your password

+

Forgot password

{/* guide: https://mattermost.com/blog/add-google-and-github-login-to-next-js-app-with-nextauth/ */}
diff --git a/frontend/pages/user/login.tsx b/frontend/pages/user/login.tsx index 77d494787..a1ae695aa 100644 --- a/frontend/pages/user/login.tsx +++ b/frontend/pages/user/login.tsx @@ -39,7 +39,7 @@ export default function Login() { }; useEffect(() => { - setLoading(false); + setLoading(true); initLogin(); }, []); From d67a2d692a5948a229764205553d7538bf6be624 Mon Sep 17 00:00:00 2001 From: Merlte Date: Mon, 29 Jul 2024 21:50:12 +1000 Subject: [PATCH 08/11] fix: bugs --- backend/database/user.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/backend/database/user.js b/backend/database/user.js index cf58a5cc0..99f702b51 100644 --- a/backend/database/user.js +++ b/backend/database/user.js @@ -1,10 +1,12 @@ // User (both Mentee and Admin) API Endpoints require("dotenv").config(); +const nodemailer = require("nodemailer"); const db = require("./db"); const { verifyToken } = require("./auth"); const { checkUserExists } = require("./helper"); +const bcrypt = require("bcrypt"); // Get user info const userInfo = async (req, res) => { @@ -47,7 +49,7 @@ const userInfo = async (req, res) => { // NOTE: re-using admin's /invite implementation and database const forgotPassword = async (req, res) => { - const email = req.email; + const email = req.body.email; // Check if email is valid const data = await db.query(`SELECT * FROM users WHERE email = $1;`, [email]); @@ -56,8 +58,12 @@ const forgotPassword = async (req, res) => { return res.status(400).json({ message: `Email doesn't exist. Please register first.` }); } + const zid = await db.query(`SELECT zid FROM users WHERE email = $1;`, [email]); + // NOTE: re-using database because of tight deadlines - const token = base64.encode(uuidv4() + email); + // token stores max size 32. Chose to use zid over email to fit length. + const token = btoa(Math.floor(Math.random() * 99999999999999 + 1) + zid.rows[0].zid); + const date = new Date().toLocaleString(); const query = ` INSERT INTO invitation_tokens (token, used, created_at) @@ -109,15 +115,17 @@ const resetPassword = async (req, res) => { // Check valid reset token (using invitation_tokens database) const tokenResult = await db.query(`SELECT * FROM invitation_tokens WHERE token = $1 AND used = $2`, [token, false]); + const check = await db.query(`SELECT * FROM invitation_tokens`); - // console.log(tokenResult); if (tokenResult.rows.length === 0) { return res.status(400).send({ message: "Invalid/expired token" }); } - const decoded = base64.decode(token); + const decoded = atob(token); + + const zid = await db.query(`SELECT zid FROM users WHERE email = $1;`, [email]); - if (!decoded.endsWith(email)) { + if (!decoded.endsWith(zid.rows[0].zid)) { return res.status(400).send({ message: "Invalid token/email" }); } From 6b94b7293e64962fe621ef1843c88741bae0633f Mon Sep 17 00:00:00 2001 From: Merlte Date: Mon, 29 Jul 2024 21:53:05 +1000 Subject: [PATCH 09/11] remove: frontend button (for merge with frontend) --- frontend/pages/user/login.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/pages/user/login.tsx b/frontend/pages/user/login.tsx index f35efc515..cc785b7e8 100644 --- a/frontend/pages/user/login.tsx +++ b/frontend/pages/user/login.tsx @@ -87,9 +87,6 @@ export default function Login() { placeholder="Enter your password" />
-
{error &&

{error}

}