Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 101 additions & 2 deletions backend/database/user.js
Original file line number Diff line number Diff line change
@@ -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) => {
Expand All @@ -21,8 +23,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];
Expand All @@ -45,6 +47,103 @@ const userInfo = async (req, res) => {
}
};

// NOTE: re-using admin's /invite implementation and database
const forgotPassword = async (req, res) => {
const email = req.body.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.` });
}

const zid = await db.query(`SELECT zid FROM users WHERE email = $1;`, [email]);

// NOTE: re-using database because of tight deadlines
// 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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In generating the token, I believe we should stick maybe to the same unique id (uuidv4)?


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]);
const check = await db.query(`SELECT * FROM invitation_tokens`);

if (tokenResult.rows.length === 0) {
return res.status(400).send({ message: "Invalid/expired token" });
}

const decoded = atob(token);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for here for tokens as well


const zid = await db.query(`SELECT zid FROM users WHERE email = $1;`, [email]);

if (!decoded.endsWith(zid.rows[0].zid)) {
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,
};
3 changes: 3 additions & 0 deletions backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ 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);
app.post("/user/forgot-password", user.forgotPassword);

// -------- Mentee --------//
app.post("/mentee/request-hours", mentee.requestHours);
Expand Down
13 changes: 0 additions & 13 deletions frontend/data/env.local

This file was deleted.

19 changes: 19 additions & 0 deletions frontend/pages/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,25 @@ export async function doLogin(
}
}

export async function doForgotPassword(
req: any,
setMessage: Dispatch<React.SetStateAction<string | null>>,
setError: Dispatch<React.SetStateAction<string | null>>
) {
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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally shouldn't update the frontend states directly inside of methods that handles request to backend

} else {
setError(data.message);
}
}

export async function getUserProfile() {
const res = await fetch(`${apiUrl}/user/profile`, {
method: 'GET',
Expand Down
86 changes: 86 additions & 0 deletions frontend/pages/user/forgot-password.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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 LoadingOverlay from '../../components/LoadingOverlay';

const montserrat = Montserrat({ subsets: ['latin'] });

export default function Forgot_password() {

const [isLoading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [message, setMessage] = useState<string | null>(null);

const handleForgotPassword = (e: FormEvent<HTMLFormElement>) => {
setLoading(true);
e.preventDefault();

const reqData = {
email: e.currentTarget.userEmail.value,
}

doForgotPassword(reqData, setMessage, setError).finally(() => setLoading(false));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of passing in the setters from useState, could define a method that takes in an argument of what to set for the targeted variable.

Example

const updateMessage = (message: string) => {
  setMessage(message)
}

However, better way to further handle this is probably to use .then() to check for response and use setMessage() to set the message based on said response.

};

const initForgotPassword = async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async is not utilised, can remove

setLoading(false);
};

useEffect(() => {
setLoading(true);
initForgotPassword();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This useEffect sets loading to true and to false immediately. Not sure why this was implemented.

}, []);

return (
<div className={styles.user}>
<main className={montserrat.className}>
<div className={styles.panel}>
<LoadingOverlay isLoading={isLoading} />
<div className={styles.left}>
<div className={styles.content}>
<h1>Forgot password</h1>
<div className={styles.auth}>
{/* guide: https://mattermost.com/blog/add-google-and-github-login-to-next-js-app-with-nextauth/ */}
</div>
{/* <div className={styles.dividerLabel}>
<hr />
OR
<hr />
</div> */}
<form method="POST" action="/user/forgot-password" onSubmit={handleForgotPassword}>
<div>
<label>Email</label>
<input
required
className={montserrat.className}
type="text"
id="userEmail"
name="userEmail"
placeholder="Enter your email"
pattern="[a-zA-Z0-9_\.]+@[a-zA-Z0-9]+(\.[a-zA-Z]+)+$"
title="name@example.com"
/>
</div>
<hr />
{error && <p className={styles.error}>{error}</p>}
{message && <p>{message}</p>}
<button className={montserrat.className} type="submit">
Submit
</button>
</form>
</div>
</div>
<div className={styles.right}>
<img src="/login/image.png" alt="woman engineers" />
</div>
</div>
</main>
<div className={styles.bg}>
<img className={styles.decor1} src="/login/bottom-left.svg" />
<img className={styles.decor2} src="/login/top-right.svg" />
</div>
</div>
);
}
5 changes: 5 additions & 0 deletions frontend/pages/user/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export default function Login() {
setLoading(false);
};

const handleResetPassword = () => {
router.push('/user/forgot-password');
};

useEffect(() => {
setLoading(true);
initLogin();
Expand Down Expand Up @@ -89,6 +93,7 @@ export default function Login() {
</div>
<hr />
{error && <p className={styles.error}>{error}</p>}
<div className={styles.forgot_password_container}><p onClick={handleResetPassword}><u>Forgot your password?</u></p></div>
<button className={montserrat.className} type="submit">
Log in
</button>
Expand Down
9 changes: 9 additions & 0 deletions frontend/styles/User.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down