diff --git a/.env.example b/.env.example index de3a95bd..702fd949 100644 --- a/.env.example +++ b/.env.example @@ -3,4 +3,10 @@ PORT=theServerListeningPort DEV_DB_URL=TheDevDatabaseConnectionUrl__Format:postgres://user:pass@host:5432/development_dbname TEST_DB_URL=TheTestDatabaseConnectionUrl__Format:postgres://user:pass@host:5432/test_dbname DB_URL=TheStaggingDatabaseConnectionUrl__Format:postgres://user:pass@host:5432/dbname -DB_URL=TheProductionDatabaseConnectionUrl__Format:postgres://user:pass@host:5432/dbname \ No newline at end of file +DB_URL=TheProductionDatabaseConnectionUrl__Format:postgres://user:pass@host:5432/dbname + + + FACEBOOK passport + clientID=FACEBOOK_APP_ID, + clientSecret=FACEBOOK_APP_SECRET, + callbackURL="http://www.example.com/auth/facebook/callback" \ No newline at end of file diff --git a/package.json b/package.json index ae713d41..3c5456e7 100644 --- a/package.json +++ b/package.json @@ -21,21 +21,31 @@ "nyc": "^14.1.1" }, "dependencies": { - "babel-plugin-istanbul": "^5.1.2", "@babel/cli": "^7.4.4", "@babel/core": "^7.4.5", "@babel/node": "^7.4.5", + "@babel/polyfill": "^7.4.4", "@babel/preset-env": "^7.4.5", "@babel/register": "^7.4.4", + "babel-plugin-istanbul": "^5.1.2", "body-parser": "^1.18.3", "chai-http": "^4.3.0", "cors": "^2.8.5", "dotenv": "^8.0.0", "expect": "^24.8.0", "express": "^4.16.4", + "express-session": "^1.16.2", + "jsonwebtoken": "^8.5.1", "morgan": "^1.9.1", + "passport": "^0.4.0", + "passport-facebook": "^3.0.0", + "passport-google-oauth": "^2.0.0", + "passport-pinterest": "^1.0.0", + "passport-twitter": "^1.0.4", "pg": "^7.11.0", "sequelize": "^5.8.7", + "sinon": "^7.3.2", + "sinon-chai": "^3.3.0", "swagger-node-express": "^2.1.3", "swagger-ui-express": "^4.0.6" }, diff --git a/src/api/controllers/db.test.js b/src/api/controllers/db.test.js index ca0a831b..90485455 100644 --- a/src/api/controllers/db.test.js +++ b/src/api/controllers/db.test.js @@ -9,10 +9,9 @@ export default { }, get: async (req, res) => { - const m = await models.Test.findAll({ - }); + const m = await models.Test.findAll({}); return res.status(200).send({ - data: m, + data: m }); }, diff --git a/src/api/controllers/socialAuth.js b/src/api/controllers/socialAuth.js new file mode 100644 index 00000000..db5d2d7e --- /dev/null +++ b/src/api/controllers/socialAuth.js @@ -0,0 +1,102 @@ +import jwt from 'jsonwebtoken'; +import model from '../models'; + +const { User } = model; + +/** + * + * + * @class socialLogin + */ +class socialLogin { + /** + * signup via facebook + * login via facebook + * + * @static + * @param {object} req - request object + * @param {object} res - response object + * @memberof socialLogin + * @returns {object} - the response body + */ + static async socialFacebookAuth(req, res) { + let findIfExist = await User.findOne({ + where: { + uniqueId: req.user.id + } + }); + if (!findIfExist) { + findIfExist = User.create({ + firstName: req.user.name.givenName, + lastName: req.user.name.familyName, + userName: req.user.username, + profileImage: req.user.photos[0].value, + provider: req.user.provider, + uniqueId: req.user.id + }); + } + const token = jwt.sign({ id: findIfExist.id }, 'secret', { expiresIn: '1d' }); + res.json({ user: { ...findIfExist.get(), token } }); + } + + /** + * signup user via Google + * login user via Google + * + * @static + * @param {object} req - request object + * @param {object} res - request response + * @memberof socialLogin + * @returns {object} - the response body + */ + static async socialGoogleAuth(req, res) { + let findIfExist = await User.findOne({ + where: { + uniqueId: req.user.id + } + }); + if (!findIfExist) { + findIfExist = User.create({ + firstName: req.user.name.familyName, + lastName: req.user.name.givenName, + userName: req.user.displayName, + email: req.user.emails[0].value, + profileImage: req.user.photos[0].value, + provider: req.user.provider, + uniqueId: req.user.id + }); + } + const token = jwt.sign({ id: findIfExist.id }, 'secret', { expiresIn: '1d' }); + res.json({ user: { ...findIfExist.get(), token } }); + } + + /** + * signup user via Twitter + * login user via Twitter + * + * @static + * @param {object} req - request object + * @param {object} res - request response + * @memberof socialLogin + * @returns {object} - the response body + */ + static async socialtwitterAuth(req, res) { + let findIfExist = await User.findOne({ + where: { + uniqueId: req.user.id + } + }); + if (!findIfExist) { + findIfExist = User.create({ + userName: req.user.username, + profileImage: req.user.photos[0].value, + provider: req.user.provider, + uniqueId: req.user.id + }); + } + const token = jwt.sign({ id: findIfExist.id }, 'secret', { expiresIn: '1d' }); + res.json({ user: { ...findIfExist.get(), token } }); + } +} + +export default socialLogin; diff --git a/src/api/models/index.js b/src/api/models/index.js index 613ec99a..70667625 100644 --- a/src/api/models/index.js +++ b/src/api/models/index.js @@ -1,15 +1,15 @@ - import Sequelize from 'sequelize'; -import environments from '../../configs/environnements'; +import { currentEnv } from '../../configs/environnements'; -const env = environments.currentEnv; +const env = currentEnv; const sequelize = new Sequelize(env.dbUrl, { logging: false }); const models = { - Test: sequelize.import('./test') + Test: sequelize.import('./test'), + User: sequelize.import('./user') }; Object.keys(models).forEach((key) => { diff --git a/src/api/models/user.js b/src/api/models/user.js new file mode 100644 index 00000000..8482f8a3 --- /dev/null +++ b/src/api/models/user.js @@ -0,0 +1,36 @@ +const user = (sequelize, DataTypes) => { + const User = sequelize.define('user', { + firstName: { + type: DataTypes.STRING, + allowNull: true + }, + lastName: { + type: DataTypes.STRING, + allowNull: true + }, + userName: { + type: DataTypes.STRING, + allowNull: true + }, + email: { + type: DataTypes.STRING, + allowNull: true + }, + profileImage: { + type: DataTypes.STRING, + allowNull: true + }, + provider: { + type: DataTypes.STRING, + allowNull: false + }, + uniqueId: { + type: DataTypes.STRING, + allowNull: false + } + }); + + return User; +}; + +export default user; diff --git a/src/api/routes/passport/facebook.js b/src/api/routes/passport/facebook.js new file mode 100644 index 00000000..16b91fe9 --- /dev/null +++ b/src/api/routes/passport/facebook.js @@ -0,0 +1,15 @@ +import express from 'express'; +import { passport } from '../../../configs/environnements'; +import socialLogin from '../../controllers/socialAuth'; + +// Declaring the app +const facebookRouter = express(); + +facebookRouter.get('/facebook', passport.authenticate('facebook')); +facebookRouter.get( + '/facebook/callback', + passport.authenticate('facebook'), + socialLogin.socialFacebookAuth +); + +export default facebookRouter; diff --git a/src/api/routes/passport/google.js b/src/api/routes/passport/google.js new file mode 100644 index 00000000..84198e09 --- /dev/null +++ b/src/api/routes/passport/google.js @@ -0,0 +1,10 @@ +import express from 'express'; +import { passport } from '../../../configs/environnements'; +import socialLogin from '../../controllers/socialAuth'; +// Declaring the app +const googleRouter = express(); + +googleRouter.get('/google', passport.authenticate('google', { scope: ['email', 'profile'] })); +googleRouter.get('/google/callback', passport.authenticate('google'), socialLogin.socialGoogleAuth); + +export default googleRouter; diff --git a/src/api/routes/passport/twitter.js b/src/api/routes/passport/twitter.js new file mode 100644 index 00000000..c0d2e66b --- /dev/null +++ b/src/api/routes/passport/twitter.js @@ -0,0 +1,14 @@ +import express from 'express'; +import { passport } from '../../../configs/environnements'; +import socialLogin from '../../controllers/socialAuth'; +// Declaring the app +const twitterRouter = express(); + +twitterRouter.get('/twitter', passport.authenticate('twitter', { scope: ['email', 'profile'] })); +twitterRouter.get( + '/twitter/callback', + passport.authenticate('twitter'), + socialLogin.socialtwitterAuth +); + +export default twitterRouter; diff --git a/src/configs/environnements.js b/src/configs/environnements.js index 4ac1e96a..161e3154 100644 --- a/src/configs/environnements.js +++ b/src/configs/environnements.js @@ -1,5 +1,10 @@ import dotenv from 'dotenv'; +import passport from 'passport'; +import FacebookStrategy from 'passport-facebook'; +import { OAuth2Strategy } from 'passport-google-oauth'; +import TwitterStrategy from 'passport-twitter'; + dotenv.config(); const port = process.env.PORT || 3000; @@ -28,6 +33,59 @@ const environnements = [ } ]; +const { + GclientId, + GclientSecret, + Gcallback, + FclientId, + FclientSecret, + Fcallback, + TclientId, + TclientSecret, + Tcallback +} = process.env; + +passport.use( + new OAuth2Strategy( + { + clientID: GclientId, + clientSecret: GclientSecret, + callbackURL: Gcallback, + profileFields: ['name', 'photos', 'email'] + }, + (accessToken, refreshToken, profile, done) => done(null, profile) + ) +); + +passport.use( + new FacebookStrategy( + { + clientID: FclientId, + clientSecret: FclientSecret, + callbackURL: Fcallback, + profileFields: ['name', 'photos', 'email'] + }, + (accessToken, refreshTocken, profile, done) => done(null, profile) + ) +); + +passport.use( + new TwitterStrategy( + { + consumerKey: TclientId, + consumerSecret: TclientSecret, + callbackURL: Tcallback, + profile: ['name', 'photo', 'email'] + }, + (accessToken, refreshTocken, profile, done) => { + console.log(profile); + done(null, profile); + } + ) +); + +passport.serializeUser((user, done) => done(null, user)); +passport.deserializeUser((user, done) => done(null, user)); const currentEnv = environnements.find(el => el.name === env.toLocaleLowerCase()); -export default { currentEnv, env }; +export { currentEnv, env, passport }; diff --git a/src/index.js b/src/index.js index e408a7ac..af13d129 100644 --- a/src/index.js +++ b/src/index.js @@ -1,25 +1,37 @@ import express from 'express'; import '@babel/polyfill'; +import passport from 'passport'; +import session from 'express-session'; +import dotenv from 'dotenv'; import apiRouter from './api/routes/index'; import docsRouter from './api/routes/docs'; import homeRouter from './api/routes/home'; +import facebookRoutes from './api/routes/passport/facebook'; +import googleRoutes from './api/routes/passport/google'; +import twitterRoutes from './api/routes/passport/twitter'; import register from './middlewares/register.app'; import { sequelize } from './api/models/index'; -import environnements from './configs/environnements'; - +import { currentEnv } from './configs/environnements'; const app = express(); -const env = environnements.currentEnv; - +const env = currentEnv; +dotenv.config(); const syncDbOnStart = env.name === 'test'; - +// session +app.use( + session({ + saveUninitialized: true, + secret: process.env.secretOrKey + }) +); // Register middleware register(app); - +app.use(passport.initialize()); app.use('/api/', apiRouter); app.use('/docs', docsRouter); app.use('/', homeRouter); +app.use('/api/auth/', facebookRoutes, googleRoutes, twitterRoutes); sequelize.sync().then(() => { app.listen(env.port, () => { diff --git a/src/middlewares/register.app.js b/src/middlewares/register.app.js index 3a96a309..712fdad5 100644 --- a/src/middlewares/register.app.js +++ b/src/middlewares/register.app.js @@ -5,7 +5,7 @@ import bodyParser from 'body-parser'; import cors from 'cors'; import logger from 'morgan'; -import environnements from '../configs/environnements'; +import { currentEnv } from '../configs/environnements'; /** * A function to register all the needed middlewares to the * app (express instance) every time the server is starting @@ -18,10 +18,10 @@ export default (app) => { // Parse req object and make data available on req.body .use(bodyParser.json()) .use(bodyParser.urlencoded({ extended: true })) - .use(cors()); // Allow cross origin requests + .use(cors()) // Allow cross origin requests + .set('view engine', 'ejs'); - if (environnements.currentEnv.name === 'development' - || environnements.currentEnv.name === 'test') { + if (currentEnv.name === 'development' || currentEnv.name === 'test') { // Logging http requests app.use(logger('dev')); } diff --git a/test/socialLogin.test.js b/test/socialLogin.test.js new file mode 100644 index 00000000..93241289 --- /dev/null +++ b/test/socialLogin.test.js @@ -0,0 +1,53 @@ +// import chai from 'chai'; +// import sinon from 'sinon'; +// import sinonChai from 'sinon-chai'; +// import User from '../src/api/controllers/socialAuth'; + +// const { expect } = chai; +// chai.use(sinonChai); + +// describe('Social login', () => { +// afterEach(() => { +// sinon.restore(); +// }); +// it('should create a new user', async () => { +// const req = { +// user: { +// name: { +// familyName: 'Gerard', +// givenName: 'Kaboneka' +// }, +// provider: 'facebook', +// id: '656897238993367546738', +// emails: [{ value: 'busernamee@email.com' }], +// photos: [{ value: 'image.jpg' }] +// } +// }; +// const res = { +// send() {}, +// status() {}, +// json() {} +// }; +// sinon.stub(res, 'status').returnsThis(); +// await User.socialFacebookAuth(req, res); +// expect(req.user).to.be.an(Object); +// // expect(res.status).to.have.been.calledWith(201); +// }); + +// it('should not create a new user with a bad req', async () => { +// const req = { +// user: { +// provider: 'facebook', +// uniqueId: '567887632' +// } +// }; + +// const res = { +// status() {}, +// json() {} +// }; +// sinon.stub(res, 'status').returnsThis(); +// // await User.socialFacebookAuth(req, res); +// // expect(res.status).to.have.been.calledWith(500); +// }); +// });