Skip to content

Commit

Permalink
Merge pull request #203 from AltCamp/fix/feedback
Browse files Browse the repository at this point in the history
improve: email verification & reset password
  • Loading branch information
tobisupreme committed Jul 12, 2023
2 parents 07484c0 + 58e2969 commit fa87916
Show file tree
Hide file tree
Showing 13 changed files with 2,973 additions and 2,755 deletions.
1 change: 1 addition & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const appConfig = (app) => {
app.use(morgan('combined', { stream: logger.stream }));
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: false }));
app.use(express.static('public'));
app.use(cookieParser());

app.get('/', (req, res) => {
Expand Down
5,555 changes: 2,877 additions & 2,678 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"lodash": "^4.17.21",
"mongoose": "^6.8.4",
"morgan": "~1.9.1",
"nanoid": "^3.1.30",
"nodemailer": "^6.9.3",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
Expand All @@ -58,7 +59,7 @@
"jest": "^29.3.1",
"lint-staged": "^13.2.2",
"mongodb-memory-server": "^8.11.3",
"nodemon": "^2.0.20",
"nodemon": "^3.0.1",
"prettier": "^2.8.3",
"supertest": "^6.3.3"
}
Expand Down
Binary file added public/favicon.ico
Binary file not shown.
11 changes: 4 additions & 7 deletions src/accounts/accountsController.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,15 @@ async function updateAccount(req, res) {

const forgotPassword = async (req, res) => {
const { email } = req.body;
await accountsService.forgotPassword({ email });
const requestId = await accountsService.requestPasswordReset({ email });

new responseHandler(res, undefined, 200, RESPONSE_MESSAGE.SUCCESS);
new responseHandler(res, { requestId }, 200, RESPONSE_MESSAGE.SUCCESS);
};

const resetPassword = async (req, res) => {
const { token, password } = req.body;
const payload = { ...req.body };

const reset = await accountsService.resetPassword({
token,
newPassword: password,
});
const reset = await accountsService.resetPassword(payload);

new responseHandler(res, reset, 200, RESPONSE_MESSAGE.SUCCESS);
};
Expand Down
47 changes: 22 additions & 25 deletions src/accounts/accountsService.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
tokenExpires,
validateCredentials,
verifyPassword,
generateId,
} = require('../../utils/helper');
const { apiFeatures } = require('../common');
const {
Expand All @@ -31,7 +32,7 @@ async function accountExists(email) {
const account = await Account.findOne({ email });

if (account) {
return true;
return account;
}

return false;
Expand All @@ -51,12 +52,13 @@ async function updatePassword(userId, oldPassword, newPassword) {
return user;
}

const forgotPassword = async ({ email }) => {
const validUser = await Account.findOne({ email });

if (!validUser) throw new BadRequestError('User does not exist!');
const requestPasswordReset = async ({ email }) => {
const validUser = await accountExists(email);
if (!validUser) return;

const requestId = generateId();
const token = await TokenService.createToken({
requestId,
type: TOKEN_TYPE.PASSWORD_RESET,
owner: validUser._id,
timeToLive: OTP_VALIDITY.PASSWORD_RESET,
Expand All @@ -67,43 +69,38 @@ const forgotPassword = async ({ email }) => {
const mailServicePayload = {
context: {
name: validUser.firstName,
token: token.token,
tokenValidity: tokenExpires(OTP_VALIDITY.PASSWORD_RESET),
token,
},
email: validUser.email,
templateName: EMAIL_TEMPLATES.PASSWORD_RESET,
subject: EMAIL_SUBJECTS.PASSWORD_RESET,
};

await mailService.sendMail(mailServicePayload);
return requestId;
};

const resetPassword = async ({ token, newPassword }) => {
const userToken = await TokenService.getToken({
type: TOKEN_TYPE.PASSWORD_RESET,
token: token,
});

if (!userToken) throw new BadRequestError('Token not found!');
const resetPassword = async ({
requestId,
token,
retypePassword: newPassword,
}) => {
const key = TOKEN_TYPE.PASSWORD_RESET + requestId;
const userToken = await TokenService.getToken(key);

if (!userToken.token === token) throw new BadRequestError('Invalid token');

const isExpired = userToken.expiresAt.getTime() <= Date.now();
if (isExpired) {
TokenService.deleteToken(userToken._id);
throw new BadRequestError('Expired token');
}
if (!userToken || !userToken.token === token)
throw new BadRequestError('Invalid token');

let user = await Account.findById(userToken.owner).select('+password');

if (!user) throw new BadRequestError('User does not exist');
if (!user) throw new BadRequestError('Invalid token');

user.password = newPassword;

await user.save();
await userToken.delete();
user = omit(user.toObject(), ['password']);

TokenService.deleteToken(key);

return user;
};

Expand Down Expand Up @@ -216,6 +213,6 @@ module.exports = {
deleteProfilePicture,
deleteAccount,
updatePassword,
forgotPassword,
requestPasswordReset,
resetPassword,
};
1 change: 1 addition & 0 deletions src/accounts/accountsValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const forgotPasswordValidator = Joi.object({
});

const resetPasswordValidator = Joi.object({
requestId: Joi.string().required(),
token: Joi.string()
.regex(/^\d{4}$/)
.required()
Expand Down
34 changes: 10 additions & 24 deletions src/auth/authController.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ const {
const responseHandler = require('../../utils/responseHandler');
const authService = require('./authService');
const TokenService = require('../token/tokenService');
const accountService = require('../accounts/accountsService');
const mailService = require('../mailer/mailerService');
const { tokenExpires } = require('../../utils/helper');
const { tokenExpires, generateId } = require('../../utils/helper');

const registerAccount = async (req, res) => {
const payload = { ...req.body };
Expand Down Expand Up @@ -56,11 +55,13 @@ const userLogout = async (req, res) => {
new responseHandler(res, undefined, 200, RESPONSE_MESSAGE.LOGOUT);
};

const verifyEmail = async (req, res) => {
const requestEmailVerification = async (req, res) => {
if (req.user.emailIsVerified)
throw new BadRequestError(RESPONSE_MESSAGE.ALREADY_VERIFIED);

const requestId = generateId();
const token = await TokenService.createToken({
requestId,
type: TOKEN_TYPE.EMAIL_VERIFICATION,
owner: req.user.id,
timeToLive: OTP_VALIDITY.EMAIL_VERIFICATION,
Expand All @@ -70,8 +71,8 @@ const verifyEmail = async (req, res) => {

const mailServicePayload = {
context: {
token: token,
name: req.user.firstName,
token: token.token,
tokenValidity: tokenExpires(OTP_VALIDITY.EMAIL_VERIFICATION),
},
email: req.user.email,
Expand All @@ -81,28 +82,13 @@ const verifyEmail = async (req, res) => {

await mailService.sendMail(mailServicePayload);

new responseHandler(res, undefined, 200, RESPONSE_MESSAGE.SUCCESS);
new responseHandler(res, { requestId }, 200, RESPONSE_MESSAGE.SUCCESS);
};

const verifyEmailOtp = async (req, res) => {
const otp = req.body.token;

const user = await accountService.getSingleAccount(req.user.id);
if (!user) throw new BadRequestError('User does not exist!');

const token = await TokenService.getToken({
type: TOKEN_TYPE.EMAIL_VERIFICATION,
owner: user.id,
token: otp,
});

if (!token) throw new BadRequestError('OTP not found!');

if (!token.token === otp) throw new BadRequestError('Incorrect OTP!');
const verifyEmail = async (req, res) => {
const payload = { ...req.body };

user.emailIsVerified = true;
await user.save();
await token.delete();
await authService.verifyEmail(payload);

new responseHandler(res, undefined, 200, 'Email verification successful!');
};
Expand All @@ -111,6 +97,6 @@ module.exports = {
registerAccount,
userLogin,
userLogout,
verifyEmailOtp,
verifyEmail,
requestEmailVerification,
};
18 changes: 18 additions & 0 deletions src/auth/authService.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
const { omit } = require('lodash');
const accountsService = require('../accounts/accountsService');
const { createToken, validateCredentials } = require('../../utils/helper');
const { TOKEN_TYPE } = require('../../constant');
const TokenService = require('../token/tokenService');
const { BadRequestError } = require('../../utils/customError');

const registerAccount = async (payload) => {
let account = await accountsService.accountExists(payload.email);
Expand Down Expand Up @@ -36,7 +39,22 @@ const userLogin = async ({ email, password }) => {
};
};

const verifyEmail = async ({ requestId, token }) => {
const key = TOKEN_TYPE.EMAIL_VERIFICATION + requestId;
const verificationToken = await TokenService.getToken(key);
if (!verificationToken) throw new BadRequestError('Invalid token');

const user = await accountsService.getSingleAccount(verificationToken.owner);
if (!user || !(verificationToken.token === token))
throw new BadRequestError('Invalid token');

user.emailIsVerified = true;
await user.save();
TokenService.deleteToken(key);
};

module.exports = {
registerAccount,
userLogin,
verifyEmail,
};
5 changes: 3 additions & 2 deletions src/auth/authValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const {
TRACKS,
} = require('../../constant');

const otpValidator = Joi.object({
const emailVerificationValidator = Joi.object({
requestId: Joi.string().required(),
token: Joi.number().integer().required().messages({
'number.base': 'token type is invalid',
'object.unknown': 'token type is invalid',
Expand Down Expand Up @@ -82,6 +83,6 @@ const createAccountValidator = Joi.object({

module.exports = {
createAccountValidator,
otpValidator,
emailVerificationValidator,
loginValidator,
};
12 changes: 6 additions & 6 deletions src/auth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ const {
registerAccount,
userLogin,
userLogout,
verifyEmailOtp,
verifyEmail,
requestEmailVerification,
} = require('./authController');
const limiter = require('../../middleware/rateLimit');
const {
createAccountValidator,
otpValidator,
emailVerificationValidator,
loginValidator,
} = require('./authValidator');
const validatorMiddleware = require('../../middleware/validator');
Expand All @@ -21,12 +21,12 @@ router.use(limiter());

router.post('/login', validatorMiddleware(loginValidator), userLogin);

router.post('/verify-email', verifyUser, verifyEmail);
router.post('/start-email-verification', verifyUser, requestEmailVerification);
router.post(
'/verify-otp',
'/verify-email',
verifyUser,
validatorMiddleware(otpValidator),
verifyEmailOtp
validatorMiddleware(emailVerificationValidator),
verifyEmail
);

router.post(
Expand Down
26 changes: 16 additions & 10 deletions src/token/tokenService.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
const { Token } = require('../../model');
const { BadRequestError } = require('../../utils/customError');
const { generate4DigitOTP } = require('../../utils/helper');
const redisClient = require('../common/cache/cacheService');

class TokenService {
static async createToken({ type, owner, timeToLive }) {
const otpCode = Math.floor(Math.random() * 9000) + 1000;
const ttlMs = Date.now() + 60 * 1000 * timeToLive;
const expiresAt = new Date(ttlMs);
static async createToken({ requestId, type, owner, timeToLive }) {
const otpCode = generate4DigitOTP();

const data = await Token.create({ token: otpCode, owner, type, expiresAt });
if (!data)
try {
await redisClient.set(
`${type + requestId}`,
JSON.stringify({ token: otpCode, owner }),
{ EX: timeToLive }
);
return otpCode;
} catch (error) {
throw new BadRequestError('An error occurred while creating token!');
return data;
}
}

static async getToken(where) {
return await Token.findOne(where);
const token = await redisClient.get(where);
return token ? JSON.parse(token) : token;
}

static async deleteToken(_id) {
return await Token.deleteOne(_id);
return await redisClient.del(_id);
}
}
module.exports = TokenService;
15 changes: 13 additions & 2 deletions utils/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const jwt = require('jsonwebtoken');
const { JSDOM } = require('jsdom');
const domPurify = createDomPurify(new JSDOM().window);
const slugify = require('slugify');
const { v4 } = require('uuid');

const createHashedToken = (token) => {
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
Expand Down Expand Up @@ -45,13 +46,23 @@ async function verifyPassword(plain, hashed) {
}

function tokenExpires(ttl) {
const mttl = parseInt(ttl, 10);
return `${mttl} minute${mttl === 1 ? 's' : ''}`;
const mttl = Math.floor(parseInt(ttl, 10) / 60);
return `${mttl} minute${mttl > 1 ? 's' : ''}`;
}

function generateId() {
return v4();
}

function generate4DigitOTP() {
return Math.floor(1000 + Math.random() * 9000);
}

module.exports = {
createHashedToken,
createToken,
generate4DigitOTP,
generateId,
generateSlug,
sanitiseHTML,
validateCredentials,
Expand Down

0 comments on commit fa87916

Please sign in to comment.