From dc6ec4574f538a6f6a04968c545545a0b6891fc3 Mon Sep 17 00:00:00 2001 From: Ayelegun Kayode Michael Date: Wed, 18 Apr 2018 18:48:14 +0100 Subject: [PATCH] feature/send-notifications - implement controller for ending notifications when an event is cancelled - install nodemailer - write custom functions for handling email notification - modify the user database table --- .env.sample | 3 + package-lock.json | 108 ++++++++++++++++++ package.json | 3 + server/controllers/event.js | 30 +++-- server/controllers/mailer.js | 15 +++ server/controllers/user.js | 10 +- .../20180418075722-add-phone-number.js | 26 +++++ server/migrations/20180418081732-dropPhone.js | 25 ++++ .../migrations/20180418081852-readdphone.js | 26 +++++ server/models/user.js | 3 + server/seeders/20180409073557-user-seed.js | 3 + server/tests/seed/userseed.js | 3 + server/tests/user.test.js | 15 ++- server/validators/validateSignup.js | 7 ++ 14 files changed, 262 insertions(+), 15 deletions(-) create mode 100644 .env.sample create mode 100644 server/controllers/mailer.js create mode 100644 server/migrations/20180418075722-add-phone-number.js create mode 100644 server/migrations/20180418081732-dropPhone.js create mode 100644 server/migrations/20180418081852-readdphone.js diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..87510b5 --- /dev/null +++ b/.env.sample @@ -0,0 +1,3 @@ +SECRET= +EMAIL_ADDRESS= +PASSWORD= diff --git a/package-lock.json b/package-lock.json index 5ab7188..7b5458b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4014,6 +4014,11 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "isemail": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz", + "integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=" + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4810,6 +4815,24 @@ "merge-stream": "1.0.1" } }, + "joi": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-6.10.1.tgz", + "integrity": "sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY=", + "requires": { + "hoek": "2.16.3", + "isemail": "1.2.0", + "moment": "2.22.0", + "topo": "1.1.0" + }, + "dependencies": { + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + } + } + }, "js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -5027,6 +5050,23 @@ "type-check": "0.3.2" } }, + "libphonenumber-js": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.1.10.tgz", + "integrity": "sha512-B403n9dSDEAt7ojWGeSG9NfW236UR6UuhcZk6i8cuZZgsJsX4qsnu0Ln4Q+G+OVPMvaUGLDSkhQQA/oNo+LSYw==", + "requires": { + "minimist": "1.2.0", + "semver-compare": "1.0.0", + "xml2js": "0.4.19" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, "load-json-file": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", @@ -5473,6 +5513,35 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, + "nexmo": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nexmo/-/nexmo-2.2.0.tgz", + "integrity": "sha512-7D/QVyQakLJ8M40hANSvINzswK/3XWfml0hY7Ce2pwn5Kinu5UL5i5+nobnvvftoi0mv1Sx288+fmVEqJkh0Mw==", + "requires": { + "jsonwebtoken": "7.4.3", + "request": "2.85.0", + "uuid": "2.0.3" + }, + "dependencies": { + "jsonwebtoken": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-7.4.3.tgz", + "integrity": "sha1-d/UCHeBYtgWheD+hKD6ZgS5kVjg=", + "requires": { + "joi": "6.10.1", + "jws": "3.1.4", + "lodash.once": "4.1.1", + "ms": "2.0.0", + "xtend": "4.0.1" + } + }, + "uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=" + } + } + }, "nocache": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.0.0.tgz", @@ -5532,6 +5601,11 @@ } } }, + "nodemailer": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.6.4.tgz", + "integrity": "sha512-SD4uuX7NMzZ5f5m1XHDd13J4UC3SmdJk8DsmU1g6Nrs5h3x9LcXr6EBPZIqXRJ3LrF7RdklzGhZRF/TuylTcLg==" + }, "nodemon": { "version": "1.17.3", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.17.3.tgz", @@ -10015,6 +10089,11 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=" + }, "semver-diff": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", @@ -10968,6 +11047,21 @@ } } }, + "topo": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/topo/-/topo-1.1.0.tgz", + "integrity": "sha1-6ddRYV0buH3IZdsYL6HKCl71NtU=", + "requires": { + "hoek": "2.16.3" + }, + "dependencies": { + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + } + } + }, "toposort-class": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", @@ -11551,6 +11645,20 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": "1.2.4", + "xmlbuilder": "9.0.7" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", diff --git a/package.json b/package.json index 5108b18..877bf98 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,11 @@ "helmet": "^3.12.0", "jest": "^22.4.3", "jsonwebtoken": "^8.2.1", + "libphonenumber-js": "^1.1.10", "lodash": "^4.17.5", "morgan": "^1.9.0", + "nexmo": "^2.2.0", + "nodemailer": "^4.6.4", "pg": "^7.4.1", "pg-hstore": "^2.3.2", "sequelize": "^4.37.6" diff --git a/server/controllers/event.js b/server/controllers/event.js index 973350a..12eb46c 100644 --- a/server/controllers/event.js +++ b/server/controllers/event.js @@ -1,8 +1,9 @@ import db from '../models'; +import mailer from './mailer'; import serverError from '../errorHandler/serverError'; import validateAddEvent from '../validators/validateAddEvent'; -const { Event, Center } = db; +const { Event, Center, User } = db; class Events { static addEvent(request, response) { @@ -93,6 +94,7 @@ class Events { }); }); } + static deleteEvent(request, response) { Event.findById(request.params.eventId) .then((event) => { @@ -101,17 +103,31 @@ class Events { message: 'No event found.' }); } - if (event.organizer != request.userDetails.id) { + if (event.organizer != request.userDetails.id + && request.userDetails.username !== 'adminuser') { return response.status(401).json({ message: 'You do not have the privilege to modify this resource' }); } - return event.destroy().then(() => { - response.status(200).json({ - message: 'Event successfully deleted.', - eventDetails: event + User.findById(event.organizer) + .then((foundUser) => { + return event.destroy().then(() => { + const mailOptions = { + from: process.env.EMAIL_ADDRESS, + to: foundUser.dataValues.email, + subject: 'Your event has been cancelled' + }; + response.status(200).json({ + message: 'Event successfully deleted.', + eventDetails: event + }); + mailer.sendMail(mailOptions, (err) => { + if (err) { + return err; + } + }); + }); }); - }); }).catch(() => { return response.status(500).json({ message: serverError diff --git a/server/controllers/mailer.js b/server/controllers/mailer.js new file mode 100644 index 0000000..9a20ff8 --- /dev/null +++ b/server/controllers/mailer.js @@ -0,0 +1,15 @@ +import nodemailer from 'nodemailer'; + +const mailer = nodemailer.createTransport({ + service: 'Gmail', + host: 'smtp.gmail.com', + port: 587, + secure: false, + requireTLS: true, + auth: { + user: process.env.EMAIL_ADDRESS, + pass: process.env.PASSWORD, + } +}); + +export default mailer; diff --git a/server/controllers/user.js b/server/controllers/user.js index b4f00fa..43c1c74 100644 --- a/server/controllers/user.js +++ b/server/controllers/user.js @@ -27,13 +27,15 @@ class Users { User.create({ username: request.body.username, email: request.body.email, - password: hashedPassword + password: hashedPassword, + phoneNumber: request.body.phoneNumber }) .then((newUser) => { const userToken = jwt.sign( { id: newUser.id, - username: newUser.username + username: newUser.username, + email: newUser.email }, process.env.SECRET, { expiresIn: '10h' } @@ -46,6 +48,7 @@ class Users { }); }) .catch((error) => { + console.log(error); if (error.errors[0].message === 'username must be unique') { return response.status(409).json({ message: 'This username is already taken' @@ -84,7 +87,8 @@ class Users { const userToken = jwt.sign( { id: user.id, - username: user.username + username: user.username, + email: user.email }, process.env.SECRET, { expiresIn: '10h' } diff --git a/server/migrations/20180418075722-add-phone-number.js b/server/migrations/20180418075722-add-phone-number.js new file mode 100644 index 0000000..793ea21 --- /dev/null +++ b/server/migrations/20180418075722-add-phone-number.js @@ -0,0 +1,26 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.createTable('users', { id: Sequelize.INTEGER }); + */ + return queryInterface.addColumn('Users', 'phoneNumber', { + type: Sequelize.INTEGER + }); + }, + + down: (queryInterface, Sequelize) => { + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.dropTable('users'); + */ + } +}; diff --git a/server/migrations/20180418081732-dropPhone.js b/server/migrations/20180418081732-dropPhone.js new file mode 100644 index 0000000..56ee4e5 --- /dev/null +++ b/server/migrations/20180418081732-dropPhone.js @@ -0,0 +1,25 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.createTable('users', { id: Sequelize.INTEGER }); + */ + return queryInterface.removeColumn('Users', 'phoneNumber'); + }, + + + down: (queryInterface, Sequelize) => { + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.dropTable('users'); + */ + } +}; diff --git a/server/migrations/20180418081852-readdphone.js b/server/migrations/20180418081852-readdphone.js new file mode 100644 index 0000000..4b503eb --- /dev/null +++ b/server/migrations/20180418081852-readdphone.js @@ -0,0 +1,26 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.createTable('users', { id: Sequelize.INTEGER }); + */ + return queryInterface.addColumn('Users', 'phoneNumber', { + type: Sequelize.BIGINT + }); + }, + + down: (queryInterface, Sequelize) => { + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.dropTable('users'); + */ + } +}; diff --git a/server/models/user.js b/server/models/user.js index 9651d45..b97a046 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -13,6 +13,9 @@ module.exports = (sequelize, DataTypes) => { isEmail: true } }, + phoneNumber: { + type: DataTypes.INTEGER + }, password: { allowNull: false, type: DataTypes.STRING diff --git a/server/seeders/20180409073557-user-seed.js b/server/seeders/20180409073557-user-seed.js index d54babc..8b60ec4 100644 --- a/server/seeders/20180409073557-user-seed.js +++ b/server/seeders/20180409073557-user-seed.js @@ -7,6 +7,7 @@ module.exports = { username: 'adminuser', email: 'admin@localhost.com', password: 'qwertyuiop', + phoneNumber: '08012345678', createdAt: '2018-03-05 12:01:18.936+01', updatedAt: '2018-03-05 12:01:18.936+01' }, @@ -14,6 +15,7 @@ module.exports = { username: 'piedpiper', email: 'qwertyuiop@gmail.com', password: 'qwertyuiop', + phoneNumber: '08012345678', createdAt: '2018-03-05 12:01:18.936+01', updatedAt: '2018-03-05 12:01:18.936+01' }, @@ -21,6 +23,7 @@ module.exports = { username: 'sadamhussein', email: 'sadamhussein@gmail.com', password: 'qwertyuiop', + phoneNumber: '08012345678', createdAt: '2018-03-05 12:01:18.936+01', updatedAt: '2018-03-05 12:01:18.936+01' } diff --git a/server/tests/seed/userseed.js b/server/tests/seed/userseed.js index d3d6311..60769f3 100644 --- a/server/tests/seed/userseed.js +++ b/server/tests/seed/userseed.js @@ -8,6 +8,7 @@ const dummyUser = { id: dummyUserID, username: 'davyjones', email: 'davyjones@gmail.com', + phoneNumber: '08012345678', password: 'qwertyuiop', token: jwt.sign({ id: dummyUserID, @@ -20,6 +21,7 @@ export const secondDummyUser = { username: 'secondDummyUser', email: 'secondDummyUserEmail@gmail.com', password: 'qwertyuiop', + phoneNumber: '08012345678', token: jwt.sign({ id: secondDummyUserId, username: 'secondDummyUser' @@ -32,6 +34,7 @@ export const adminUser = { username: 'adminuser', email: 'admin@localhost.com', password: 'qwertyuiop', + phoneNumber: '08012345678', token: jwt.sign({ id: adminUserID, username: 'adminuser' diff --git a/server/tests/user.test.js b/server/tests/user.test.js index 178b91a..132ed2d 100644 --- a/server/tests/user.test.js +++ b/server/tests/user.test.js @@ -99,7 +99,8 @@ describe('Integration tests for Authentication', () => { const testUser = { username: 'randomuser', email: 'qwertyuiop@gmail.com', - password: 'qwertyuiop' + password: 'qwertyuiop', + phoneNumber: '08012345678' }; request.post(signupAPI) .set('Connection', 'keep alive') @@ -119,7 +120,8 @@ describe('Integration tests for Authentication', () => { const testUser = { username: 'piedpiper', email: 'randomemail@gmail.com', - password: 'qwertyuiop' + password: 'qwertyuiop', + phoneNumber: '08012345678' }; request.post(signupAPI) .set('Connection', 'keep alive') @@ -152,7 +154,8 @@ describe('Integration tests for Authentication', () => { (done) => { const testUser = { email: dummyUser.email, - password: dummyUser.password + password: dummyUser.password, + phoneNumber: dummyUser.phoneNumber }; delete testUser.email; request.post(signinAPI) @@ -174,7 +177,8 @@ describe('Integration tests for Authentication', () => { (done) => { const testUser = { email: dummyUser.email, - password: dummyUser.password + password: dummyUser.password, + phoneNumber: dummyUser.phoneNumber }; delete testUser.password; request.post(signinAPI) @@ -193,7 +197,8 @@ describe('Integration tests for Authentication', () => { (done) => { const testUser = { email: dummyUser.email, - password: dummyUser.password + password: dummyUser.password, + phoneNumber: dummyUser.phoneNumber }; testUser.password = 'zxcvbnmasdf'; request.post(signinAPI) diff --git a/server/validators/validateSignup.js b/server/validators/validateSignup.js index 4c271a3..d25301b 100644 --- a/server/validators/validateSignup.js +++ b/server/validators/validateSignup.js @@ -1,4 +1,5 @@ import { isEmpty } from 'lodash'; +import { isValidNumber } from 'libphonenumber-js'; const validateEmail = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/; @@ -25,6 +26,12 @@ const validateSignup = (userData) => { ) { errors.password = 'Your password should be at least 8 characters long.'; } + if (userData.phoneNumber === undefined || + userData.phoneNumber.trim() === '' || + isValidNumber({ phone: userData.phoneNumber, country: 'NG' }) !== true + ) { + errors.phoneNumber = 'Please input a valid phone-number'; + } return { errors,