Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix invite and reset passwd email service #482

Merged
merged 7 commits into from
Jan 13, 2025
Merged
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions .env
Original file line number Diff line number Diff line change
@@ -11,9 +11,9 @@ DEV_DB_PORT=5432
# JWT Secret Key
JWT_SECRET="NKrbO2lpCsOpVAlqAPsjZ0tZXzIoKru7gAmYZ7XlHn0=qqwqeq"

EMAIL=autobluewave@gmail.com
EMAIL=bluewaveguidefox@gmail.com
EMAIL_PASSWORD=passwor
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=465
APP_PASSWORD=password
APP_PASSWORD=ukzwakckupguegiw
EMAIL_ENABLE=false
9 changes: 8 additions & 1 deletion backend/.env
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ DEV_DB_NAME=onboarding_db
DEV_DB_HOST=db
DEV_DB_PORT=5432

EMAIL_ENABLE=false
EMAIL_ENABLE=true

# JWT Secret Key
JWT_SECRET="NKrbO2lpCsOpVAlqAPsjZ0tZXzIoKru7gAmYZ7XlHn0=qqwqeq"
@@ -19,6 +19,13 @@ TEST_DB_NAME=onboarding_db_test
TEST_DB_HOST=localhost
TEST_DB_PORT=5432

[email protected]
EMAIL_PASSWORD=passwor
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=465
APP_PASSWORD=ukzwakckupguegiw
EMAIL_ENABLE=true

ENABLE_IP_CHECK=false
# Allowed IP range for the API "baseIp/rangeStart-rangeEnd" (e.g. 192.168.1/1-255) separated by comma
ALLOWED_IP_RANGE=11.22.33/10-200, 192.168.65/1-255
1 change: 0 additions & 1 deletion backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions backend/src/controllers/auth.controller.js
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ const sequelize = db.sequelize;
const { generateToken, verifyToken } = require('../utils/jwt.helper');
const crypto = require('crypto');
const { TOKEN_LIFESPAN } = require('../utils/constants.helper');
const { sendSignupEmail, sendPasswordResetEmail, findUserByEmail } = require('../service/email.service');
const { sendPasswordResetEmail, findUserByEmail } = require('../service/email.service');
const settings = require('../../config/settings');
const { decode } = require('../utils/auth.helper');

