diff --git a/gulpfile.js b/gulpfile.js index 3c0a6f7..8caa47f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -59,7 +59,7 @@ gulp.task('test', ['lint-test', 'instrument'], function() { thresholds: { global: { statements: 80, - branches: 60 + branches: 55 } } })); diff --git a/integration-test/game.js b/integration-test/game.js index 12aae47..0498ed6 100644 --- a/integration-test/game.js +++ b/integration-test/game.js @@ -29,27 +29,31 @@ }); function withGame(word, callback) { - page.open(rootUrl + '/', function() { - page.evaluateAsync(function(w) { - $('input[name=word]').val(w); - $('form#createGame').submit(); - }, 0, word); - - page.evaluate(function() { - $(document).ajaxComplete(window.callPhantom); - }); - - page.onCallback = function() { - var gamePath = page.evaluate(function() { - return $('#createdGames .game a').first().attr('href'); + page.open(rootUrl + '/auth/test', + 'POST', + 'username=TestUser&password=dummy', + function() { + page.evaluateAsync(function(w) { + $('input[name=word]').val(w); + $('form#createGame').submit(); + }, 0, word); + + page.evaluate(function() { + $(document).ajaxComplete(window.callPhantom); }); - page.onCallback = undefined; - page.clearCookies(); - - page.open(rootUrl + gamePath, verify(callback)); - }; - }); + page.onCallback = function() { + var gamePath = page.evaluate(function() { + return $('#createdGames .game a').first().attr('href'); + }); + + page.onCallback = undefined; + page.clearCookies(); + + page.open(rootUrl + gamePath, verify(callback)); + }; + } + ); } function getText(selector) { diff --git a/package.json b/package.json index 64dde18..2b39962 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "hjs": "~0.0.6", "mongoose": "^4.4.15", "morgan": "~1.6.1", + "passport": "^0.3.2", + "passport-twitter": "^1.0.4", "redis": "^2.6.0-2", "serve-favicon": "~2.3.0", "socket.io": "^1.4.6", @@ -32,6 +34,7 @@ "gulp-shell": "^0.5.2", "mocha": "^2.4.5", "mockgoose": "^5.4.1", + "passport-local": "^1.0.0", "phantomjs-prebuilt": "^2.1.7", "redis-js": "^0.1.2", "sinon": "^1.17.4", diff --git a/src/app.js b/src/app.js index 8f63244..06dcc70 100644 --- a/src/app.js +++ b/src/app.js @@ -6,13 +6,13 @@ module.exports = (mongoose) => { var logger = require('morgan'); var bodyParser = require('body-parser'); - let sessions = require('./middleware/sessions'); let gamesService = require('./services/games')(mongoose); let usersService = require('./services/users'); - let users = require('./middleware/users')(usersService); let routes = require('./routes/index')(gamesService, usersService); let games = require('./routes/games')(gamesService, usersService); let profile = require('./routes/profile')(usersService); + let passport = require('./config/passport')(usersService); + let sessions = require('./middleware/sessions')(passport); var app = express(); @@ -29,7 +29,16 @@ module.exports = (mongoose) => { app.use(sessions); app.use(express.static(path.join(__dirname, 'public'))); - app.use(users); + app.post('/auth/twitter', passport.authenticate('twitter')); + app.get('/auth/twitter/callback', + passport.authenticate('twitter', + { successRedirect: '/', failureRedirect: '/' })); + + if (process.env.NODE_ENV === 'test') { + app.post('/auth/test', + passport.authenticate('local',{ successRedirect: '/' })); + } + app.use('/', routes); app.use('/games', games); app.use('/profile', profile); diff --git a/src/config/passport.js b/src/config/passport.js new file mode 100644 index 0000000..a7b7c14 --- /dev/null +++ b/src/config/passport.js @@ -0,0 +1,45 @@ +'use strict'; + +const passport = require('passport'); +const TwitterStrategy = require('passport-twitter').Strategy; + +module.exports = (usersService) => { + if(process.env.TWITTER_API_KEY && + process.env.TWITTER_API_SECRET) { + passport.use(new TwitterStrategy({ + consumerKey: process.env.TWITTER_API_KEY, + consumerSecret: process.env.TWITTER_API_SECRET, + callbackURL: '/auth/twitter/callback', + passReqToCallback: true + }, (req, token, tokenSecret, profile, done) => { + usersService.getOrCreate('twitter', profile.id, + profile.username || profile.displayName) + .then(user => done(null, user), done); + })); + } + + if (process.env.NODE_ENV === 'test') { + const LocalStrategy = require('passport-local'); + const uuid = require('uuid'); + passport.use(new LocalStrategy((username, password, done) => { + const userId = uuid.v4(); + usersService.setUsername(userId, username) + .then(() => { + done(null, { id: userId, name: username }); + }); + } + )); + } + + passport.serializeUser((user, done) => { + done(null, user.id); + }); + + passport.deserializeUser((id, done) => { + usersService.getUser(id) + .then(user => done(null, user)) + .catch(done); + }); + + return passport; +}; diff --git a/src/middleware/sessions.js b/src/middleware/sessions.js index c036efa..570d6ff 100644 --- a/src/middleware/sessions.js +++ b/src/middleware/sessions.js @@ -13,4 +13,5 @@ if (process.env.REDIS_URL && process.env.NODE_ENV !== 'test') { config.store = new RedisStore({ url: process.env.REDIS_URL }); } -module.exports = session(config); +const expressSession = session(config); +module.exports = passport => [expressSession, passport.initialize(), passport.session()]; \ No newline at end of file diff --git a/src/middleware/users.js b/src/middleware/users.js deleted file mode 100644 index 407599c..0000000 --- a/src/middleware/users.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -module.exports = (service) => { - const uuid = require('uuid'); - - return function(req, res, next) { - let userId = req.session.userId; - if (!userId) { - userId = uuid.v4(); - req.session.userId = userId; - req.user = { - id: userId - }; - next(); - } else { - service.getUsername(userId).then(username => { - req.user = { - id: userId, - name: username - }; - next(); - }); - } - }; -}; diff --git a/src/realtime/chat.js b/src/realtime/chat.js index 130d2d0..6300661 100644 --- a/src/realtime/chat.js +++ b/src/realtime/chat.js @@ -4,7 +4,10 @@ module.exports = io => { const namespace = io.of('/chat'); namespace.on('connection', (socket) => { - const username = socket.request.user.name; + let username = null; + if (socket.request.user) { + username = socket.request.user.name; + } socket.on('joinRoom', (room) => { socket.join(room); diff --git a/src/realtime/games.js b/src/realtime/games.js index 8c674ee..3efbb55 100644 --- a/src/realtime/games.js +++ b/src/realtime/games.js @@ -8,7 +8,8 @@ module.exports = (io, service) => { function forwardEvent(name, socket) { service.events.on(name, game => { - if (game.setBy !== socket.request.user.id) { + if (!socket.request.user || + game.setBy !== socket.request.user.id) { socket.emit(name, game.id); } }); diff --git a/src/routes/games.js b/src/routes/games.js index a6f6397..967e87f 100644 --- a/src/routes/games.js +++ b/src/routes/games.js @@ -43,7 +43,7 @@ module.exports = (gamesService, usersService) => { req.params.id, res, game => { - if (game.matches(req.body.word)) { + if (req.user && game.matches(req.body.word)) { usersService.recordWin(req.user.id); } res.send({ diff --git a/src/routes/index.js b/src/routes/index.js index 2fc5391..a645f41 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -5,17 +5,20 @@ module.exports = (gamesService, usersService) => { var router = express.Router(); router.get('/', function(req, res, next) { - Promise.all([ - gamesService.createdBy(req.user.id), - gamesService.availableTo(req.user.id), - usersService.getUsername(req.user.id), - usersService.getRanking(req.user.id), - usersService.getTopPlayers() - ]) + let userId = null; + if (req.user) { + userId = req.user.id; + } + + Promise.all([gamesService.createdBy(userId), + gamesService.availableTo(userId), + usersService.getUsername(userId), + usersService.getRanking(userId), + usersService.getTopPlayers()]) .then(results => { res.render('index', { title: 'Hangman online', - userId: req.user.id, + loggedIn: req.isAuthenticated(), createdGames: results[0], availableGames: results[1], username: results[2], diff --git a/src/server.js b/src/server.js index 5e811a6..2848f59 100644 --- a/src/server.js +++ b/src/server.js @@ -10,9 +10,10 @@ module.exports = require('./config/mongoose').then(mongoose => { io.adapter(redisAdapter(process.env.REDIS_URL)); } - io.use(adapt(require('./middleware/sessions'))); const usersService = require('./services/users.js'); - io.use(adapt(require('./middleware/users')(usersService))); + let passport = require('./config/passport')(usersService); + require('./middleware/sessions')(passport).forEach( + middleware => io.use(adapt(middleware))); require('./realtime/chat')(io); const gamesService = require('./services/games.js')(mongoose); diff --git a/src/services/users.js b/src/services/users.js index a8374d9..5eb88d9 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -1,12 +1,37 @@ 'use strict'; -let redisClient = require('../config/redis.js'); +const redisClient = require('../config/redis.js'); +const uuid = require('uuid'); + +const getUser = userId => + redisClient.getAsync(`user:${userId}:name`) + .then(userName => ({ + id: userId, + name: userName + })); + +const setUsername = (userId, name) => + redisClient.setAsync(`user:${userId}:name`, name); module.exports = { + getOrCreate: (provider, providerId, providerUsername) => { + let providerKey = `provider:${provider}:${providerId}:user`; + let newUserId = uuid.v4(); + return redisClient.setnxAsync(providerKey, newUserId) + .then(created => { + if (created) { + return setUsername(newUserId, providerUsername) + .then(() => getUser(newUserId)); + } else { + return redisClient + .getAsync(providerKey).then(getUser); + } + }); + }, + getUser: getUser, getUsername: userId => redisClient.getAsync(`user:${userId}:name`), - setUsername: (userId, name) => - redisClient.setAsync(`user:${userId}:name`, name), + setUsername: setUsername, recordWin: userId => redisClient.zincrbyAsync('user:wins', 1, userId), getTopPlayers: () => diff --git a/src/views/index.hjs b/src/views/index.hjs index 4be168a..3b37092 100644 --- a/src/views/index.hjs +++ b/src/views/index.hjs @@ -10,16 +10,25 @@

{{ title }}

-

Profile

+

Account

{{#ranking}}

You are ranked #{{ranking.rank}} with {{ranking.wins}} wins!

{{/ranking}} -
- - - -
+ {{^loggedIn}} +
+ +
+ {{/loggedIn}} + {{#loggedIn}} +

Profile

+
+ + + +
+ {{/loggedIn}}

Games

+ {{#loggedIn}}
@@ -31,6 +40,7 @@ {{> createdGame}} {{/createdGames}} + {{/loggedIn}}

Games available to play