Skip to content

Commit

Permalink
Merge pull request hagopj13#78 from mustafaMohd/email-verification
Browse files Browse the repository at this point in the history
Email verification
  • Loading branch information
hagopj13 authored Mar 30, 2021
2 parents 9ba9f1b + bac7cb3 commit c20040e
Show file tree
Hide file tree
Showing 15 changed files with 319 additions and 13 deletions.
34 changes: 34 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "nodemon",
"runtimeExecutable": "nodemon",
"program": "${workspaceFolder}/app.js",
"restart": true,
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"env": {
"NODE_ENV": "development"
},
"skipFiles": [
"<node_internals>/**"
]
},
// {
// "type": "node",
// "request": "launch",
// "name": "Launch via Yarn",
// "runtimeExecutable": "yarn",
// "cwd": "${workspaceFolder}",
// "runtimeArgs": ["start:debug"],
// "port": 5858
// }

]
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "create-nodejs-express-app",
"version": "1.6.0",
"version": "1.6.1",
"description": "Create a Node.js app for building production-ready RESTful APIs using Express, by running one command",
"bin": "bin/createNodejsApp.js",
"main": "src/index.js",
Expand All @@ -13,7 +13,7 @@
"scripts": {
"start": "pm2 start ecosystem.config.json --no-daemon",
"dev": "cross-env NODE_ENV=development nodemon src/index.js",
"test": "jest -i",
"test": "jest -i --colors --testEnvironment=node --verbose --coverage --detectOpenHandles",
"test:watch": "jest -i --watchAll",
"coverage": "jest -i --coverage",
"coverage:coveralls": "jest -i --coverage --coverageReporters=text-lcov | coveralls",
Expand Down
1 change: 1 addition & 0 deletions src/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ module.exports = {
accessExpirationMinutes: envVars.JWT_ACCESS_EXPIRATION_MINUTES,
refreshExpirationDays: envVars.JWT_REFRESH_EXPIRATION_DAYS,
resetPasswordExpirationMinutes: 10,
verificationEmailExpirationMinutes: 10,
},
email: {
smtp: {
Expand Down
2 changes: 1 addition & 1 deletion src/config/roles.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const roles = ['user', 'admin'];

const roleRights = new Map();
roleRights.set(roles[0], []);
roleRights.set(roles[0], ['verifyEmail']);
roleRights.set(roles[1], ['getUsers', 'manageUsers']);

module.exports = {
Expand Down
1 change: 1 addition & 0 deletions src/config/tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const tokenTypes = {
ACCESS: 'access',
REFRESH: 'refresh',
RESET_PASSWORD: 'resetPassword',
VERIFICATION_EMAIL: 'verificationEmail',
};

module.exports = {
Expand Down
13 changes: 13 additions & 0 deletions src/controllers/auth.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,24 @@ const resetPassword = catchAsync(async (req, res) => {
res.status(httpStatus.NO_CONTENT).send();
});

const verificationEmail = catchAsync(async (req, res) => {
const verifyEmailToken = await tokenService.generateVerificationEmailToken(req.body.email);
await emailService.sendVerificationEmail(req.body.email, verifyEmailToken);
res.status(httpStatus.NO_CONTENT).send();
});

const verifyEmail = catchAsync(async (req, res) => {
await authService.verifyEmail(req.query.token);
res.status(httpStatus.NO_CONTENT).send();
});

module.exports = {
register,
login,
logout,
refreshTokens,
forgotPassword,
resetPassword,
verificationEmail,
verifyEmail,
};
2 changes: 1 addition & 1 deletion src/models/token.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const tokenSchema = mongoose.Schema(
},
type: {
type: String,
enum: [tokenTypes.REFRESH, tokenTypes.RESET_PASSWORD],
enum: [tokenTypes.REFRESH, tokenTypes.RESET_PASSWORD, tokenTypes.VERIFICATION_EMAIL],
required: true,
},
expires: {
Expand Down
4 changes: 4 additions & 0 deletions src/models/user.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ const userSchema = mongoose.Schema(
enum: roles,
default: 'user',
},
isEmailVarified: {
type: Boolean,
default: false,
},
},
{
timestamps: true,
Expand Down
67 changes: 67 additions & 0 deletions src/routes/v1/auth.route.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const express = require('express');
const validate = require('../../middlewares/validate');
const authValidation = require('../../validations/auth.validation');
const authController = require('../../controllers/auth.controller');
const auth = require('../../middlewares/auth');

const router = express.Router();

Expand All @@ -11,6 +12,13 @@ router.post('/logout', validate(authValidation.logout), authController.logout);
router.post('/refresh-tokens', validate(authValidation.refreshTokens), authController.refreshTokens);
router.post('/forgot-password', validate(authValidation.forgotPassword), authController.forgotPassword);
router.post('/reset-password', validate(authValidation.resetPassword), authController.resetPassword);
router.post(
'/verification-email',
auth('verifyEmail'),
validate(authValidation.verificationEmail),
authController.verificationEmail
);
router.post('/verify-email', validate(authValidation.verifyEmail), authController.verifyEmail);

module.exports = router;

Expand Down Expand Up @@ -243,3 +251,62 @@ module.exports = router;
* code: 401
* message: Password reset failed
*/

/**
* @swagger
* path:
* /auth/verification-email:
* post:
* summary: verification-email Email
* description: An email will be sent to verify email.
* tags: [Auth]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - email
* properties:
* email:
* type: string
* format: email
* example:
* email: fake@example.com
* responses:
* "204":
* description: No content
* "404":
* $ref: '#/components/responses/NotFound'
*/

/**
* @swagger
* path:
* /auth/verify-email:
* post:
* summary: verify email
* tags: [Auth]
* parameters:
* - in: query
* name: token
* required: true
* schema:
* type: string
* description: The verify email token
* responses:
* "204":
* description: No content
* "401":
* description: verify email failed
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* example:
* code: 401
* message: verify email failed
*/
19 changes: 19 additions & 0 deletions src/services/auth.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,29 @@ const resetPassword = async (resetPasswordToken, newPassword) => {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Password reset failed');
}
};
/**
* Verify Email
* @param {string} EmailVerificationToken
* @returns {Promise}
*/
const verifyEmail = async (emailVarificationToken) => {
try {
const emailVarificationTokenDoc = await tokenService.verifyToken(emailVarificationToken, tokenTypes.VERIFICATION_EMAIL);
const user = await userService.getUserById(emailVarificationTokenDoc.user);
if (!user) {
throw new Error();
}
await Token.deleteMany({ user: user.id, type: tokenTypes.VERIFICATION_EMAIL });
await userService.updateUserById(user.id, { isEmailVarified: true });
} catch (error) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'email verification failed');
}
};

module.exports = {
loginUserWithEmailAndPassword,
logout,
refreshAuth,
resetPassword,
verifyEmail,
};
13 changes: 11 additions & 2 deletions src/services/email.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,24 @@ const sendEmail = async (to, subject, text) => {
const sendResetPasswordEmail = async (to, token) => {
const subject = 'Reset password';
// replace this url with the link to the reset password page of your front-end app
const resetPasswordUrl = `http://link-to-app/reset-password?token=${token}`;
const resetPasswordUrl = `http://link-to-app/verify-email?token=${token}`;
const text = `Dear user,
To reset your password, click on this link: ${resetPasswordUrl}
If you did not request any password resets, then ignore this email.`;
await sendEmail(to, subject, text);
};

const sendVerificationEmail = async (to, token) => {
const subject = 'Email Verification';
// replace this url with the link to the reset password page of your front-end app
const verificationEmailUrl = `http://link-to-app/verify-email?token=${token}`;
const text = `Dear user,
To verify your email, click on this link: ${verificationEmailUrl}
If you did not request any email verification, then ignore this email.`;
await sendEmail(to, subject, text);
};
module.exports = {
transport,
sendEmail,
sendResetPasswordEmail,
sendVerificationEmail,
};
12 changes: 11 additions & 1 deletion src/services/token.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,21 @@ const generateResetPasswordToken = async (email) => {
await saveToken(resetPasswordToken, user.id, expires, tokenTypes.RESET_PASSWORD);
return resetPasswordToken;
};

const generateVerificationEmailToken = async (email) => {
const user = await userService.getUserByEmail(email);
if (!user) {
throw new ApiError(httpStatus.NOT_FOUND, 'No users found with this email');
}
const expires = moment().add(config.jwt.verificationEmailExpirationMinutes, 'minutes');
const verificationEmailToken = generateToken(user.id, expires);
await saveToken(verificationEmailToken, user.id, expires, tokenTypes.VERIFICATION_EMAIL);
return verificationEmailToken;
};
module.exports = {
generateToken,
saveToken,
verifyToken,
generateAuthTokens,
generateResetPasswordToken,
generateVerificationEmailToken,
};
14 changes: 14 additions & 0 deletions src/validations/auth.validation.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable no-unused-vars */
/* eslint-disable prettier/prettier */
const Joi = require('joi');
const { password } = require('./custom.validation');

Expand Down Expand Up @@ -43,11 +45,23 @@ const resetPassword = {
}),
};

const verificationEmail = {
body: Joi.object().keys({
email: Joi.string().required(),
})
};
const verifyEmail = {
query: Joi.object().keys({
token: Joi.string().required(),
})
};
module.exports = {
register,
login,
logout,
refreshTokens,
forgotPassword,
resetPassword,
verificationEmail,
verifyEmail,
};
Loading

0 comments on commit c20040e

Please sign in to comment.