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
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
4 changes: 2 additions & 2 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 11 additions & 1 deletion backend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ DEV_DB_NAME=onboarding_db
DEV_DB_HOST=db
DEV_DB_PORT=5432

EMAIL_ENABLE=false
EMAIL_ENABLE=true
thomastepi marked this conversation as resolved.
Show resolved Hide resolved

# JWT Secret Key
JWT_SECRET="NKrbO2lpCsOpVAlqAPsjZ0tZXzIoKru7gAmYZ7XlHn0=qqwqeq"
Expand All @@ -19,8 +19,18 @@ 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
erenfn marked this conversation as resolved.
Show resolved Hide resolved
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
# Allowed IP addresses for the API separated by comma
ALLOWED_IPS=127.0.0.1, 11.22.33.44, 11.22.33.45, 11.22.33.46, 192.168.65.1

# FRONTEND_URL=https://onboarding-demo.bluewavelabs.ca/
FRONTEND_URL=http://localhost:4173/
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
Expand Up @@ -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');

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions backend/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const cors = require("cors");
const helmet = require("helmet");
const dotenv = require("dotenv");
const bodyParser = require("body-parser");
const compression = require("compression");
// const compression = require("compression");
const jsonErrorMiddleware = require("./middleware/jsonError.middleware");
const fileSizeValidator = require("./middleware/fileSizeValidator.middleware");
const { MAX_FILE_SIZE } = require("./utils/constants.helper");
Expand Down Expand Up @@ -32,7 +32,7 @@ app.use(cors());
app.options('*', cors()); // this is for preflight requests
app.use(helmet());
app.use(bodyParser.json({ limit: MAX_FILE_SIZE }));
app.use(compression());
// app.use(compression());
app.use(jsonErrorMiddleware);
if (process.env.ENABLE_IP_CHECK === 'true') {
app.use(ipFilter);
Expand Down
20 changes: 16 additions & 4 deletions backend/src/service/email.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
erenfn marked this conversation as resolved.
Show resolved Hide resolved
});

const readHTMLFile = (filePath) => {
Expand Down Expand Up @@ -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,
});
thomastepi marked this conversation as resolved.
Show resolved Hide resolved
};

