From c4b47c6c60ed872a0c8725b9415698b251d2c592 Mon Sep 17 00:00:00 2001 From: Adedamola Salisu Date: Fri, 1 Mar 2019 17:57:07 +0100 Subject: [PATCH 1/6] feat(social): enable social login [Finishes #164198492] --- .env.sample | 9 ++++ .travis.yml | 10 ++++ package.json | 9 +++- server/app.js | 13 +++++ server/config/passport.js | 108 +++++++++++++++++++++++++++++-------- server/controllers/user.js | 28 ++++++++++ server/routes/users.js | 20 +++++++ server/test/user.spec.js | 25 +++++++++ swagger.yaml | 36 +++++++++++++ 9 files changed, 233 insertions(+), 25 deletions(-) diff --git a/.env.sample b/.env.sample index 0e29b54..9fa5ad2 100644 --- a/.env.sample +++ b/.env.sample @@ -2,3 +2,12 @@ DATABASE_URL= SECRET_KEY= PORT= SENDGRID_API_KEY= +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_RETURN_URL= +TWITTER_CONSUMER_KEY = +TWITTER_CONSUMER_SECRET = +TWITTER_RETURN_URL= +FACEBOOK_APP_ID = +FACEBOOK_APP_SECRET = +FACEBOOK_RETURN_URL= \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 8d5807f..b680823 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,16 @@ cache: script: - export DATABASE_URL="postgres://postgres@127.0.0.1:5432/artemisah" + - export GOOGLE_CLIENT_ID="53636154845-afrnvoq70t0mbceslfrrdegpmobvqcnh.apps.googleusercontent.com" + - export GOOGLE_CLIENT_SECRET="Brnx1TxmaUZlhAk0RJYMmxi_" + - export GOOGLE_RETURN_URL="/api/users/auth/google/redirect" + - export TWITTER_CONSUMER_KEY = "fI361EhD3pB8ye7wEQHDezrQi" + - export TWITTER_CONSUMER_SECRET = "ItJz0SHSXstsQINs4MMZDkQaAaPAvccfBTeGKEQjQlFcGzau0s" + - export TWITTER_RETURN_URL="/api/users/auth/twitter/redirect" + - export FACEBOOK_APP_ID = "775660842814342" + - export FACEBOOK_APP_SECRET = "f63a934f47f2499286c251cc530b443c" + - export FACEBOOK_RETURN_URL="/api/users/auth/facebook/redirect" + - export SESSION_SECRET= "asdfghjhkjlqwer" - npm test - npm run coveralls diff --git a/package.json b/package.json index 9b57e4d..a1c6d52 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,16 @@ "description": "A Social platform for the creative at heart", "main": "index.js", "scripts": { - "start": "babel-node ./server/app.js", + "start": " node ./dist/app.js", "dev": "nodemon --exec babel-node ./server/app.js", "test": "npm run reset:db && nyc mocha --require @babel/polyfill --require @babel/register ./server/test/ --timeout 10000 --exit", "sequelize": "./node_modules/.bin/babel-node ./node_modules/.bin/sequelize $*", "migrate": "./node_modules/.bin/babel-node ./node_modules/.bin/sequelize db:migrate", "coveralls": "nyc report --reporter=text-lcov | coveralls", "reset:db": "npm run sequelize db:migrate:undo:all && npm run sequelize db:migrate", - "start:dev": "npm run reset:db && npm run dev" + "start:dev": "npm run reset:db && npm run dev", + "build": "rm -rf dist && mkdir dist && babel -d ./dist ./server -s", + "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm run build" }, "author": "Andela Simulations Programme", "license": "MIT", @@ -41,6 +43,9 @@ "nyc": "^13.3.0", "passport": "^0.4.0", "passport-local": "^1.0.0", + "passport-facebook": "^3.0.0", + "passport-google-oauth20": "^1.0.0", + "passport-twitter": "^1.0.4", "pg": "^7.8.1", "request": "^2.87.0", "sendgrid": "^5.2.3", diff --git a/server/app.js b/server/app.js index e9d65bf..ac0c11f 100644 --- a/server/app.js +++ b/server/app.js @@ -2,8 +2,13 @@ import express from 'express'; import bodyParser from 'body-parser'; import logger from 'morgan'; import cors from 'cors'; +import passport from 'passport'; +import dotenv from 'dotenv'; +import expressSession from 'express-session'; import routes from './routes'; +dotenv.config(); + // Set up the express app const app = express(); @@ -16,6 +21,14 @@ app.use(logger('dev')); // Parse incoming requests data app.use(bodyParser.json()); +// Initializing Passport +app.use(passport.initialize()); + +// Creating user session +app.use(expressSession( + { secret: process.env.SESSION_SECRET, resave: true, saveUninitialized: true } +)); + // Setup a default catch-all route that sends back a welcome message in JSON format. app.get('/', (req, res) => res.status(200).send({ message: 'Authors Haven.', diff --git a/server/config/passport.js b/server/config/passport.js index 3b1d769..fd08550 100644 --- a/server/config/passport.js +++ b/server/config/passport.js @@ -1,26 +1,88 @@ -const passport = require("passport"); -const LocalStrategy = require("passport-local").Strategy; -const mongoose = require("mongoose"); -const User = mongoose.model("User"); +import passport from 'passport'; +import passportGoogle from 'passport-google-oauth20'; +import passportTwitter from 'passport-twitter'; +import passportFacebook from 'passport-facebook'; +import dotenv from 'dotenv'; +import db from '../database/models'; + +const { User } = db; + +const GoogleStrategy = passportGoogle.Strategy; +const TwitterStrategy = passportTwitter.Strategy; +const FacebookStrategy = passportFacebook.Strategy; + +dotenv.config(); + +let trustProxy = false; + +if (process.env.DYNO) { + trustProxy = true; +} + +const googleClientId = process.env.GOOGLE_CLIENT_ID; +const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET; +const googleReturnUrl = process.env.GOOGLE_RETURN_URL; +const twitterConsumerKey = process.env.TWITTER_CONSUMER_KEY; +const twitterConsumerSecret = process.env.TWITTER_CONSUMER_SECRET; +const twitterReturnUrl = process.env.TWITTER_RETURN_URL; +const facebookAppId = process.env.FACEBOOK_APP_ID; +const facebookAppSecret = process.env.FACEBOOK_APP_SECRET; +const facebookReturnUrl = process.env.FACEBOOK_RETURN_URL; + +const handleSocialLogin = (email, firstname, lastname, username, photo, cb) => { + User.findOne({ where: { email } }) + .then((existingUser) => { + if (existingUser) { + return cb(null, { + data: existingUser.dataValues + }); + } + User.create({ + email, + firstname, + lastname, + username: username ? `${username}-${new Date().getTime()}` : `${firstname ? firstname.toLowerCase() : email}${lastname ? lastname.toLowerCase() : ''}-${new Date().getTime()}`, + image: photo, + }).then(user => cb(null, { data: user.dataValues })); + }); +}; passport.use( - new LocalStrategy( - { - usernameField: "user[email]", - passwordField: "user[password]" - }, - function(email, password, done) { - User.findOne({ email: email }) - .then(function(user) { - if (!user || !user.validPassword(password)) { - return done(null, false, { - errors: { "email or password": "is invalid" } - }); - } - - return done(null, user); - }) - .catch(done); - } - ) + new GoogleStrategy({ + clientID: googleClientId, + clientSecret: googleClientSecret, + callbackURL: googleReturnUrl + }, (accessToken, refreshToken, profile, cb) => { + const { name, emails, photos } = profile; + handleSocialLogin(emails[0].value, name.givenName, name.familyName, null, photos[0].value, cb); + }) ); + +passport.use(new TwitterStrategy({ + consumerKey: twitterConsumerKey, + consumerSecret: twitterConsumerSecret, + callbackURL: twitterReturnUrl, + userProfileURL: 'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true', + includeEmail: true, + proxy: trustProxy +}, +(token, tokenSecret, profile, cb) => { + const { username, emails, photos } = profile; + handleSocialLogin(emails[0].value, null, null, username, photos[0].value, cb); +})); + +passport.use(new FacebookStrategy({ + clientID: facebookAppId, + clientSecret: facebookAppSecret, + callbackURL: facebookReturnUrl, + profileFields: ['id', 'displayName', 'photos', 'email'] +}, +(accessToken, refreshToken, profile, cb) => { + const { displayName, photos, emails } = profile; + const splitnames = displayName.split(' '); + const firstname = splitnames[0]; + const lastname = splitnames.length > 1 ? splitnames[1] : ''; + handleSocialLogin(emails[0].value, firstname, lastname, null, photos[0].value, cb); +})); + +export default passport; diff --git a/server/controllers/user.js b/server/controllers/user.js index c621c30..fe70f5e 100644 --- a/server/controllers/user.js +++ b/server/controllers/user.js @@ -91,4 +91,32 @@ export default class Users { res.status(400).json({ message: 'invalid email' }); } } + + /** +* @description This controller method completes the social sign in process +* +* @param {object} req - Express request object +* @param {object} res - Express response object +* @return {undefined} +*/ + static async socialLogin(req, res) { + const { data } = req.user; + + const userToken = await HelperUtils.generateToken(data); + + const { + email, username, bio, image + } = data; + + response(res).success({ + message: 'user logged in successfully', + user: { + email, + username, + bio, + image, + token: userToken, + } + }); + } } diff --git a/server/routes/users.js b/server/routes/users.js index d6ea6cb..331d16d 100644 --- a/server/routes/users.js +++ b/server/routes/users.js @@ -1,9 +1,29 @@ import express from 'express'; import { Users } from '../controllers'; +import passport from '../config/passport'; const authRoute = express.Router(); authRoute.post('/users', Users.signupUser); authRoute.get('/users/verifyemail', Users.verifyUserEmail); +authRoute.get('/users/auth/google', passport.authenticate('google', { + scope: [ + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/userinfo.email' + ] +})); + +authRoute.get('/users/auth/google/redirect', passport.authenticate('google', { session: false }), Users.socialLogin); +authRoute.get('/users/auth/facebook', passport.authenticate('facebook', { + scope: ['email'] +})); + +authRoute.get('/users/auth/facebook/redirect', passport.authenticate('facebook', { session: false }), Users.socialLogin); + +authRoute.get('/users/auth/twitter', passport.authenticate('twitter', { + scope: ['include_email=true'] +})); + +authRoute.get('/users/auth/twitter/redirect', passport.authenticate('twitter', { session: false }), Users.socialLogin); export default authRoute; diff --git a/server/test/user.spec.js b/server/test/user.spec.js index 1b6df7e..a286989 100644 --- a/server/test/user.spec.js +++ b/server/test/user.spec.js @@ -65,3 +65,28 @@ describe('Test signup endpoint and email verification endpoint', () => { }); }); }); + +describe('Social Login with Google', () => { + it('should return the google authentication webpage', (done) => { + chai + .request(app) + .get(`${signupURL}/auth/google`) + .end((err, res) => { + expect(res.redirects[0]).to.contain('https://accounts.google.com/o/oauth2'); + done(); + }); + }); +}); + +describe('Social Login with Facebook', () => { + it('should return the facebook authentication webpage', (done) => { + chai + .request(app) + .get(`${signupURL}/auth/facebook`) + .end((err, res) => { + expect(res.redirects[0]).to.contain('https://www.facebook.com'); + expect(res.redirects[0]).to.contain('oauth'); + done(); + }); + }); +}); diff --git a/swagger.yaml b/swagger.yaml index d42c9d9..1403fda 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -128,6 +128,42 @@ paths: description: "email verified successfully" 400: description: "invalid email" + + /users/auth/google: + get: + tags: + - "users" + summary: "Signs users in with google" + description: "Redirects to google authentication page to seek their permission to share their information with the platform" + produces: + - "text/html" + responses: + 200: + description: "user logged in successfully" + + /users/auth/facebook: + get: + tags: + - "users" + summary: "Signs users in with facebook" + description: "Redirects to facebook authentication page to seek their permission to share their information with the platform" + produces: + - "text/html" + responses: + 200: + description: "user logged in successfully" + + /users/auth/twitter: + get: + tags: + - "users" + summary: "Signs users in with twitter" + description: "Redirects to twitter authentication page to seek their permission to share their information with the platform" + produces: + - "text/html" + responses: + 200: + description: "user logged in successfully" securityDefinitions: petstore_auth: From 8c88b1e28fccd5217d03934348cfde026a97c5da Mon Sep 17 00:00:00 2001 From: Adedamola Salisu Date: Fri, 1 Mar 2019 18:00:08 +0100 Subject: [PATCH 2/6] fix(travis): remove space in travis export scripts --- .travis.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index b680823..efb2771 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,13 +12,13 @@ script: - export GOOGLE_CLIENT_ID="53636154845-afrnvoq70t0mbceslfrrdegpmobvqcnh.apps.googleusercontent.com" - export GOOGLE_CLIENT_SECRET="Brnx1TxmaUZlhAk0RJYMmxi_" - export GOOGLE_RETURN_URL="/api/users/auth/google/redirect" - - export TWITTER_CONSUMER_KEY = "fI361EhD3pB8ye7wEQHDezrQi" - - export TWITTER_CONSUMER_SECRET = "ItJz0SHSXstsQINs4MMZDkQaAaPAvccfBTeGKEQjQlFcGzau0s" + - export TWITTER_CONSUMER_KEY="fI361EhD3pB8ye7wEQHDezrQi" + - export TWITTER_CONSUMER_SECRET="ItJz0SHSXstsQINs4MMZDkQaAaPAvccfBTeGKEQjQlFcGzau0s" - export TWITTER_RETURN_URL="/api/users/auth/twitter/redirect" - - export FACEBOOK_APP_ID = "775660842814342" - - export FACEBOOK_APP_SECRET = "f63a934f47f2499286c251cc530b443c" + - export FACEBOOK_APP_ID="775660842814342" + - export FACEBOOK_APP_SECRET="f63a934f47f2499286c251cc530b443c" - export FACEBOOK_RETURN_URL="/api/users/auth/facebook/redirect" - - export SESSION_SECRET= "asdfghjhkjlqwer" + - export SESSION_SECRET="asdfghjhkjlqwer" - npm test - npm run coveralls From 0df03b84c9b90e89b976aeca7f553df57fc1bf6b Mon Sep 17 00:00:00 2001 From: Ayo-Oluwa Adebayo Date: Sat, 2 Mar 2019 11:48:06 +0100 Subject: [PATCH 3/6] feat(sendgrid): Reset password via email - send reset password link to user's email - link should authenticate user email and update table with new password --- package.json | 2 +- server/controllers/user.js | 94 +++++++++++++++++++++ server/routes/users.js | 2 + server/test/user.spec.js | 82 +++++++++++++++++- server/utils/markups/passwordResetMarkup.js | 89 +++++++++++++++++++ 5 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 server/utils/markups/passwordResetMarkup.js diff --git a/package.json b/package.json index 2384004..bffad65 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "migrate": "./node_modules/.bin/babel-node ./node_modules/.bin/sequelize db:migrate", "coveralls": "nyc report --reporter=text-lcov | coveralls", "reset:db": "npm run sequelize db:migrate:undo:all && npm run sequelize db:migrate", - "start:dev":"npm run reset:db && npm run dev" + "start:dev": "npm run reset:db && npm run dev" }, "author": "Andela Simulations Programme", "license": "MIT", diff --git a/server/controllers/user.js b/server/controllers/user.js index c621c30..e433cb5 100644 --- a/server/controllers/user.js +++ b/server/controllers/user.js @@ -4,6 +4,7 @@ import db from '../database/models'; import { HelperUtils } from '../utils'; import response from '../utils/response'; import verifyEmailMarkup from '../utils/markups/emailVerificationMarkup'; +import passwordResetMarkup from '../utils/markups/passwordResetMarkup'; import '@babel/polyfill'; const { User } = db; @@ -91,4 +92,97 @@ export default class Users { res.status(400).json({ message: 'invalid email' }); } } + + /** + * @description This controller method sends password reset link e-mail + * + * @param {object} req - Express request object + * @param {object} res - Express response object + * @returns {object} Json response + */ + static async resetPasswordEmail(req, res) { + const { email } = req.body; + + const hashedEmail = HelperUtils.hashPassword(email); + + try { + const user = await User.findOne({ + where: { email } + }); + + if (user === null) { + response(res).notFound({ + message: 'user not found in our records' + }); + } else { + HelperUtils.sendMail( + email, + 'Authors Haven ', + 'Password Reset', + 'Reset Password', + passwordResetMarkup(user.firstname, email, hashedEmail) + ); + response(res).success({ + message: 'Please, verify password reset link in your email box' + }); + } + } catch (err) { + response(res).sendData(400, { + message: err + }); + } + } + + /** + * @description This controller method resets user password + * + * @param {object} req - Express request object + * @param {object} res - Express response object + * @return {object} Json response + */ + static async resetPassword(req, res) { + const { newPassword, confirmPassword } = req.body; + const isPassword = newPassword === confirmPassword; + + if (!isPassword) { + response(res).sendData(400, { + message: 'The supplied passwords do not match' + }); + } + + try { + const hashPassword = HelperUtils.hashPassword(newPassword); + + const { email, hash } = req.query; + const isEmail = await HelperUtils.comparePasswordOrEmail(email, hash); + console.log(isEmail); + + if (isEmail) { + const user = await User.findOne({ + where: { email } + }); + + if (!user) { + response(res).notFound({ + message: 'User not found' + }) + } else { + await user.update({ + password: hashPassword + }); + response(res).success({ + message: 'Password reset successful. Please, login using your new password.' + }); + } + } else { + response(res).sendData(400, { + message: 'Invalid password reset link' + }) + } + } catch (err) { + response(res).sendData(400, { + message: err + }); + } + } } diff --git a/server/routes/users.js b/server/routes/users.js index d6ea6cb..624e959 100644 --- a/server/routes/users.js +++ b/server/routes/users.js @@ -5,5 +5,7 @@ const authRoute = express.Router(); authRoute.post('/users', Users.signupUser); authRoute.get('/users/verifyemail', Users.verifyUserEmail); +authRoute.post('/users/reset-password', Users.resetPasswordEmail); +authRoute.patch('/users/reset-password', Users.resetPassword); export default authRoute; diff --git a/server/test/user.spec.js b/server/test/user.spec.js index 1b6df7e..a6e5730 100644 --- a/server/test/user.spec.js +++ b/server/test/user.spec.js @@ -4,9 +4,15 @@ import app from '../app'; chai.use(chaiHttp); +const QueryURL = '?email=nwabuzor.obiora@gmail.com&hash=$2a$08$vu6Gwj1EgU7/6IJv6juphuraxOv6tOHaeNOvWmsjh0oYHOLRO8/9q'; +const invalidQueryURL = '?email=invalid.obiora@gmail.com&hash=$2a$08$vu6Gwj1EgU7/6IJv6juphuraxOv6tOHaeNOvWmsjh0oYHOLRO8/9q'; const signupURL = '/api/users'; -const verifyURL = '/api/users/verifyemail?email=nwabuzor.obiora@gmail.com&hash=$2a$08$vu6Gwj1EgU7/6IJv6juphuraxOv6tOHaeNOvWmsjh0oYHOLRO8/9q'; -const invalidVerifyURL = '/api/users/verifyemail?email=invalid.obiora@gmail.com&hash=$2a$08$vu6Gwj1EgU7/6IJv6juphuraxOv6tOHaeNOvWmsjh0oYHOLRO8/9q'; +const resetPassword = '/api/users/reset-password'; +const resetPasswordURL = '/api/users/reset-password' + QueryURL; +const invalidResetPasswordURL = '/api/users/reset-password' + invalidQueryURL; +const verifyURL = '/api/users/verifyemail' + QueryURL; +const invalidVerifyURL = '/api/users/verifyemail' + invalidQueryURL; + describe('Test signup endpoint and email verification endpoint', () => { it('It should return a 404 if user don\'t exist during email verification', (done) => { @@ -65,3 +71,75 @@ describe('Test signup endpoint and email verification endpoint', () => { }); }); }); + +describe('Test reset password mail endpoint and password link endpoint', () => { + it('It should return a 404 if user records not found', (done) => { + const data = {email: 'ayo-oluwa.adebayo@andela.com'} + chai + .request(app) + .post(resetPassword) + .send(data) + .end((err, res) => { + expect(res.status).to.equal(404); + expect(res.body.message).to.be.a('string'); + expect(res.body.message).to.equal('user not found in our records'); + done(); + }); + }); + + it('It should return a 200 if user email is found in the database', (done) => { + const data = {email: 'nwabuzor.obiora@gmail.com'} + chai + .request(app) + .post(resetPassword) + .send(data) + .end((err, res) => { + expect(res.status).to.equal(200); + expect(res.body.message).to.be.a('string'); + expect(res.body.message).to.equal('Please, verify password reset link in your email box'); + done(); + }); + }); + + it('It should return a 400 if user passwords do not match', (done) => { + const data = {newPassword: 'hello', confirmPassword: 'hell'} + chai + .request(app) + .patch(resetPasswordURL) + .send(data) + .end((err, res) => { + expect(res.status).to.equal(400); + expect(res.body.message).to.be.a('string'); + expect(res.body.message).to.equal('The supplied passwords do not match'); + done(); + }); + }); + + it('It should return a 200 if user passwords match', (done) => { + const data = {newPassword: 'hello', confirmPassword: 'hello'} + chai + .request(app) + .patch(resetPasswordURL) + .send(data) + .end((err, res) => { + expect(res.status).to.equal(200); + expect(res.body.message).to.be.a('string'); + expect(res.body.message).to.equal('Password reset successful. Please, login using your new password.'); + done(); + }); + }); + + it('It should return a 400 if reset link is invalid', (done) => { + const data = {newPassword: 'hello', confirmPassword: 'hello'} + chai + .request(app) + .patch(invalidResetPasswordURL) + .send(data) + .end((err, res) => { + expect(res.status).to.equal(400); + expect(res.body.message).to.be.a('string'); + expect(res.body.message).to.equal('Invalid password reset link'); + done(); + }); + }); +}); diff --git a/server/utils/markups/passwordResetMarkup.js b/server/utils/markups/passwordResetMarkup.js new file mode 100644 index 0000000..8c3fcd5 --- /dev/null +++ b/server/utils/markups/passwordResetMarkup.js @@ -0,0 +1,89 @@ +const passwordResetMarkup = (username, email, hash) => ( + ` + + + + + + Authors Haven + + + + +
+
+ AH_logo +

Authors Haven

+
+
+

Hello ${username},

+

+ You recently requested to reset your password for your Authors Haven account. +


+

Please, click the button below to proceed.

+
+ + Reset Password + +
+ + +` +); + +export default passwordResetMarkup; From 8e1243bbd4c18a664956241cd352744793d9f67d Mon Sep 17 00:00:00 2001 From: Adedamola Salisu Date: Sat, 2 Mar 2019 22:57:14 +0100 Subject: [PATCH 4/6] fix(sociallogin): refactor code - refactor promises to use async await - add server error response to helper util file - wrap all asynchronous code in try/catch block --- server/config/passport.js | 29 +++++++++++++++------------- server/controllers/user.js | 39 ++++++++++++++++++++++---------------- server/utils/response.js | 11 ++++++++++- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/server/config/passport.js b/server/config/passport.js index fd08550..e9b8ed1 100644 --- a/server/config/passport.js +++ b/server/config/passport.js @@ -29,22 +29,23 @@ const facebookAppId = process.env.FACEBOOK_APP_ID; const facebookAppSecret = process.env.FACEBOOK_APP_SECRET; const facebookReturnUrl = process.env.FACEBOOK_RETURN_URL; -const handleSocialLogin = (email, firstname, lastname, username, photo, cb) => { - User.findOne({ where: { email } }) - .then((existingUser) => { - if (existingUser) { - return cb(null, { - data: existingUser.dataValues - }); - } - User.create({ +const handleSocialLogin = async (email, firstname, lastname, username, photo, cb) => { + try{ + const existingUser = await User.findOne({ where: { email } }) + return cb(null, { + data: existingUser.dataValues + }); + } + catch{ + const user = await User.create({ email, firstname, lastname, username: username ? `${username}-${new Date().getTime()}` : `${firstname ? firstname.toLowerCase() : email}${lastname ? lastname.toLowerCase() : ''}-${new Date().getTime()}`, image: photo, - }).then(user => cb(null, { data: user.dataValues })); - }); + }) + return cb(null, { data: user.dataValues }) + } }; passport.use( @@ -69,7 +70,8 @@ passport.use(new TwitterStrategy({ (token, tokenSecret, profile, cb) => { const { username, emails, photos } = profile; handleSocialLogin(emails[0].value, null, null, username, photos[0].value, cb); -})); +}) +); passport.use(new FacebookStrategy({ clientID: facebookAppId, @@ -83,6 +85,7 @@ passport.use(new FacebookStrategy({ const firstname = splitnames[0]; const lastname = splitnames.length > 1 ? splitnames[1] : ''; handleSocialLogin(emails[0].value, firstname, lastname, null, photos[0].value, cb); -})); +}) +); export default passport; diff --git a/server/controllers/user.js b/server/controllers/user.js index fe70f5e..f33e8bd 100644 --- a/server/controllers/user.js +++ b/server/controllers/user.js @@ -102,21 +102,28 @@ export default class Users { static async socialLogin(req, res) { const { data } = req.user; - const userToken = await HelperUtils.generateToken(data); - - const { - email, username, bio, image - } = data; - - response(res).success({ - message: 'user logged in successfully', - user: { - email, - username, - bio, - image, - token: userToken, - } - }); + try{ + const userToken = await HelperUtils.generateToken(data); + + const { + email, username, bio, image + } = data; + + response(res).success({ + message: 'user logged in successfully', + user: { + email, + username, + bio, + image, + token: userToken, + } + }); + } + catch{ + response(res).serverError({ + message: 'token could not be generated, please try again later' + }); + } } } diff --git a/server/utils/response.js b/server/utils/response.js index 67ec336..6911efe 100644 --- a/server/utils/response.js +++ b/server/utils/response.js @@ -61,13 +61,22 @@ export default (res) => { /** * Sends status 400 and `data` to the client - * + * * @param {*} data */ badRequest(data) { this.sendData(400, data); }, + /** + * Sends status 500 and `data` to the client + * Should be sent when there's a server error + * @param {*} data + */ + serverError(data) { + this.sendData(500, data); + }, + /** * Send data to the client. * From ab0e3116e7b425ad32092ea4bfd7535dddb44c5a Mon Sep 17 00:00:00 2001 From: Ayo-Oluwa Adebayo Date: Mon, 4 Mar 2019 11:57:17 +0100 Subject: [PATCH 5/6] fix(): Refactor code to pass test --- server/controllers/user.js | 16 +++++++++------- server/test/user.spec.js | 18 +++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/server/controllers/user.js b/server/controllers/user.js index 78ec8af..1aff987 100644 --- a/server/controllers/user.js +++ b/server/controllers/user.js @@ -185,13 +185,14 @@ export default class Users { } } -/** -* @description This controller method completes the social sign in process -* -* @param {object} req - Express request object -* @param {object} res - Express response object -* @return {undefined} -*/ + + /** + * @description This controller method completes the social sign in process + * + * @param {object} req - Express request object + * @param {object} res - Express response object + * @return {undefined} + */ static async socialLogin(req, res) { const { data } = req.user; @@ -219,3 +220,4 @@ export default class Users { }); } } +} diff --git a/server/test/user.spec.js b/server/test/user.spec.js index ea70e1a..701e03b 100644 --- a/server/test/user.spec.js +++ b/server/test/user.spec.js @@ -8,10 +8,10 @@ const QueryURL = '?email=nwabuzor.obiora@gmail.com&hash=$2a$08$vu6Gwj1EgU7/6IJv6 const invalidQueryURL = '?email=invalid.obiora@gmail.com&hash=$2a$08$vu6Gwj1EgU7/6IJv6juphuraxOv6tOHaeNOvWmsjh0oYHOLRO8/9q'; const signupURL = '/api/users'; const resetPassword = '/api/users/reset-password'; -const resetPasswordURL = '/api/users/reset-password' + QueryURL; -const invalidResetPasswordURL = '/api/users/reset-password' + invalidQueryURL; -const verifyURL = '/api/users/verifyemail' + QueryURL; -const invalidVerifyURL = '/api/users/verifyemail' + invalidQueryURL; +const resetPasswordURL = `/api/users/reset-password${QueryURL}`; +const invalidResetPasswordURL = `/api/users/reset-password${invalidQueryURL}`; +const verifyURL = `/api/users/verifyemail${QueryURL}`; +const invalidVerifyURL = `/api/users/verifyemail${invalidQueryURL}`; describe('Test signup endpoint and email verification endpoint', () => { @@ -74,7 +74,7 @@ describe('Test signup endpoint and email verification endpoint', () => { describe('Test reset password mail endpoint and password link endpoint', () => { it('It should return a 404 if user records not found', (done) => { - const data = {email: 'ayo-oluwa.adebayo@andela.com'} + const data = { email: 'ayo-oluwa.adebayo@andela.com' } chai .request(app) .post(resetPassword) @@ -88,7 +88,7 @@ describe('Test reset password mail endpoint and password link endpoint', () => { }); it('It should return a 200 if user email is found in the database', (done) => { - const data = {email: 'nwabuzor.obiora@gmail.com'} + const data = { email: 'nwabuzor.obiora@gmail.com' } chai .request(app) .post(resetPassword) @@ -102,7 +102,7 @@ describe('Test reset password mail endpoint and password link endpoint', () => { }); it('It should return a 400 if user passwords do not match', (done) => { - const data = {newPassword: 'hello', confirmPassword: 'hell'} + const data = { newPassword: 'hello', confirmPassword: 'hell' } chai .request(app) .patch(resetPasswordURL) @@ -116,7 +116,7 @@ describe('Test reset password mail endpoint and password link endpoint', () => { }); it('It should return a 200 if user passwords match', (done) => { - const data = {newPassword: 'hello', confirmPassword: 'hello'} + const data = { newPassword: 'hello', confirmPassword: 'hello' } chai .request(app) .patch(resetPasswordURL) @@ -130,7 +130,7 @@ describe('Test reset password mail endpoint and password link endpoint', () => { }); it('It should return a 400 if reset link is invalid', (done) => { - const data = {newPassword: 'hello', confirmPassword: 'hello'} + const data = { newPassword: 'hello', confirmPassword: 'hello' } chai .request(app) .patch(invalidResetPasswordURL) From 60ef54371aa27bc93cc9a7ecfebf85ac470192f1 Mon Sep 17 00:00:00 2001 From: Ayo-Oluwa Adebayo Date: Mon, 4 Mar 2019 12:06:24 +0100 Subject: [PATCH 6/6] fix(): Refactor code to pass houndCI checks --- server/test/user.spec.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/test/user.spec.js b/server/test/user.spec.js index 701e03b..2ba693b 100644 --- a/server/test/user.spec.js +++ b/server/test/user.spec.js @@ -74,7 +74,7 @@ describe('Test signup endpoint and email verification endpoint', () => { describe('Test reset password mail endpoint and password link endpoint', () => { it('It should return a 404 if user records not found', (done) => { - const data = { email: 'ayo-oluwa.adebayo@andela.com' } + const data = { email: 'ayo-oluwa.adebayo@andela.com' }; chai .request(app) .post(resetPassword) @@ -88,7 +88,7 @@ describe('Test reset password mail endpoint and password link endpoint', () => { }); it('It should return a 200 if user email is found in the database', (done) => { - const data = { email: 'nwabuzor.obiora@gmail.com' } + const data = { email: 'nwabuzor.obiora@gmail.com' }; chai .request(app) .post(resetPassword) @@ -102,7 +102,7 @@ describe('Test reset password mail endpoint and password link endpoint', () => { }); it('It should return a 400 if user passwords do not match', (done) => { - const data = { newPassword: 'hello', confirmPassword: 'hell' } + const data = { newPassword: 'hello', confirmPassword: 'hell' }; chai .request(app) .patch(resetPasswordURL) @@ -116,7 +116,7 @@ describe('Test reset password mail endpoint and password link endpoint', () => { }); it('It should return a 200 if user passwords match', (done) => { - const data = { newPassword: 'hello', confirmPassword: 'hello' } + const data = { newPassword: 'hello', confirmPassword: 'hello' }; chai .request(app) .patch(resetPasswordURL) @@ -130,7 +130,7 @@ describe('Test reset password mail endpoint and password link endpoint', () => { }); it('It should return a 400 if reset link is invalid', (done) => { - const data = { newPassword: 'hello', confirmPassword: 'hello' } + const data = { newPassword: 'hello', confirmPassword: 'hello' }; chai .request(app) .patch(invalidResetPasswordURL)