rewrite: start adding facebook auth. note, this isn't going to work properly due to issues outlined in http://stackoverflow.com/questions/14572600/passport-js-restful-auth . gotta figure something out here...

This commit is contained in:
Tyler Renelle 2013-09-03 23:15:43 -04:00
parent e43740e939
commit ceae8bf768
10 changed files with 352 additions and 254 deletions

View file

@ -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';

View file

@ -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",

219
src/controllers/auth.js Normal file
View file

@ -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')
// }
};

View file

@ -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*/

View file

@ -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

View file

@ -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

11
src/routes/auth.js Normal file
View file

@ -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;

View file

@ -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) {

View file

@ -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}"
*/
*/

View file

@ -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