2013-07-01 23:50:03 +00:00
|
|
|
/**
|
|
|
|
* Module Dependencies.
|
|
|
|
*/
|
|
|
|
|
2013-07-02 23:51:38 +00:00
|
|
|
var Model = require('../asteroid').Model
|
|
|
|
, asteroid = require('../asteroid')
|
2013-07-03 05:37:31 +00:00
|
|
|
, path = require('path')
|
|
|
|
, crypto = require('crypto')
|
2013-07-02 23:51:38 +00:00
|
|
|
, passport = require('passport')
|
|
|
|
, LocalStrategy = require('passport-local').Strategy;
|
2013-07-01 23:50:03 +00:00
|
|
|
|
2013-07-02 00:01:26 +00:00
|
|
|
/**
|
|
|
|
* Default User properties.
|
|
|
|
*/
|
|
|
|
|
|
|
|
var properties = {
|
|
|
|
id: {type: String, required: true},
|
2013-07-03 05:37:31 +00:00
|
|
|
realm: {type: String, },
|
2013-07-02 00:01:26 +00:00
|
|
|
username: {type: String, required: true},
|
2013-07-02 23:51:38 +00:00
|
|
|
password: {type: String, transient: true}, // Transient property
|
2013-07-02 00:01:26 +00:00
|
|
|
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
|
|
|
|
email: String,
|
|
|
|
emailVerified: Boolean,
|
2013-07-03 05:37:31 +00:00
|
|
|
verificationToken: String,
|
2013-07-02 00:01:26 +00:00
|
|
|
credentials: [
|
|
|
|
'UserCredential' // User credentials, private or public, such as private/public keys, Kerberos tickets, oAuth tokens, facebook, google, github ids
|
|
|
|
],
|
|
|
|
challenges: [
|
|
|
|
'Challenge' // Security questions/answers
|
|
|
|
],
|
|
|
|
// https://en.wikipedia.org/wiki/Multi-factor_authentication
|
|
|
|
/*
|
|
|
|
factors: [
|
|
|
|
'AuthenticationFactor'
|
|
|
|
],
|
|
|
|
*/
|
|
|
|
status: String,
|
|
|
|
created: Date,
|
|
|
|
lastUpdated: Date
|
|
|
|
}
|
|
|
|
|
2013-07-01 23:50:03 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Extends from the built in `asteroid.Model` type.
|
|
|
|
*/
|
|
|
|
|
2013-07-02 00:01:26 +00:00
|
|
|
var User = module.exports = Model.extend('user', properties);
|
2013-07-01 23:50:03 +00:00
|
|
|
|
2013-07-02 23:51:38 +00:00
|
|
|
/**
|
|
|
|
* 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) {
|
2013-07-12 19:40:36 +00:00
|
|
|
var Session = UserCtor.session;
|
2013-07-02 23:51:38 +00:00
|
|
|
|
|
|
|
Session.create({uid: user.id}, function (err, session) {
|
|
|
|
if(err) {
|
|
|
|
fn(err);
|
|
|
|
} else {
|
|
|
|
fn(null, session)
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-07-03 05:37:31 +00:00
|
|
|
/**
|
|
|
|
* Logout a user with the given session id.
|
|
|
|
*
|
|
|
|
* User.logout('asd0a9f8dsj9s0s3223mk', function (err) {
|
|
|
|
* console.log(err || 'Logged out');
|
|
|
|
* });
|
|
|
|
*
|
|
|
|
* @param {String} sessionID
|
|
|
|
*/
|
|
|
|
|
|
|
|
User.logout = function (sid, fn) {
|
|
|
|
var UserCtor = this;
|
|
|
|
|
|
|
|
var Session = UserCtor.settings.session || asteroid.Session;
|
|
|
|
|
|
|
|
Session.findById(sid, function (err, session) {
|
|
|
|
if(err) {
|
|
|
|
fn(err);
|
|
|
|
} else if(session) {
|
|
|
|
session.destroy(fn);
|
|
|
|
} else {
|
|
|
|
fn(new Error('could not find session'));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2013-07-02 23:51:38 +00:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2013-07-03 05:37:31 +00:00
|
|
|
* Verify a user's identity.
|
|
|
|
*
|
|
|
|
* var options = {
|
|
|
|
* type: 'email',
|
|
|
|
* to: user.email,
|
|
|
|
* template: 'verify.ejs',
|
|
|
|
* redirect: '/'
|
|
|
|
* };
|
|
|
|
*
|
|
|
|
* user.verify(options, next);
|
|
|
|
*
|
|
|
|
* @param {Object} options
|
2013-07-02 23:51:38 +00:00
|
|
|
*/
|
|
|
|
|
2013-07-03 05:37:31 +00:00
|
|
|
User.prototype.verify = function (options, fn) {
|
|
|
|
var user = this;
|
|
|
|
assert(typeof options === 'object', 'options required when calling user.verify()');
|
|
|
|
assert(options.type, 'You must supply a verification type (options.type)');
|
|
|
|
assert(options.type === 'email', 'Unsupported verification type');
|
|
|
|
assert(options.to || this.email, 'Must include options.to when calling user.verify() or the user must have an email property');
|
|
|
|
assert(options.from, 'Must include options.from when calling user.verify() or the user must have an email property');
|
2013-07-02 23:51:38 +00:00
|
|
|
|
2013-07-03 05:37:31 +00:00
|
|
|
options.redirect = options.redirect || '/';
|
|
|
|
options.template = path.resolve(options.template || path.join(__dirname, '..', '..', 'templates', 'verify.ejs'));
|
|
|
|
options.user = this;
|
|
|
|
options.protocol = options.protocol || 'http';
|
|
|
|
options.host = options.host || 'localhost';
|
|
|
|
options.verifyHref = options.verifyHref ||
|
|
|
|
options.protocol
|
|
|
|
+ '://'
|
|
|
|
+ options.host
|
|
|
|
+ (User.sharedCtor.http.path || '/' + User.pluralModelName)
|
|
|
|
+ User.confirm.http.path;
|
2013-07-02 23:51:38 +00:00
|
|
|
|
2013-07-03 05:37:31 +00:00
|
|
|
|
|
|
|
|
|
|
|
// Email model
|
2013-07-12 19:40:36 +00:00
|
|
|
var Email = options.mailer || this.constructor.email || asteroid.Email;
|
2013-07-03 05:37:31 +00:00
|
|
|
|
|
|
|
crypto.randomBytes(64, function(err, buf) {
|
|
|
|
if(err) {
|
|
|
|
fn(err);
|
|
|
|
} else {
|
|
|
|
user.verificationToken = buf.toString('base64');
|
|
|
|
user.save(function (err) {
|
|
|
|
if(err) {
|
|
|
|
fn(err);
|
|
|
|
} else {
|
|
|
|
sendEmail(user);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// TODO - support more verification types
|
|
|
|
function sendEmail(user) {
|
|
|
|
options.verifyHref += '?token=' + user.verificationToken;
|
|
|
|
|
|
|
|
options.text = options.text || 'Please verify your email by opening this link in a web browser:\n\t{href}';
|
|
|
|
|
|
|
|
options.text = options.text.replace('{href}', options.verifyHref);
|
|
|
|
|
|
|
|
var template = asteroid.template(options.template);
|
|
|
|
Email.send({
|
|
|
|
to: options.to || user.email,
|
|
|
|
subject: options.subject || 'Thanks for Registering',
|
|
|
|
text: options.text,
|
|
|
|
html: template(options)
|
|
|
|
}, function (err, email) {
|
|
|
|
if(err) {
|
|
|
|
fn(err);
|
|
|
|
} else {
|
|
|
|
fn(null, {email: email, token: user.verificationToken, uid: user.id});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2013-07-02 23:51:38 +00:00
|
|
|
}
|
|
|
|
|
2013-07-03 05:37:31 +00:00
|
|
|
User.confirm = function (uid, token, redirect, fn) {
|
|
|
|
this.findById(uid, function (err, user) {
|
|
|
|
if(err) {
|
|
|
|
fn(err);
|
|
|
|
} else {
|
|
|
|
if(user.verificationToken === token) {
|
|
|
|
user.verificationToken = undefined;
|
|
|
|
user.emailVerified = true;
|
|
|
|
user.save(function (err) {
|
|
|
|
if(err) {
|
|
|
|
fn(err)
|
|
|
|
} else {
|
|
|
|
fn();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
fn(new Error('invalid token'));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2013-07-12 19:40:36 +00:00
|
|
|
* Setup an extended user model.
|
2013-07-03 05:37:31 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
User.setup = function () {
|
|
|
|
var UserModel = this;
|
|
|
|
|
2013-07-02 23:51:38 +00:00
|
|
|
asteroid.remoteMethod(
|
|
|
|
UserModel.login,
|
|
|
|
{
|
|
|
|
accepts: [
|
|
|
|
{arg: 'credentials', type: 'object', required: true, http: {source: 'body'}}
|
|
|
|
],
|
|
|
|
returns: {arg: 'session', type: 'object'},
|
|
|
|
http: {verb: 'post'}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2013-07-03 05:37:31 +00:00
|
|
|
asteroid.remoteMethod(
|
|
|
|
UserModel.logout,
|
|
|
|
{
|
|
|
|
accepts: [
|
|
|
|
{arg: 'sid', type: 'string', required: true}
|
|
|
|
],
|
|
|
|
http: {verb: 'all'}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
asteroid.remoteMethod(
|
|
|
|
UserModel.confirm,
|
|
|
|
{
|
|
|
|
accepts: [
|
|
|
|
{arg: 'uid', type: 'string', required: true},
|
|
|
|
{arg: 'token', type: 'string', required: true},
|
|
|
|
{arg: 'redirect', type: 'string', required: true}
|
|
|
|
],
|
|
|
|
http: {verb: 'get', path: '/confirm'}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
UserModel.on('attached', function () {
|
|
|
|
UserModel.afterRemote('confirm', function (ctx, inst, next) {
|
|
|
|
if(ctx.req) {
|
|
|
|
ctx.res.redirect(ctx.req.param('redirect'));
|
|
|
|
} else {
|
|
|
|
fn(new Error('transport unsupported'));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2013-07-12 19:40:36 +00:00
|
|
|
// default models
|
|
|
|
UserModel.email = require('./email');
|
|
|
|
UserModel.session = require('./session');
|
|
|
|
|
2013-07-02 23:51:38 +00:00
|
|
|
return UserModel;
|
|
|
|
}
|
|
|
|
|
2013-07-03 05:37:31 +00:00
|
|
|
/*!
|
|
|
|
* Setup the base user.
|
|
|
|
*/
|
|
|
|
|
|
|
|
User.setup();
|