diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 262ba399..89f2c1fe 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -36,9 +36,9 @@ 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); +const sendVerificationEmail = catchAsync(async (req, res) => { + const verifyEmailToken = await tokenService.generateVerifyEmailToken(req.user); + await emailService.sendVerificationEmail(req.user.email, verifyEmailToken); res.status(httpStatus.NO_CONTENT).send(); }); @@ -54,6 +54,6 @@ module.exports = { refreshTokens, forgotPassword, resetPassword, - verificationEmail, + sendVerificationEmail, verifyEmail, }; diff --git a/src/routes/v1/auth.route.js b/src/routes/v1/auth.route.js index 7856a74a..220fde38 100644 --- a/src/routes/v1/auth.route.js +++ b/src/routes/v1/auth.route.js @@ -12,7 +12,7 @@ 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(), validate(authValidation.verificationEmail), authController.verificationEmail); +router.post('/send-verification-email', auth(), authController.sendVerificationEmail); router.post('/verify-email', validate(authValidation.verifyEmail), authController.verifyEmail); module.exports = router; @@ -249,59 +249,43 @@ module.exports = router; /** * @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' + * /auth/send-verification-email: + * post: + * summary: Send verification email + * description: An email will be sent to verify email. + * tags: [Auth] + * security: + * - bearerAuth: [] + * responses: + * "204": + * description: No content + * "401": + * $ref: '#/components/responses/Unauthorized' */ /** * @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 + * /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 */ diff --git a/src/services/auth.service.js b/src/services/auth.service.js index 60f499e4..18d9b221 100644 --- a/src/services/auth.service.js +++ b/src/services/auth.service.js @@ -70,22 +70,23 @@ const resetPassword = async (resetPasswordToken, newPassword) => { throw new ApiError(httpStatus.UNAUTHORIZED, 'Password reset failed'); } }; + /** - * Verify Email - * @param {string} EmailVerificationToken + * Verify email + * @param {string} verifyEmailToken * @returns {Promise} */ -const verifyEmail = async (emailVarificationToken) => { +const verifyEmail = async (verifyEmailToken) => { try { - const emailVarificationTokenDoc = await tokenService.verifyToken(emailVarificationToken, tokenTypes.VERIFY_EMAIL); - const user = await userService.getUserById(emailVarificationTokenDoc.user); + const verifyEmailTokenDoc = await tokenService.verifyToken(verifyEmailToken, tokenTypes.VERIFY_EMAIL); + const user = await userService.getUserById(verifyEmailTokenDoc.user); if (!user) { throw new Error(); } await Token.deleteMany({ user: user.id, type: tokenTypes.VERIFY_EMAIL }); await userService.updateUserById(user.id, { isEmailVerified: true }); } catch (error) { - throw new ApiError(httpStatus.UNAUTHORIZED, 'email verification failed'); + throw new ApiError(httpStatus.UNAUTHORIZED, 'Email verification failed'); } }; diff --git a/src/services/email.service.js b/src/services/email.service.js index fd736aa5..93235123 100644 --- a/src/services/email.service.js +++ b/src/services/email.service.js @@ -32,21 +32,29 @@ 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/verify-email?token=${token}`; + const resetPasswordUrl = `http://link-to-app/reset-password?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); }; + +/** + * Send verification email + * @param {string} to + * @param {string} token + * @returns {Promise} + */ 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 + // replace this url with the link to the email verification 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.`; +To verify your email, click on this link: ${verificationEmailUrl} +If you did not create an account, then ignore this email.`; await sendEmail(to, subject, text); }; + module.exports = { transport, sendEmail, diff --git a/src/services/token.service.js b/src/services/token.service.js index 810a5e2c..455b2814 100644 --- a/src/services/token.service.js +++ b/src/services/token.service.js @@ -99,21 +99,24 @@ 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'); - } + +/** + * Generate verify email token + * @param {string} email + * @returns {Promise} + */ +const generateVerifyEmailToken = async (user) => { const expires = moment().add(config.jwt.verifyEmailExpirationMinutes, 'minutes'); - const verificationEmailToken = generateToken(user.id, expires); - await saveToken(verificationEmailToken, user.id, expires, tokenTypes.VERIFY_EMAIL); - return verificationEmailToken; + const verifyEmailToken = generateToken(user.id, expires, tokenTypes.VERIFY_EMAIL); + await saveToken(verifyEmailToken, user.id, expires, tokenTypes.VERIFY_EMAIL); + return verifyEmailToken; }; + module.exports = { generateToken, saveToken, verifyToken, generateAuthTokens, generateResetPasswordToken, - generateVerificationEmailToken, + generateVerifyEmailToken, }; diff --git a/src/validations/auth.validation.js b/src/validations/auth.validation.js index 44933b08..e562c989 100644 --- a/src/validations/auth.validation.js +++ b/src/validations/auth.validation.js @@ -1,5 +1,3 @@ -/* eslint-disable no-unused-vars */ -/* eslint-disable prettier/prettier */ const Joi = require('joi'); const { password } = require('./custom.validation'); @@ -45,16 +43,12 @@ 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, @@ -62,6 +56,5 @@ module.exports = { refreshTokens, forgotPassword, resetPassword, - verificationEmail, verifyEmail, }; diff --git a/tests/integration/auth.test.js b/tests/integration/auth.test.js index ab8abf74..ef1ae44f 100644 --- a/tests/integration/auth.test.js +++ b/tests/integration/auth.test.js @@ -13,7 +13,7 @@ const setupTestDB = require('../utils/setupTestDB'); const { User, Token } = require('../../src/models'); const { roleRights } = require('../../src/config/roles'); const { tokenTypes } = require('../../src/config/tokens'); -const { userOne, admin, insertUsers, userTwo } = require('../fixtures/user.fixture'); +const { userOne, admin, insertUsers } = require('../fixtures/user.fixture'); const { userOneAccessToken, adminAccessToken } = require('../fixtures/token.fixture'); setupTestDB(); @@ -243,11 +243,7 @@ describe('Auth routes', () => { await insertUsers([userOne]); const sendResetPasswordEmailSpy = jest.spyOn(emailService, 'sendResetPasswordEmail'); - await request(app) - .post('/v1/auth/forgot-password') - .set('Authorization', `Bearer ${userOneAccessToken}`) - .send({ email: userOne.email }) - .expect(httpStatus.NO_CONTENT); + await request(app).post('/v1/auth/forgot-password').send({ email: userOne.email }).expect(httpStatus.NO_CONTENT); expect(sendResetPasswordEmailSpy).toHaveBeenCalledWith(userOne.email, expect.any(String)); const resetPasswordToken = sendResetPasswordEmailSpy.mock.calls[0][1]; @@ -358,49 +354,32 @@ describe('Auth routes', () => { .expect(httpStatus.BAD_REQUEST); }); }); - describe('POST /v1/auth/verification-email', () => { + + describe('POST /v1/auth/send-verification-email', () => { beforeEach(() => { jest.spyOn(emailService.transport, 'sendMail').mockResolvedValue(); }); test('should return 204 and send verification email to the user', async () => { await insertUsers([userOne]); - const sendVerificationEmaillSpy = jest.spyOn(emailService, 'sendVerificationEmail'); + const sendVerificationEmailSpy = jest.spyOn(emailService, 'sendVerificationEmail'); await request(app) - .post('/v1/auth/verification-email') + .post('/v1/auth/send-verification-email') .set('Authorization', `Bearer ${userOneAccessToken}`) - .send({ email: userOne.email }) .expect(httpStatus.NO_CONTENT); - expect(sendVerificationEmaillSpy).toHaveBeenCalledWith(userOne.email, expect.any(String)); - const VerificationEmailToken = sendVerificationEmaillSpy.mock.calls[0][1]; - const dbVerificationEmailTokenDoc = await Token.findOne({ token: VerificationEmailToken, user: userOne._id }); + expect(sendVerificationEmailSpy).toHaveBeenCalledWith(userOne.email, expect.any(String)); + const verifyEmailToken = sendVerificationEmailSpy.mock.calls[0][1]; + const dbVerifyEmailToken = await Token.findOne({ token: verifyEmailToken, user: userOne._id }); - expect(dbVerificationEmailTokenDoc).toBeDefined(); + expect(dbVerifyEmailToken).toBeDefined(); }); - test('should return 400 if email is missing', async () => { - await insertUsers([userOne]); - - await request(app) - .post('/v1/auth/verification-email') - .set('Authorization', `Bearer ${userOneAccessToken}`) - .send() - .expect(httpStatus.BAD_REQUEST); - }); - test('should return 404 if email does not belong to any user', async () => { - await insertUsers([userOne]); - await request(app) - .post('/v1/auth/verification-email') - .set('Authorization', `Bearer ${userOneAccessToken}`) - .send({ email: userTwo.email }) - .expect(httpStatus.NOT_FOUND); - }); test('should return 401 error if access token is missing', async () => { await insertUsers([userOne]); - await request(app).post('/v1/auth/verification-email').send({ email: userOne.email }).expect(httpStatus.UNAUTHORIZED); + await request(app).post('/v1/auth/send-verification-email').send().expect(httpStatus.UNAUTHORIZED); }); }); @@ -408,12 +387,12 @@ describe('Auth routes', () => { test('should return 204 and verify the email', async () => { await insertUsers([userOne]); const expires = moment().add(config.jwt.verifyEmailExpirationMinutes, 'minutes'); - const verificationEmailToken = tokenService.generateToken(userOne._id, expires); - await tokenService.saveToken(verificationEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL); + const verifyEmailToken = tokenService.generateToken(userOne._id, expires); + await tokenService.saveToken(verifyEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL); await request(app) .post('/v1/auth/verify-email') - .query({ token: verificationEmailToken }) + .query({ token: verifyEmailToken }) .send() .expect(httpStatus.NO_CONTENT); @@ -421,53 +400,53 @@ describe('Auth routes', () => { expect(dbUser.isEmailVerified).toBe(true); - const dbVerificationEmailTokenCount = await Token.countDocuments({ + const dbVerifyEmailToken = await Token.countDocuments({ user: userOne._id, type: tokenTypes.VERIFY_EMAIL, }); - expect(dbVerificationEmailTokenCount).toBe(0); + expect(dbVerifyEmailToken).toBe(0); }); - test('should return 400 if email verification token is missing', async () => { + test('should return 400 if verify email token is missing', async () => { await insertUsers([userOne]); await request(app).post('/v1/auth/verify-email').send().expect(httpStatus.BAD_REQUEST); }); - test('should return 401 if verification email token is blacklisted', async () => { + test('should return 401 if verify email token is blacklisted', async () => { await insertUsers([userOne]); const expires = moment().add(config.jwt.verifyEmailExpirationMinutes, 'minutes'); - const verificationEmailToken = tokenService.generateToken(userOne._id, expires); - await tokenService.saveToken(verificationEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL, true); + const verifyEmailToken = tokenService.generateToken(userOne._id, expires); + await tokenService.saveToken(verifyEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL, true); await request(app) .post('/v1/auth/verify-email') - .query({ token: verificationEmailToken }) + .query({ token: verifyEmailToken }) .send() .expect(httpStatus.UNAUTHORIZED); }); - test('should return 401 if email verification token is expired', async () => { + test('should return 401 if verify email token is expired', async () => { await insertUsers([userOne]); const expires = moment().subtract(1, 'minutes'); - const verificationEmailToken = tokenService.generateToken(userOne._id, expires); - await tokenService.saveToken(verificationEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL); + const verifyEmailToken = tokenService.generateToken(userOne._id, expires); + await tokenService.saveToken(verifyEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL); await request(app) .post('/v1/auth/verify-email') - .query({ token: verificationEmailToken }) + .query({ token: verifyEmailToken }) .send() .expect(httpStatus.UNAUTHORIZED); }); test('should return 401 if user is not found', async () => { const expires = moment().add(config.jwt.verifyEmailExpirationMinutes, 'minutes'); - const verificationEmailToken = tokenService.generateToken(userOne._id, expires); - await tokenService.saveToken(verificationEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL); + const verifyEmailToken = tokenService.generateToken(userOne._id, expires); + await tokenService.saveToken(verifyEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL); await request(app) .post('/v1/auth/verify-email') - .query({ token: verificationEmailToken }) + .query({ token: verifyEmailToken }) .send() .expect(httpStatus.UNAUTHORIZED); });