From 2f13c53161fa892085aa1e46df2bf2c84d9c42f1 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 2 Jul 2013 16:51:38 -0700 Subject: [PATCH] Initial users --- lib/application.js | 4 +- lib/asteroid.js | 10 ++--- lib/middleware/auth.js | 34 +++++++++++++++ lib/models/session.js | 22 ++++++++++ lib/models/user.js | 93 +++++++++++++++++++++++++++++++++++++++++- package.json | 5 ++- test/user.test.js | 38 +++++++++++++++++ 7 files changed, 194 insertions(+), 12 deletions(-) create mode 100644 lib/middleware/auth.js create mode 100644 lib/models/session.js create mode 100644 test/user.test.js diff --git a/lib/application.js b/lib/application.js index c292c8d8..703e630e 100644 --- a/lib/application.js +++ b/lib/application.js @@ -55,9 +55,7 @@ app.model = function (Model) { this._models.push(Model); Model.shared = true; Model.app = this; - if(Model._remoteHooks) { - Model._remoteHooks.emit('attached', app); - } + Model.emit('attached', this); } /** diff --git a/lib/asteroid.js b/lib/asteroid.js index 7c2c46d4..3ca416d4 100644 --- a/lib/asteroid.js +++ b/lib/asteroid.js @@ -216,8 +216,8 @@ asteroid.createModel = function (name, properties, options) { }); } else { var args = arguments; - this._remoteHooks.once('attached', function () { - self.beforeRemote.apply(ModelCtor, args); + this.once('attached', function () { + self.beforeRemote.apply(self, args); }); } } @@ -232,15 +232,12 @@ asteroid.createModel = function (name, properties, options) { }); } else { var args = arguments; - this._remoteHooks.once('attached', function () { + this.once('attached', function () { self.afterRemote.apply(ModelCtor, args); }); } } - // allow hooks to be added before attaching to an app - ModelCtor._remoteHooks = new EventEmitter(); - return ModelCtor; } @@ -266,3 +263,4 @@ asteroid.remoteMethod = function (fn, options) { asteroid.Model = asteroid.createModel('model'); asteroid.User = require('./models/user'); +asteroid.Session = require('./models/session'); diff --git a/lib/middleware/auth.js b/lib/middleware/auth.js new file mode 100644 index 00000000..79f8acd9 --- /dev/null +++ b/lib/middleware/auth.js @@ -0,0 +1,34 @@ +/** + * Module dependencies. + */ + +var asteroid = require('../asteroid') + , passport = require('passport'); + +/** + * Export the middleware. + */ + +module.exports = auth; + +/** + * Build a temp app for mounting resources. + */ + +function auth() { + return function (req, res, next) { + var sub = asteroid(); + + // TODO clean this up + sub._models = req.app._models; + sub._remotes = req.app._remotes; + + sub.use(asteroid.session({secret: 'change me'})) + sub.use(passport.initialize()); + sub.use(passport.session()); + + sub.handle(req, res, next); + } +} + + diff --git a/lib/models/session.js b/lib/models/session.js new file mode 100644 index 00000000..eadfeb5e --- /dev/null +++ b/lib/models/session.js @@ -0,0 +1,22 @@ +/** + * Module Dependencies. + */ + +var Model = require('../asteroid').Model + , asteroid = require('../asteroid'); + +/** + * Default Session properties. + */ + +var properties = { + id: {type: String, required: true}, + uid: {type: String}, + ttl: {type: Number, ttl: true} +}; + +/** + * Extends from the built in `asteroid.Model` type. + */ + +var Session = module.exports = Model.extend('session', properties); \ No newline at end of file diff --git a/lib/models/user.js b/lib/models/user.js index f13d0be1..21118917 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -2,7 +2,10 @@ * Module Dependencies. */ -var Model = require('../asteroid').Model; +var Model = require('../asteroid').Model + , asteroid = require('../asteroid') + , passport = require('passport') + , LocalStrategy = require('passport-local').Strategy; /** * Default User properties. @@ -12,7 +15,7 @@ var properties = { id: {type: String, required: true}, realm: {type: String}, username: {type: String, required: true}, - // password: {type: String, transient: true}, // Transient property + password: {type: String, transient: true}, // Transient property hash: {type: String}, // Hash code calculated from sha256(realm, username, password, salt, macKey) salt: {type: String}, macKey: {type: String}, // HMAC to calculate the hash code @@ -42,3 +45,89 @@ var properties = { var User = module.exports = Model.extend('user', properties); +/** + * Login a user by with the given `credentials`. + * + * User.login({username: 'foo', password: 'bar'}, function (err, session) { + * console.log(session.id); + * }); + * + * @param {Object} credentials + */ + +User.login = function (credentials, fn) { + var UserCtor = this; + + this.findOne({username: credentials.username}, function(err, user) { + var defaultError = new Error('login failed'); + + if(err) { + fn(defaultError); + } else if(user) { + user.hasPassword(credentials.password, function(err, isMatch) { + if(err) { + fn(defaultError); + } else if(isMatch) { + createSession(user, fn); + } else { + fn(defaultError); + } + }); + } else { + fn(defaultError); + } + }); + + function createSession(user, fn) { + var Session = UserCtor.settings.session || asteroid.Session; + + Session.create({uid: user.id}, function (err, session) { + if(err) { + fn(err); + } else { + fn(null, session) + } + }); + } +} + +/** + * Compare the given `password` with the users hashed password. + * + * @param {String} password The plain text password + * @returns {Boolean} + */ + +User.prototype.hasPassword = function (plain, fn) { + // TODO - bcrypt + fn(null, this.password === plain); +} + +/** + * Override the extend method to setup any extended user models. + */ + +User.extend = function () { + var EUser = Model.extend.apply(User, arguments); + + setup(EUser); + + return EUser; +} + +function setup(UserModel) { + asteroid.remoteMethod( + UserModel.login, + { + accepts: [ + {arg: 'credentials', type: 'object', required: true, http: {source: 'body'}} + ], + returns: {arg: 'session', type: 'object'}, + http: {verb: 'post'} + } + ); + + return UserModel; +} + +setup(User); \ No newline at end of file diff --git a/package.json b/package.json index 127cad16..80b32837 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,10 @@ "express": "~3.1.1", "jugglingdb": "git+ssh://git@github.com:strongloop/jugglingdb.git", "sl-remoting": "git+ssh://git@github.com:strongloop/sl-remoting.git", - "inflection": "~1.2.5" + "inflection": "~1.2.5", + "bcrypt": "~0.7.6", + "passport": "~0.1.17", + "passport-local": "~0.1.6" }, "devDependencies": { "mocha": "latest", diff --git a/test/user.test.js b/test/user.test.js new file mode 100644 index 00000000..55b6bf1b --- /dev/null +++ b/test/user.test.js @@ -0,0 +1,38 @@ +var User = asteroid.User; +var passport = require('passport'); + +describe('User', function(){ + + beforeEach(function (done) { + var memory = asteroid.createDataSource({ + connector: asteroid.Memory + }); + asteroid.User.attachTo(memory); + asteroid.Session.attachTo(memory); + app.use(asteroid.cookieParser()); + app.use(asteroid.auth()); + app.use(asteroid.rest()); + app.model(asteroid.User); + + asteroid.User.create({email: 'foo@bar.com', password: 'bar'}, done); + }); + + describe('User.login', function(){ + it('Login a user by providing credentials.', function(done) { + request(app) + .post('/users/login') + .expect('Content-Type', /json/) + .expect(200) + .send({email: 'foo@bar.com', password: 'bar'}) + .end(function(err, res){ + if(err) return done(err); + var session = res.body; + + assert(session.uid); + assert(session.id); + + done(); + }); + }); + }); +}); \ No newline at end of file