@@ -81,8 +81,6 @@ const register = async (req, res) => {

await Token.create({ token, userId: newUser.id, type: 'auth' });

await sendSignupEmail(newUser.email, newUser.name);

res.status(201).json({
user: {
id: newUser.id,
@@ -160,6 +158,8 @@ const forgetPassword = async (req, res) => {
const user = await findUserByEmail(email);
if (!user) return res.status(400).json({ error: 'User not found' });

await Token.destroy({ where: { userId: user.id, type: 'reset' } });

const resetToken = crypto.randomBytes(32).toString('hex');
const hash = await bcrypt.hash(resetToken, 10);
const expiresAt = new Date(Date.now() + TOKEN_LIFESPAN);
20 changes: 16 additions & 4 deletions backend/src/service/email.service.js
Original file line number Diff line number Diff line change
@@ -2,18 +2,21 @@ const nodemailer = require("nodemailer");
const handlebars = require("handlebars");
const fs = require("fs");
const path = require("path");
const { API_BASE_URL } = require("../utils/constants.helper");
const { API_BASE_URL, FRONTEND_URL } = require('../utils/constants.helper');
const db = require("../models");
const User = db.User;

const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST || "localhost",
host: process.env.EMAIL_HOST || 'localhost',
port: process.env.EMAIL_PORT || 465,
secure: true,
auth: {
user: process.env.EMAIL,
pass: process.env.APP_PASSWORD,
},
tls: {
rejectUnauthorized: process.env.NODE_ENV === 'production',
},
});

const readHTMLFile = (filePath) => {
@@ -59,16 +62,25 @@ const sendSignupEmail = async (email, name) => {
};

const sendPasswordResetEmail = async (email, name, resetToken) => {
const resetLink = `${API_BASE_URL}reset-password?token=${resetToken}`;
await sendEmail(email, "Password Reset", "resetPassword", {
const resetLink = `${FRONTEND_URL}set-new-password?token=${resetToken}`;
await sendEmail(email, 'Reset your password for Guidefox', 'resetPassword', {
name,
resetLink,
});
};

const sendInviteEmail = async (email) => {
const inviteLink = FRONTEND_URL;
await sendEmail(email, 'You’re invited to join Guidefox!', 'invite', {
inviteLink,
});
};


module.exports = {
sendSignupEmail,
sendPasswordResetEmail,
sendInviteEmail,
findUserByEmail,
transporter,
};
3 changes: 3 additions & 0 deletions backend/src/service/invite.service.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const settings = require("../../config/settings");
const { sendInviteEmail } = require("./email.service");
const db = require("../models");
const Invite = db.Invite;
const User = db.User;
@@ -22,13 +23,15 @@ class InviteService {
invitedBy: userId,
role: settings.user.role[role],
})
sendInviteEmail(invitedEmail);
}
else {
await Invite.create({
invitedBy: userId,
invitedEmail: invitedEmail,
role: settings.user.role[role],
});
sendInviteEmail(invitedEmail);
}
}
catch (err) {
7 changes: 7 additions & 0 deletions backend/src/templates/invite.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<p>Hello,</p>
<p>You’ve been invited to join Guidefox - a platform designed to help you build better user experiences.</p>
<p>To get started, simply click the link below to create your account:</p>
<p><a href="{{inviteLink}}">Join Guidefox</a></p>
<p>Once you’ve created your account, you’ll be able to log in and start exploring all the features we’ve prepared for you.</p>
<p>We’re excited to have you with us and can’t wait for you to experience Guidefox!</p>
<p>--<br>The Guidefox Team</p>
11 changes: 7 additions & 4 deletions backend/src/templates/resetPassword.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<h1>Password Reset</h1>
<p>Hi {{name}},</p>
<p>Please use the following link to reset your password:</p>
<a href="{{resetLink}}">Reset Password</a>
<p>Hello {{name}},</p>
<p>We received a request to reset your password for your Guidefox account. If you didn’t request a password reset, you can safely ignore this email.</p>
<p>To reset your password, click the link below:</p>
<a href="{{resetLink}}">Reset My Password</a>
<p>Thank you for using Guidefox!</p>
<p>--<br>The Guidefox Team</p>

3 changes: 2 additions & 1 deletion backend/src/utils/constants.helper.js
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ module.exports = Object.freeze({
TOKEN_LIFESPAN: 3600 * 1000,
// API_BASE_URL: 'https://onboarding-demo.bluewavelabs.ca/api/',
API_BASE_URL: 'localhost:3000/api/',
FRONTEND_URL: 'https://onboarding-demo.bluewavelabs.ca/',
MAX_FILE_SIZE: 3 * 1024 * 1024,
ROLE: {
ADMIN: '1',
@@ -13,5 +14,5 @@ module.exports = Object.freeze({
ORG_NAME_REGEX: /^[a-zA-Z0-9\s\-_&.]+$/,
URL_PROTOCOL_REGEX: /^(https?:\/\/)/,
URL_DOMAIN_REGEX: /^https?:\/\/([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/,
});
});

15 changes: 10 additions & 5 deletions frontend/src/scenes/login/SetNewPassword.jsx
Original file line number Diff line number Diff line change
@@ -5,13 +5,17 @@ import CustomTextField from "@components/TextFieldComponents/CustomTextField/Cus
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import CircularProgress from "@mui/material/CircularProgress";
import { resetPassword } from "../../services/loginServices";
import { useNavigate } from "react-router-dom";
import { useNavigate, useLocation } from "react-router-dom";
import * as Yup from "yup";
import { Formik, Form } from "formik";

function SetNewPasswordPage({ email = "[email protected]" }) {
const [serverErrors, setServerErrors] = useState([]);
const navigate = useNavigate();
const location = useLocation();

const queryParams = new URLSearchParams(location.search);
const token = queryParams.get("token");

const validationSchema = Yup.object({
password: Yup.string()
@@ -35,17 +39,18 @@ function SetNewPasswordPage({ email = "[email protected]" }) {
validationSchema={validationSchema}
validateOnChange={false}
validateOnBlur={true}
onSubmit={async (values, { setSubmitting }) => {
onSubmit={async (values, { setSubmitting, resetForm }) => {
setServerErrors([]);
try {
const response = await resetPassword({
email: email,
password: values.password,
token: token,
newPassword: values.password,
});
resetForm();
navigate("/reset-password");
} catch (error) {
console.error("Password Reset failed:", error);
console.log("error", error);
if (error.response?.data?.errors) {
setServerErrors(error.response?.data?.errors);
} else if (error.response?.data?.error) {
@@ -70,7 +75,7 @@ function SetNewPasswordPage({ email = "[email protected]" }) {
<Form className={styles["login-container"]}>
<h2 style={{ marginBottom: "0px" }}>Set new Password</h2>
<h3>
Your new password must be different to previously used passwords.
Your new password must be different from previously used passwords.
</h3>
<div className={styles["form-group"]}>
<CustomTextField
2 changes: 1 addition & 1 deletion frontend/src/services/settingServices.js
Original file line number Diff line number Diff line change
@@ -53,7 +53,7 @@ export const removeTeamMember = async (memberId) => {

export const inviteMember = async (inputs) => {
try {
const response = await apiClient.post('/team/invite', { invitedEmail: inputs.email, role: inputs.role });
const response = await apiClient.post('team/invite', { invitedEmail: inputs.email, role: inputs.role });
return response;
} catch (error) {
console.error('Error inviting member: ', error.message);

Unchanged files with check annotations Beta

expect(screen.getByText("Set new Password")).toBeTruthy();
expect(
screen.getByText(

Check failure on line 30 in frontend/src/tests/scenes/login/SetNewPassword.test.jsx

GitHub Actions / build (22.x)

src/tests/scenes/login/SetNewPassword.test.jsx > SetNewPasswordPage > renders the set new password reset page

TestingLibraryElementError: Unable to find an element with the text: Your new password must be different to previously used passwords.. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <div class="login-body" > <form action="#" class="login-container" > <h2 style="margin-bottom: 0px;" > Set new Password </h2> <h3> Your new password must be different from previously used passwords. </h3> <div class="form-group" > <div style="width: 100%;" > <div style="display: flex; gap: 0.5rem;" > <label class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-animated MuiFormLabel-colorPrimary MuiInputLabel-root MuiInputLabel-animated css-ks4zjq-MuiFormLabel-root-MuiInputLabel-root" > Password*: </label> </div> <div class="MuiFormControl-root MuiFormControl-fullWidth MuiTextField-root textField css-1k3tngy-MuiFormControl-root-MuiTextField-root" > <div class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-colorPrimary MuiInputBase-fullWidth MuiInputBase-formControl css-1blp12k-MuiInputBase-root-MuiOutlinedInput-root" style="height: 34px; padding-top: 0px; padding-bottom: 0px;" > <input aria-invalid="false" class="MuiInputBase-input MuiOutlinedInput-input css-16wblaj-MuiInputBase-input-MuiOutlinedInput-input" id="password" name="password" placeholder="Create your password" required="" rows="1" type="password" value="" /> <fieldset aria-hidden="true" class="MuiOutlinedInput-notchedOutline css-1ll44ll-MuiOutlinedInput-notchedOutline" > <legend class="css-w4cd9x" > <span aria-hidden="true" class="notranslate" > ​ </span> </legend> </fieldset> </div> </div> </div> </div> <div class="form-group" style="margin-bottom: 0px;" > <div style="width: 100%;" > <div style="display: flex; gap: 0.5rem;" > <label class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-animated MuiFormLabel-colorPrimary MuiInputLabel-root MuiInputLabel-animated css-ks4zjq-MuiFormLabel-root-MuiInputLabel-root" > Confirm Password*: </label> </div> <div class="MuiFormControl-root MuiFormControl-fullWidth MuiTextField-root textField css-1k3tngy-MuiFormControl-root-MuiTextField-root" > <div class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-colorPrimary MuiInputBase-fullWidth MuiInputBase-formControl css-1blp12k-MuiInputBase-root-MuiOutlinedInput-root" style="height: 34px; padding-top: 0px; padding-bottom: 0px;" > <input aria-invalid="false" class="MuiInputBase-input MuiOutlinedInput-input css-16wblaj-MuiInputBase-input-MuiOutlinedInput-input" id="confirmPassword" name="confirmPassword" placeholder="Confirm your password" required="" rows="1" type="passw
"Your new password must be different to previously used passwords."
)
).toBeTruthy();
fireEvent.click(resetButton);
});
expect(resetPassword).toHaveBeenCalledWith({

Check failure on line 64 in frontend/src/tests/scenes/login/SetNewPassword.test.jsx

GitHub Actions / build (22.x)

src/tests/scenes/login/SetNewPassword.test.jsx > SetNewPasswordPage > handles reset password success

AssertionError: expected "spy" to be called with arguments: [ { email: 'asdf@asdf.com', …(1) } ] Received: 1st spy call: Array [ Object { - "email": "asdf@asdf.com", - "password": "Test123!@", + "newPassword": "Test123!@", + "token": null, }, ] Number of calls: 1 ❯ src/tests/scenes/login/SetNewPassword.test.jsx:64:27
email: "asdf@asdf.com",
password: "Test123!@",
});
expect(screen.getByText("Internal Server Error")).toBeTruthy();
});
expect(resetPassword).toHaveBeenCalledWith({

Check failure on line 105 in frontend/src/tests/scenes/login/SetNewPassword.test.jsx

GitHub Actions / build (22.x)

src/tests/scenes/login/SetNewPassword.test.jsx > SetNewPasswordPage > handles reset password internal server error

AssertionError: expected "spy" to be called with arguments: [ { email: 'asdf@asdf.com', …(1) } ] Received: 1st spy call: Array [ Object { - "email": "asdf@asdf.com", - "password": "Test123!@", + "newPassword": "Test123!@", + "token": null, }, ] 2nd spy call: Array [ Object { - "email": "asdf@asdf.com", - "password": "Test123!@", + "newPassword": "Test123!@", + "token": null, }, ] Number of calls: 2 ❯ src/tests/scenes/login/SetNewPassword.test.jsx:105:27
email: "asdf@asdf.com",
password: "Test123!@",
});