Skip to content

Commit

Permalink
Merge pull request #25 from jareerzeenam/dev
Browse files Browse the repository at this point in the history
User Authentication, reset password improvements and email services added
  • Loading branch information
jareerzeenam authored Aug 15, 2024
2 parents e3936f2 + 369f9f3 commit 7ed0ea7
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 68 deletions.
9 changes: 9 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"graphql": "^16.9.0",
"http": "^0.0.1-security",
"jsonwebtoken": "^9.0.0",
"moment": "^2.30.1",
"mongodb-memory-server": "^8.12.2",
"mongoose": "^7.0.3",
"nodemailer": "^6.9.1",
Expand Down
55 changes: 55 additions & 0 deletions repositories/user-repository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const { User } = require('../models/User.model');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

class UserRepository {
async create(payload) {
const username = payload.username;
const email = payload.email;
const password = payload.password;
// Encrypt Password
var encryptedPassword = await bcrypt.hash(password, 10);

// Build out the mongoose model
const newUser = new User({
username: username,
email: email.toLowerCase(),
password: encryptedPassword,
});

// Create our JWT (attach to user model)
const token = jwt.sign(
{
user_id: newUser._id,
email,
},
process.env.JWT_HASH_TOKEN_KEY,
{
expiresIn: '2h',
}
);
newUser.token = token;

// Save user to MongoDB
const res = await newUser.save();

return {
id: res.id,
...res._doc,
};
}

/**
* Find a user by email.
*
* @param {String} email - The email of the user.
* @returns {User}
*/

async findByEmail(email) {
const user = await User.findOne({ email });
return user;
}
}

module.exports = UserRepository;
125 changes: 57 additions & 68 deletions services/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,34 @@ const { User } = require('../models/User.model');
const { ForbiddenError } = require('apollo-server-errors');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const nodemailer = require('nodemailer');
const moment = require('moment');
const Validator = require('../utils/validator');
const ValidationError = require('../errors/ValidationError');
const UserRepository = require('../repositories/user-repository');
const { sendEmail } = require('./email');

const validateUserInput = (
{ username, email, password },
context = 'register'
) => {
const rules = {
username: 'required|string',
email: 'required|email',
password: 'required',
};

// Modify rules for login
if (context === 'login') {
delete rules.username;
}

const validateUserInput = ({ username, email, password }) => {
const validation = new Validator(
{
username,
email,
password,
},
{
username: 'required',
email: 'required|email',
password: 'required',
}
rules
);

validation.setAttributeNames({
Expand All @@ -39,59 +51,32 @@ const registerUser = async (payload) => {
// Validate Use Input
validateUserInput(payload);

const username = payload.username;
const email = payload.email;
const password = payload.password;

// See if an old user is exists with email attempting to register
const oldUser = await User.findOne({ email });
const userRepo = new UserRepository();
const userExists = await userRepo.findByEmail(payload.email);

// Throw error if that user exists
if (oldUser) {
if (userExists) {
throw new ForbiddenError(
`A user is already registered with the email of ${email} please try a different email.`
`A user is already registered with the email of ${payload.email} please try a different email.`
);
}

// Encrypt Password
var encryptedPassword = await bcrypt.hash(password, 10);

// Build out the mongoose model
const newUser = new User({
username: username,
email: email.toLowerCase(),
password: encryptedPassword,
});

// Create our JWT (attach to user model)
const token = jwt.sign(
{
user_id: newUser._id,
email,
},
process.env.JWT_HASH_TOKEN_KEY,
{
expiresIn: '2h',
}
);
newUser.token = token;
const newUser = await userRepo.create(payload);

// Save user to MongoDB
const res = await newUser.save(); // TODO :: Move save to user repository
return {
id: res.id,
...res._doc,
};
return newUser;
};

const loginUser = async (payload) => {
// TODO :: Validate payload
// Validate Use Input
validateUserInput(payload, 'login');

const email = payload.email;
const password = payload.password;

// See first if the user exist with the email
const user = await User.findOne({ email }); // TODO :: Move find to user repository
const userRepo = new UserRepository();
const user = await userRepo.findByEmail(payload.email);

// Throw error if that user does not exists
if (!user) {
Expand Down Expand Up @@ -130,28 +115,17 @@ const loginUser = async (payload) => {
const sendResetPasswordEmail = async (email) => {
const token = await generateResetToken(email);

const transporter = nodemailer.createTransport({
host: process.env.MAIL_HOST,
port: process.env.MAIL_PORT,
auth: {
user: process.env.MAIL_USERNAME,
pass: process.env.MAIL_PASSWORD,
},
});
const subject = 'Reset your password';
const resetUrl = `https://example.com/reset-password?email=${email}&token=${token}`;
const mailOptions = {
from: process.env.MAIL_FROM_ADDRESS,
to: email,
subject: 'Reset your password',
html: `Click <a href="${resetUrl}">here</a> to reset your password
<br><hr>
Email : ${email}
<br>
Token : ${token}
<br><hr>
`,
};
await transporter.sendMail(mailOptions);
const content = `
<p>Click <a href="${resetUrl}">here</a> to reset your password.</p>
<br><hr>
<p>Email: ${email}</p>
<p>Token: ${token}</p>
<br><hr>
`;

await sendEmail(email, subject, content);

return {
message: 'Email Sent',
Expand All @@ -166,7 +140,10 @@ const resetPassword = async (payload) => {

const user = await User.findOne({ email, resetToken: token });

if (!user || user.resetTokenExpires < Date.now()) {
if (
!user ||
moment(user.resetTokenExpires) < moment().subtract(1, 'hour')
) {
throw new Error('Invalid or expired token');
}

Expand Down Expand Up @@ -194,11 +171,23 @@ const resetPassword = async (payload) => {
async function generateResetToken(email) {
const user = await User.findOne({ email });

// TODO :: Check when is the last reset email was sent and send the second one (valid time within 1hr)
if (!user) {
throw new Error('User not found');
}

if (user.resetTokenExpires) {
const resetTokenExpires = moment(tokenExpires);
const tokenHasExpired = resetTokenExpires.isBefore(
moment().subtract(1, 'hour')
);

if (!tokenHasExpired) {
throw new Error(
'An email has already been sent. Please check your email.'
);
}
}

const tokenPayload = { id: user.id };
const tokenOptions = { expiresIn: '1h' };

Expand All @@ -210,7 +199,7 @@ async function generateResetToken(email) {

// Update user reset token fields
user.resetToken = jwtToken;
user.resetTokenExpires = Date.now() + 3600000; // 1 hour
user.resetTokenExpires = moment().add(1, 'hour'); // 1 hour
await user.save();

return jwtToken;
Expand Down
24 changes: 24 additions & 0 deletions services/email.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const nodemailer = require('nodemailer');

const transporter = nodemailer.createTransport({
host: process.env.MAIL_HOST,
port: process.env.MAIL_PORT,
auth: {
user: process.env.MAIL_USERNAME,
pass: process.env.MAIL_PASSWORD,
},
});

const sendEmail = async (to, subject, html) => {
const mailOptions = {
from: process.env.MAIL_FROM_ADDRESS,
to,
subject,
html,
};
await transporter.sendMail(mailOptions);
};

module.exports = {
sendEmail,
};

0 comments on commit 7ed0ea7

Please sign in to comment.