const sendInviteEmail = async (email) => {
const inviteLink = FRONTEND_URL;
await sendEmail(email, 'You’re invited to join Guidefox!', 'invite', {
inviteLink,
});
};
erenfn marked this conversation as resolved.
Show resolved Hide resolved


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;
Expand All @@ -22,13 +23,15 @@ class InviteService {
invitedBy: userId,
role: settings.user.role[role],
})
sendInviteEmail(invitedEmail);
thomastepi marked this conversation as resolved.
Show resolved Hide resolved
}
else {
await Invite.create({
invitedBy: userId,
invitedEmail: invitedEmail,
role: settings.user.role[role],
});
sendInviteEmail(invitedEmail);
}
}
catch (err) {
Expand Down
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>

2 changes: 1 addition & 1 deletion backend/src/test/unit/services/email.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ describe("Test email service", () => {
expect(params).to.deep.equal({
from: process.env.EMAIL,
to: user().build().email,
subject: "Password Reset",
subject: "Reset your password for Guidefox",
html: "html",
});
Comment on lines +95 to 97
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Mom's spaghetti moment: Let's beef up this reset password test! 🍝

The test verifies the basic email parameters but misses some crucial checks:

  1. The reset link construction using FRONTEND_URL
  2. The template variables passed to handlebars

Here's how we can enhance it:

it("sendPasswordResetEmail - should send the email with the reset password content", async () => {
  process.env.EMAIL_ENABLE = "true";
  process.env.FRONTEND_URL = "https://guidefox.com";
  const token = "test-token";
  
  const compileMock = sinon.stub().returns("compiled-html");
  handlebars.compile.returns(compileMock);
  
  await service.sendPasswordResetEmail(
    user().build().email,
    user().build().name,
    token
  );
  
  expect(compileMock.calledWith({
    name: user().build().name,
    resetLink: `${process.env.FRONTEND_URL}/reset-password?token=${token}`
  })).to.be.true;
  
  const params = transporterMock.getCall(0).args[0];
  expect(params).to.deep.equal({
    from: process.env.EMAIL,
    to: user().build().email,
    subject: "Reset your password for Guidefox",
    html: "compiled-html",
  });
});

});
Expand Down
33 changes: 17 additions & 16 deletions backend/src/utils/constants.helper.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
require('dotenv').config();

module.exports = Object.freeze({
JWT_EXPIRES_IN_1H: '1h',
JWT_EXPIRES_IN_20M: '20m',
TOKEN_LIFESPAN: 3600 * 1000,
// API_BASE_URL: 'https://onboarding-demo.bluewavelabs.ca/api/',
API_BASE_URL: 'localhost:3000/api/',
MAX_FILE_SIZE: 3 * 1024 * 1024,
ROLE: {
ADMIN: '1',
MEMBER: '2'
},
MAX_ORG_NAME_LENGTH: 100,
ORG_NAME_REGEX: /^[a-zA-Z0-9\s\-_&.]+$/,
URL_PROTOCOL_REGEX: /^(https?:\/\/)/,
URL_DOMAIN_REGEX: /^https?:\/\/([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/,
});

JWT_EXPIRES_IN_1H: '1h',
JWT_EXPIRES_IN_20M: '20m',
TOKEN_LIFESPAN: 3600 * 1000,
API_BASE_URL: process.env.API_BASE_URL || 'localhost:3000/api/',
FRONTEND_URL: process.env.FRONTEND_URL,
MAX_FILE_SIZE: 3 * 1024 * 1024,
ROLE: {
ADMIN: '1',
MEMBER: '2'
},
MAX_ORG_NAME_LENGTH: 100,
ORG_NAME_REGEX: /^[a-zA-Z0-9\s\-_&.]+$/,
URL_PROTOCOL_REGEX: /^(https?:\/\/)/,
URL_DOMAIN_REGEX: /^https?:\/\/([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/,
});
46 changes: 23 additions & 23 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,29 +59,29 @@ services:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
frontend:
build: ./frontend
volumes:
- ./frontend:/app
- /app/node_modules
ports:
- "4173:4173"
develop:
watch:
- action: sync
path: ./frontend
target: /app
ignore:
- node_modules/
environment:
- NODE_ENV=${NODE_ENV:-development}
command: >
bash -c "
if [ \"$NODE_ENV\" = \"development\" ]; then
npm run dev;
elif [ \"$NODE_ENV\" != \"development\" ]; then
npm run build && npm run preview;
fi"
# frontend:
# build: ./frontend
# volumes:
# - ./frontend:/app
# - /app/node_modules
# ports:
# - "4173:4173"
# develop:
# watch:
# - action: sync
# path: ./frontend
# target: /app
# ignore:
# - node_modules/
# environment:
# - NODE_ENV=${NODE_ENV:-development}
# command: >
# bash -c "
# if [ \"$NODE_ENV\" = \"development\" ]; then
# npm run dev;
# elif [ \"$NODE_ENV\" != \"development\" ]; then
# npm run build && npm run preview;
# fi"
mailhog:
image: mailhog/mailhog
ports:
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/assets/theme.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ export const lightTheme = createTheme({
},
MuiOutlinedInput: {
styleOverrides: {
input: {
boxSizing: 'border-box'
},
root: {
borderRadius: "8px",
"&:hover .MuiOutlinedInput-notchedOutline": {
Expand Down Expand Up @@ -138,6 +141,9 @@ export const darkTheme = createTheme({
},
MuiOutlinedInput: {
styleOverrides: {
input: {
boxSizing: 'border-box'
},
root: {
borderRadius: "8px",
"&:hover .MuiOutlinedInput-notchedOutline": {
Expand Down
13 changes: 12 additions & 1 deletion frontend/src/components/CustomLink/CustomLink.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,24 @@ import './CustomLinkStyles.css';

const CustomLink = ({
text = 'Default Text',
url = '#',
url = '',
className = '',
underline = 'none',
onClick,
}) => {
const handleClick = (event) => {
if (onClick) {
event.preventDefault();
onClick(event);
}
};

return (
<Link
href={url}
className={`custom-link ${className}`}
underline={underline}
onClick={handleClick}
>
{text}
</Link>
Expand All @@ -25,5 +34,7 @@ CustomLink.propTypes = {
url: PropTypes.string,
className: PropTypes.string,
underline: PropTypes.oneOf(['none', 'hover', 'always']),
onClick: PropTypes.func,
};

export default CustomLink;
36 changes: 30 additions & 6 deletions frontend/src/scenes/login/CheckYourEmailPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,46 @@ import styles from './Login.module.css';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { useLocation, useNavigate } from 'react-router-dom';
import CustomLink from '@components/CustomLink/CustomLink';
import { forgotPassword } from "../../services/loginServices";

const CheckYourEmailPage = () => {
const navigate = useNavigate();
const location = useLocation();
const { email: emailFromState } = location.state || {};
const values = location.state?.values || {};
const { email: emailFromState } = values;

const handleResendClick = () => {
if (emailFromState) {
forgotPassword(values)
.then(response => {
console.log("Password reset link resent successfully:", response);
})
.catch(error => {
console.error("Error resending password reset link:", error);
});
} else {
console.warn("No email provided to resend the password reset link.");
}
};
Comment on lines +14 to +26
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Lose yourself in better error handling

The current implementation has several issues:

  1. Console logs shouldn't be in production code
  2. No user feedback for success/error states
  3. Missing loading state during API call

Consider using a toast notification system and adding loading state:

const [isResending, setIsResending] = useState(false);

const handleResendClick = async () => {
  if (!emailFromState) {
    toast.warn("No email provided");
    return;
  }
  
  setIsResending(true);
  try {
    await forgotPassword(values);
    toast.success("Password reset link sent successfully");
  } catch (error) {
    toast.error("Failed to send reset link. Please try again.");
  } finally {
    setIsResending(false);
  }
};


return (
<div className={styles["login-container"]}>
<h2>Check Your Email</h2>
<h3 style={{margin: "0px"}}>We sent a password reset link to</h3>
<h3 style={{marginTop: "5px", fontWeight: "bold", marginBottom:"10px"}}>{emailFromState}</h3>
<button className={styles["create-account-button"]} style={{marginBottom: "30px"}}>Open email app</button>
<h3 style={{ margin: "0px" }}>We sent a password reset link to</h3>
<h3 style={{ marginTop: "0.25rem", fontWeight: "bold", marginBottom: "1rem" }}>
{emailFromState || "Email not provided"}
</h3>
<div className={styles["sign-up-link"]}>
Didn't receive the email? <CustomLink text="Click to resend" url="#" />
Didn't receive the email? <CustomLink text="Click to resend" onClick={handleResendClick} />
</div>
<button className={styles["back-to-login-button"]} style={{marginTop: "20px"}} onClick={() => navigate('/')}> <ArrowBackIcon style={{fontSize: "18px", marginRight: "5px"}}/>Back to log in</button>
<button
className={styles["back-to-login-button"]}
style={{ marginTop: "20px" }}
onClick={() => navigate('/')}
>
<ArrowBackIcon style={{ fontSize: "18px", marginRight: "5px" }} />
Back to log in
</button>
</div>
);
};
Expand Down
Loading
Loading