diff --git a/assets/js/controllers/authCtrl.js b/assets/js/controllers/authCtrl.js index 18ee9302c1..1895063af8 100644 --- a/assets/js/controllers/authCtrl.js +++ b/assets/js/controllers/authCtrl.js @@ -50,9 +50,18 @@ habitrpg.controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$loca }); }; + function errorAlert(data, status, headers, config) { + if (status === 0) { + alert("Server not currently reachable, try again later"); + } else if (!!data && !!data.err) { + alert(data.err); + } else { + alert("ERROR: " + status); + } + } + $scope.auth = function() { - var data; - data = { + var data = { username: $scope.loginUsername, password: $scope.loginPassword }; @@ -62,18 +71,17 @@ habitrpg.controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$loca $http.post(API_URL + "/api/v1/user/auth/local", data) .success(function(data, status, headers, config) { runAuth(data.id, data.token); - }).error(function(data, status, headers, config) { - if (status === 0) { - alert("Server not currently reachable, try again later"); - } else if (!!data && !!data.err) { - alert(data.err); - } else { - alert("ERROR: " + status); - } - }); + }).error(errorAlert); } }; + $scope.facebookAuth = function(){ + $http.get('/auth/facebook') + .success(function(data){ + runAuth(data.id, data.token); + }).error(errorAlert); + } + $scope.playButtonClick = function(){ if (User.authenticated()) { window.location.href = '/#/tasks'; diff --git a/package.json b/package.json index ac6a9f4f45..f96a72df07 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#rewrite", "derby-auth": "git://github.com/lefnire/derby-auth#master", "connect-mongo": "*", - "passport-facebook": "*", + "passport-facebook": "~1.0.0", "express": "*", "gzippo": "*", "guid": "*", @@ -28,7 +28,8 @@ "connect-assets": "~2.5.2", "bower": "~1.2.4", "nib": "~1.0.1", - "jade": "~0.35.0" + "jade": "~0.35.0", + "passport": "~0.1.17" }, "private": true, "subdomain": "habitrpg", diff --git a/src/controllers/auth.js b/src/controllers/auth.js new file mode 100644 index 0000000000..f56069e6c0 --- /dev/null +++ b/src/controllers/auth.js @@ -0,0 +1,219 @@ +var passport = require('passport'); +var _ = require('lodash'); +var User = require('../models/user').model; + +var api = module.exports; + +var NO_TOKEN_OR_UID = { err: "You must include a token and uid (user id) in your request"}; +var NO_USER_FOUND = {err: "No user found."}; + +/* + beforeEach auth interceptor + */ + +api.auth = function(req, res, next) { + var token, uid; + uid = req.headers['x-api-user']; + token = req.headers['x-api-key']; + if (!(uid && token)) { + return res.json(401, NO_TOKEN_OR_UID); + } + return User.findOne({ + _id: uid, + apiToken: token + }, function(err, user) { + if (err) { + return res.json(500, { + err: err + }); + } + if (_.isEmpty(user)) { + return res.json(401, NO_USER_FOUND); + } + res.locals.wasModified = +user._v !== +req.query._v; + res.locals.user = user; + req.session.userId = user._id; + return next(); + }); +}; + + +api.registerUser = function(req, res, next) { + var confirmPassword, e, email, password, username, _ref; + _ref = req.body, email = _ref.email, username = _ref.username, password = _ref.password, confirmPassword = _ref.confirmPassword; + if (!(username && password && email)) { + return res.json(401, { + err: ":username, :email, :password, :confirmPassword required" + }); + } + if (password !== confirmPassword) { + return res.json(401, { + err: ":password and :confirmPassword don't match" + }); + } + try { + validator.check(email).isEmail(); + } catch (_error) { + e = _error; + return res.json(401, { + err: e.message + }); + } + return async.waterfall([ + function(cb) { + return User.findOne({ + 'auth.local.email': email + }, cb); + }, function(found, cb) { + if (found) { + return cb("Email already taken"); + } + return User.findOne({ + 'auth.local.username': username + }, cb); + }, function(found, cb) { + var newUser, salt, user; + if (found) { + return cb("Username already taken"); + } + newUser = helpers.newUser(true); + salt = utils.makeSalt(); + newUser.auth = { + local: { + username: username, + email: email, + salt: salt + } + }; + newUser.auth.local.hashed_password = derbyAuthUtil.encryptPassword(password, salt); + user = new User(newUser); + return user.save(cb); + } + ], function(err, saved) { + if (err) { + return res.json(401, { + err: err + }); + } + return res.json(200, saved); + }); +}; + +/* + Register new user with uname / password + */ + + +api.loginLocal = function(req, res, next) { + var username = req.body.username; + var password = req.body.password; + async.waterfall([ + function(cb) { + if (!(username && password)) return cb('No username or password'); + User.findOne({'auth.local.username': username}, cb); + }, function(user, cb) { + if (!user) return cb('Username not found'); + // We needed the whole user object first so we can get his salt to encrypt password comparison + User.findOne({ + 'auth.local.username': username, + 'auth.local.hashed_password': utils.encryptPassword(password, user.auth.local.salt) + }, cb); + } + ], function(err, user) { + if (!user) err = 'Incorrect password'; + if (err) return res.json(401, {err: err}); + res.json(200, { + id: user._id, + token: user.apiToken + }); + }); +}; + +/* + POST /user/auth/facebook + */ + + +api.loginFacebook = function(req, res, next) { + var email, facebook_id, name, _ref; + _ref = req.body, facebook_id = _ref.facebook_id, email = _ref.email, name = _ref.name; + if (!facebook_id) { + return res.json(401, { + err: 'No facebook id provided' + }); + } + return User.findOne({ + 'auth.local.facebook.id': facebook_id + }, function(err, user) { + if (err) { + return res.json(401, { + err: err + }); + } + if (user) { + return res.json(200, { + id: user.id, + token: user.apiToken + }); + } else { + /* FIXME: create a new user instead*/ + + return res.json(403, { + err: "Please register with Facebook on https://habitrpg.com, then come back here and log in." + }); + } + }); +}; + +/* + Registers a new user. Only accepting username/password registrations, no Facebook + */ + +api.setupPassport = function(router) { + + router.get('/logout', function(req, res) { + req.logout(); + delete req.session.userId; + res.redirect('/'); + }) + + // GET /auth/facebook + // Use passport.authenticate() as route middleware to authenticate the + // request. The first step in Facebook authentication will involve + // redirecting the user to facebook.com. After authorization, Facebook will + // redirect the user back to this application at /auth/facebook/callback + router.get('/auth/facebook', + passport.authenticate('facebook'), + function(req, res){ + // The request will be redirected to Facebook for authentication, so this + // function will not be called. + }); + + // GET /auth/facebook/callback + // Use passport.authenticate() as route middleware to authenticate the + // request. If authentication fails, the user will be redirected back to the + // login page. Otherwise, the primary route function function will be called, + // which, in this example, will redirect the user to the home page. + router.get('/auth/facebook/callback', + passport.authenticate('facebook', { failureRedirect: '/login' }), + function(req, res) { + //res.redirect('/'); + + User.findOne({'auth.facebook.id':req.user.id}, function(err, user){ + if (err) return res.json(500, {err:err}); + if (!user) return res.json(401, {err: "New Facebook registrations aren't yet supported, only existing Facebook users. Help us code this!"}); + res.json({id: user._id, token: user.apiToken}); + }) + + }); + + // Simple route middleware to ensure user is authenticated. + // Use this route middleware on any resource that needs to be protected. If + // the request is authenticated (typically via a persistent login session), + // the request will proceed. Otherwise, the user will be redirected to the + // login page. +// function ensureAuthenticated(req, res, next) { +// if (req.isAuthenticated()) { return next(); } +// res.redirect('/login') +// } +}; \ No newline at end of file diff --git a/src/controllers/deprecated.js b/src/controllers/deprecated.js index 3ebbae8cac..ce33a4d726 100644 --- a/src/controllers/deprecated.js +++ b/src/controllers/deprecated.js @@ -1,19 +1,14 @@ -var api, deprecatedMessage, express, icalendar, initDeprecated, router, _; - -express = require('express'); - -router = new express.Router(); - -_ = require('lodash'); - -icalendar = require('icalendar'); - -api = require('./user'); +var express = require('express'); +var router = new express.Router(); +var _ = require('lodash'); +var icalendar = require('icalendar'); +var api = require('./user'); +var auth = require('./auth'); /* ---------- Deprecated Paths ------------*/ -deprecatedMessage = 'This API is no longer supported, see https://github.com/lefnire/habitrpg/wiki/API for new protocol'; +var deprecatedMessage = 'This API is no longer supported, see https://github.com/lefnire/habitrpg/wiki/API for new protocol'; router.get('/:uid/up/:score?', function(req, res) { return res.send(500, deprecatedMessage); @@ -30,13 +25,13 @@ router.post('/users/:uid/tasks/:taskId/:direction', function(req, res) { /* Redirect to new API*/ -initDeprecated = function(req, res, next) { +var initDeprecated = function(req, res, next) { req.headers['x-api-user'] = req.params.uid; req.headers['x-api-key'] = req.body.apiToken; return next(); }; -router.post('/v1/users/:uid/tasks/:taskId/:direction', initDeprecated, api.auth, api.scoreTask); +router.post('/v1/users/:uid/tasks/:taskId/:direction', initDeprecated, auth.auth, api.scoreTask); router.get('/v1/users/:uid/calendar.ics', function(req, res) { /*return next() #disable for now*/ diff --git a/src/controllers/user.js b/src/controllers/user.js index 3a8b302f97..4f5c9aa026 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -18,44 +18,6 @@ var User = require('./../models/user').model; var Group = require('./../models/group').model; var api = module.exports; -/* - ------------------------------------------------------------------------ - Misc - ------------------------------------------------------------------------ -*/ - -var NO_TOKEN_OR_UID = { err: "You must include a token and uid (user id) in your request"}; -var NO_USER_FOUND = {err: "No user found."}; - -/* - beforeEach auth interceptor -*/ - -api.auth = function(req, res, next) { - var token, uid; - uid = req.headers['x-api-user']; - token = req.headers['x-api-key']; - if (!(uid && token)) { - return res.json(401, NO_TOKEN_OR_UID); - } - return User.findOne({ - _id: uid, - apiToken: token - }, function(err, user) { - if (err) { - return res.json(500, { - err: err - }); - } - if (_.isEmpty(user)) { - return res.json(401, NO_USER_FOUND); - } - res.locals.wasModified = +user._v !== +req.query._v; - res.locals.user = user; - req.session.userId = user._id; - return next(); - }); -}; /* ------------------------------------------------------------------------ @@ -430,73 +392,6 @@ api.buy = function(req, res, next) { ------------------------------------------------------------------------ */ - -/* - Registers a new user. Only accepting username/password registrations, no Facebook -*/ - - -api.registerUser = function(req, res, next) { - var confirmPassword, e, email, password, username, _ref; - _ref = req.body, email = _ref.email, username = _ref.username, password = _ref.password, confirmPassword = _ref.confirmPassword; - if (!(username && password && email)) { - return res.json(401, { - err: ":username, :email, :password, :confirmPassword required" - }); - } - if (password !== confirmPassword) { - return res.json(401, { - err: ":password and :confirmPassword don't match" - }); - } - try { - validator.check(email).isEmail(); - } catch (_error) { - e = _error; - return res.json(401, { - err: e.message - }); - } - return async.waterfall([ - function(cb) { - return User.findOne({ - 'auth.local.email': email - }, cb); - }, function(found, cb) { - if (found) { - return cb("Email already taken"); - } - return User.findOne({ - 'auth.local.username': username - }, cb); - }, function(found, cb) { - var newUser, salt, user; - if (found) { - return cb("Username already taken"); - } - newUser = helpers.newUser(true); - salt = utils.makeSalt(); - newUser.auth = { - local: { - username: username, - email: email, - salt: salt - } - }; - newUser.auth.local.hashed_password = derbyAuthUtil.encryptPassword(password, salt); - user = new User(newUser); - return user.save(cb); - } - ], function(err, saved) { - if (err) { - return res.json(401, { - err: err - }); - } - return res.json(200, saved); - }); -}; - /* Get User */ @@ -515,72 +410,6 @@ api.getUser = function(req, res, next) { return res.json(200, user); }; -/* - Register new user with uname / password -*/ - - -api.loginLocal = function(req, res, next) { - var username = req.body.username; - var password = req.body.password; - async.waterfall([ - function(cb) { - if (!(username && password)) return cb('No username or password'); - User.findOne({'auth.local.username': username}, cb); - }, function(user, cb) { - if (!user) return cb('Username not found'); - // We needed the whole user object first so we can get his salt to encrypt password comparison - User.findOne({ - 'auth.local.username': username, - 'auth.local.hashed_password': utils.encryptPassword(password, user.auth.local.salt) - }, cb); - } - ], function(err, user) { - if (!user) err = 'Incorrect password'; - if (err) return res.json(401, {err: err}); - res.json(200, { - id: user._id, - token: user.apiToken - }); - }); -}; - -/* - POST /user/auth/facebook -*/ - - -api.loginFacebook = function(req, res, next) { - var email, facebook_id, name, _ref; - _ref = req.body, facebook_id = _ref.facebook_id, email = _ref.email, name = _ref.name; - if (!facebook_id) { - return res.json(401, { - err: 'No facebook id provided' - }); - } - return User.findOne({ - 'auth.local.facebook.id': facebook_id - }, function(err, user) { - if (err) { - return res.json(401, { - err: err - }); - } - if (user) { - return res.json(200, { - id: user.id, - token: user.apiToken - }); - } else { - /* FIXME: create a new user instead*/ - - return res.json(403, { - err: "Please register with Facebook on https://habitrpg.com, then come back here and log in." - }); - } - }); -}; - /* Update user FIXME add documentation here diff --git a/src/routes/api.js b/src/routes/api.js index 673ac9846b..5e874cb11a 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -2,18 +2,18 @@ var express = require('express'); var router = new express.Router(); var user = require('../controllers/user'); var groups = require('../controllers/groups'); +var auth = require('../controllers/auth'); /* - ---------- /api/v1 API ------------ - Every url added to router is prefaced by /api/v1 - See ./routes/coffee for routes + ---------- /api/v1 API ------------ + Every url added to router is prefaced by /api/v1 + See ./routes/coffee for routes - v1 user. Requires x-api-user (user id) and x-api-key (api key) headers, Test with: - $ cd node_modules/racer && npm install && cd ../.. - $ mocha test/user.mocha.coffee -*/ + v1 user. Requires x-api-user (user id) and x-api-key (api key) headers, Test with: + $ cd node_modules/racer && npm install && cd ../.. + $ mocha test/user.mocha.coffee + */ -var auth = user.auth var verifyTaskExists = user.verifyTaskExists var cron = user.cron; @@ -23,50 +23,52 @@ router.get('/status', function(req, res) { }); }); -/* Auth*/ -router.post('/register', user.registerUser); +/* auth.auth*/ +auth.setupPassport(router); //FIXME make this consistent with the others +router.post('/register', auth.registerUser); +router.post('/user/auth/local', auth.loginLocal); +router.post('/user/auth/facebook', auth.loginFacebook); + /* Scoring*/ -router.post('/user/task/:id/:direction', auth, cron, user.scoreTask); -router.post('/user/tasks/:id/:direction', auth, cron, user.scoreTask); +router.post('/user/task/:id/:direction', auth.auth, cron, user.scoreTask); +router.post('/user/tasks/:id/:direction', auth.auth, cron, user.scoreTask); /* Tasks*/ -router.get('/user/tasks', auth, cron, user.getTasks); -router.get('/user/task/:id', auth, cron, user.getTask); -router.put('/user/task/:id', auth, cron, verifyTaskExists, user.updateTask); -router.post('/user/tasks', auth, cron, user.updateTasks); -router["delete"]('/user/task/:id', auth, cron, verifyTaskExists, user.deleteTask); -router.post('/user/task', auth, cron, user.createTask); -router.put('/user/task/:id/sort', auth, cron, verifyTaskExists, user.sortTask); -router.post('/user/clear-completed', auth, cron, user.clearCompleted); +router.get('/user/tasks', auth.auth, cron, user.getTasks); +router.get('/user/task/:id', auth.auth, cron, user.getTask); +router.put('/user/task/:id', auth.auth, cron, verifyTaskExists, user.updateTask); +router.post('/user/tasks', auth.auth, cron, user.updateTasks); +router["delete"]('/user/task/:id', auth.auth, cron, verifyTaskExists, user.deleteTask); +router.post('/user/task', auth.auth, cron, user.createTask); +router.put('/user/task/:id/sort', auth.auth, cron, verifyTaskExists, user.sortTask); +router.post('/user/clear-completed', auth.auth, cron, user.clearCompleted); /* Items*/ -router.post('/user/buy/:type', auth, cron, user.buy); +router.post('/user/buy/:type', auth.auth, cron, user.buy); /* User*/ -router.get('/user', auth, cron, user.getUser); -router.post('/user/auth/local', user.loginLocal); -router.post('/user/auth/facebook', user.loginFacebook); -router.put('/user', auth, cron, user.updateUser); -router.post('/user/revive', auth, cron, user.revive); -router.post('/user/batch-update', auth, cron, user.batchUpdate); -router.post('/user/reroll', auth, cron, user.reroll); -router.post('/user/buy-gems', auth, user.buyGems); +router.get('/user', auth.auth, cron, user.getUser); +router.put('/user', auth.auth, cron, user.updateUser); +router.post('/user/revive', auth.auth, cron, user.revive); +router.post('/user/batch-update', auth.auth, cron, user.batchUpdate); +router.post('/user/reroll', auth.auth, cron, user.reroll); +router.post('/user/buy-gems', auth.auth, user.buyGems); /* Groups*/ -router.get('/groups', auth, groups.getGroups); -router.post('/groups', auth, groups.createGroup); +router.get('/groups', auth.auth, groups.getGroups); +router.post('/groups', auth.auth, groups.createGroup); //TODO: //GET /groups/:gid (get group) //PUT /groups/:gid (edit group) //DELETE /groups/:gid -router.post('/groups/:gid/join', auth, groups.attachGroup, groups.join); -router.post('/groups/:gid/leave', auth, groups.attachGroup, groups.leave); -router.post('/groups/:gid/invite', auth, groups.attachGroup, groups.invite); +router.post('/groups/:gid/join', auth.auth, groups.attachGroup, groups.join); +router.post('/groups/:gid/leave', auth.auth, groups.attachGroup, groups.leave); +router.post('/groups/:gid/invite', auth.auth, groups.attachGroup, groups.invite); //GET /groups/:gid/chat -router.post('/groups/:gid/chat', auth, groups.attachGroup, groups.postChat); +router.post('/groups/:gid/chat', auth.auth, groups.attachGroup, groups.postChat); //PUT /groups/:gid/chat/:messageId //DELETE /groups/:gid/chat/:messageId diff --git a/src/routes/auth.js b/src/routes/auth.js new file mode 100644 index 0000000000..ed921d1ee3 --- /dev/null +++ b/src/routes/auth.js @@ -0,0 +1,11 @@ +var auth = require('../controllers/auth'); +var express = require('express'); +var router = new express.Router(); + +/* auth.auth*/ +auth.setupPassport(router); //FIXME make this consistent with the others +router.post('/register', auth.registerUser); +router.post('/user/auth/local', auth.loginLocal); +router.post('/user/auth/facebook', auth.loginFacebook); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/pages.js b/src/routes/pages.js index 965c226fdc..8e1f7fe590 100644 --- a/src/routes/pages.js +++ b/src/routes/pages.js @@ -19,11 +19,6 @@ router.get('/partials/options', function(req, res) { res.render('options'); }); -router.get('/logout', function(req, res) { - delete req.session.userId; - res.redirect('/'); -}) - // -------- Marketing -------- router.get('/splash.html', function(req, res) { diff --git a/src/server.js b/src/server.js index 269dfd6b63..eabec8582b 100644 --- a/src/server.js +++ b/src/server.js @@ -9,11 +9,11 @@ var middleware = require('./middleware'); var server; var TWO_WEEKS = 1000 * 60 * 60 * 24 * 14; -// Setup configurations +// ------------ Setup configurations ------------ require('./config'); require('./errors'); -// MongoDB Configuration +// ------------ MongoDB Configuration ------------ mongoose = require('mongoose'); require('./models/user'); //load up the user schema - TODO is this necessary? require('./models/group'); @@ -22,12 +22,50 @@ mongoose.connect(nconf.get('NODE_DB_URI'), function(err) { console.info('Connected with Mongoose'); }); -/** - Server Configuration - */ + +// ------------ Passport Configuration ------------ +var passport = require('passport') +var util = require('util') +var FacebookStrategy = require('passport-facebook').Strategy; +// Passport session setup. +// To support persistent login sessions, Passport needs to be able to +// serialize users into and deserialize users out of the session. Typically, +// this will be as simple as storing the user ID when serializing, and finding +// the user by ID when deserializing. However, since this example does not +// have a database of user records, the complete Facebook profile is serialized +// and deserialized. +passport.serializeUser(function(user, done) { + done(null, user); +}); + +passport.deserializeUser(function(obj, done) { + done(null, obj); +}); -// all environments +// Use the FacebookStrategy within Passport. +// Strategies in Passport require a `verify` function, which accept +// credentials (in this case, an accessToken, refreshToken, and Facebook +// profile), and invoke a callback with a user object. +passport.use(new FacebookStrategy({ + clientID: nconf.get("FACEBOOK_KEY"), + clientSecret: nconf.get("FACEBOOK_SECRET"), + callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback" + }, + function(accessToken, refreshToken, profile, done) { + // asynchronous verification, for effect... + //process.nextTick(function () { + + // To keep the example simple, the user's Facebook profile is returned to + // represent the logged-in user. In a typical application, you would want + // to associate the Facebook account with a user record in your database, + // and return that user instead. + return done(null, profile); + //}); + } +)); + +// ------------ Server Configuration ------------ app.set("port", nconf.get('PORT')); app.set("views", __dirname + "/../views"); app.set("view engine", "jade"); @@ -43,6 +81,12 @@ app.use(express.cookieSession({ secret: nconf.get('SESSION_SECRET'), httpOnly: f //app.use(express.session()); app.use(middleware.splash); app.use(middleware.locals); + +// Initialize Passport! Also use passport.session() middleware, to support +// persistent login sessions (recommended). +app.use(passport.initialize()); +app.use(passport.session()); + app.use(app.router); app.use(express['static'](path.join(__dirname, "/../public"))); @@ -53,6 +97,7 @@ if ("development" === app.get("env")) { // Custom Directives app.use(require('./routes/pages').middleware); +app.use(require('./routes/auth').middleware); app.use('/api/v1', require('./routes/api').middleware); app.use(require('./controllers/deprecated').middleware); server = http.createServer(app).listen(app.get("port"), function() { @@ -68,24 +113,17 @@ module.exports = server; # # #expressApp - # .use(middleware.allowCrossDomain) # .use(express.favicon("#{publicPath}/favicon.ico")) # # Gzip static files and serve from memory # .use(gzippo.staticGzip(publicPath, maxAge: ONE_YEAR)) # # Gzip dynamically rendered content # .use(express.compress()) # .use(middleware.translate) - # .use(middleware.view) # .use(auth.middleware(strategies, options)) - # # Creates an express middleware from the app's routes - # .use(app.router()) - # .use(require('./static').middleware) - # .use(expressApp.router) # .use(serverError(root)) # # ## Errors #expressApp.all '*', (req) -> # throw "404: #{req.url}" - */ - + */ \ No newline at end of file diff --git a/views/static/front.jade b/views/static/front.jade index 38924f3345..59b4fdfe00 100644 --- a/views/static/front.jade +++ b/views/static/front.jade @@ -25,8 +25,8 @@ block content button.close(type='button', data-dismiss='modal', aria-hidden='true') × h4.modal-title Login / Register .modal-body - //a(href='/auth/facebook') - img(src='/img/facebook-login-register.jpeg', alt='Login / Register With Facebook') + //-a(ng-click='facebookAuth()') + img(src='/bower_components/habitrpg-shared/img/facebook-login-register.jpeg', alt='Login / Register With Facebook') //h3 Or ul.nav.nav-tabs li.active