ripple-core
Version:
Ripple is an interactive audience response system that allows presenters to survey audience members in real time communication through their mobile devices.
503 lines (452 loc) • 16.8 kB
JavaScript
var bcrypt = require('bcrypt')
, DB = require('./db-manager.js')
, EM = require('./email-dispatcher.js')
, GLOBALS = require('./globals')
, logger = require('./log')
, log = logger.logPair
, plugin = require('./plugins')
, log = logger.logPair;
var util = require('util');
var AM = {};
AM.accounts = DB.init.collection('accounts');
AM.permissions = DB.init.collection('permissions');
AM.passReset = DB.init.collection('password_reset');
module.exports = AM;
// Subsequent Page logins
AM.autoLogin = function(user, pass, callback){
AM.accounts.findOne({user:user}, function(err, o) {
if (o){
o.pass == pass ? callback(o) : callback(null);
} else{
callback(null);
}
});
}
// If the given data is null or a blank string, calls callback with err and returns false,
// otherwise returns true. Meant to be used synchronously - callback use is strictly to help
// the calling function return more easily.
//
// TODO: This belongs in a more generalized module
AM.isBlank = function(data, err, callback) {
if (data === null || data === "") {
callback(err);
return true;
}
return false;
};
// Finds a user in the database with the given username. We do this instead of just calling
// findOne because findOne doesn't give us any error feedback.
AM.findUserByName = function(username, callback) {
AM.accounts.findOne({user:username}, function(err, user) {
if (user === null) {
callback('user-not-found');
} else {
callback(null, user)
}
});
}
// Validates the given password is valid for the given user
AM.validatePassword = function(password, user, callback) {
bcrypt.compare(password, user.pass, function(err, res) {
if (res) {
callback(null, user);
} else {
callback('invalid-password');
}
});
}
// Initial Login that generates the session
AM.manualLogin = function(user, pass, callback) {
var authData = {user: user, password: pass};
// If no plugins handle authentication, just skip to local auth
if (plugin.handlers("auth.presenterAuth").length == 0) {
return AM.localAuth(authData, callback);
}
// TODO: Consider finding a way to invoke plugin handlers in some sort of cascading way such
// that we send the message to handlers, and the first one that handles it stops the rest (and
// stops our default handler). If we don't do something like that, we'll never be able to allow
// multiple auth types (LDAP or Facebook or local, for instance)
/**
* Whenever the account manager's "manualLogin" method is called, this is fired off with a single
* object and a callback as parameters:
* * An object containing "user" and "password" values for authentication.
* * A callback which takes an error and a response object. The error should be null if the
* authentication didn't have any critical errors. The response object should be null to
* allow standard authentication to happen, or else be a user object with the following data:
* * "user": User id
* * "email": User's email address
* * "name": User's full name
* * "password": Password as stored in the database (generally hashed for real local accounts)
*
* Right now authentication happens in multiple places, and only the main sign-in has a handler
* option. The session verification still hits the local database. This means overriding
* authenticate also requires inserting a dummy record into the database.
*
* @event presenterAuth
* @for plugin-server.auth
* @async
* @param {object} authInfo
* @param {function} callback function(err, userObj){}
* if userObj is returned then external authenication was successful
*/
plugin.invokeAll("auth.presenterAuth", authData, function(err, userResponse) {
// If a user was returned, skip normal authentication
if (userResponse) {
return callback(null, userResponse);
}
// No user came from auth plugin, so continue with local user authentication
return AM.localAuth(authData, callback);
});
}
// Validates local account and local account rules. App should call manualLogin unless we
// absolutely do not want plugins to check authentication.
AM.localAuth = function(authData, callback) {
if (AM.isBlank(authData.user, "define-user", callback)) { return; }
if (AM.isBlank(authData.password, "define-password", callback)) { return; }
AM.findUserByName(authData.user, function(err, user) {
if (err) {
callback(err);
}
else {
AM.validatePassword(authData.password, user, callback);
}
});
}
// record insertion, update & deletion methods //
AM.signup = function(newData, callback) {
AM.accounts.findOne({user:newData.user}, function(e, o) {
if (o){
log("[AM.signup] User Signup", util.inspect(o) );
callback('username-taken');
} else{
// External accounts get special treatment:
// * We don't salt or hash the password to keep local auth from hitting this account
// * We don't check for duped email addresses since there's currently no easy way to just
// "link" the local account with external accounts, and we don't do email validation to
// ensure a user actually has the email they claim to have.
if (newData.external) {
return AM.accounts.insert(newData, callback(null));
}
AM.accounts.findOne({email:newData.email}, function(e, o) {
if (o){
callback('email-taken');
} else{
AM.saltAndHash(newData.pass, function(hash){
newData.pass = hash;
AM.accounts.insert(newData, {safe: true}, callback);
});
}
});
}
});
}
AM.update = function(o, newData, callback) {
o.name = newData.name;
o.email = newData.email;
logger.debugPair("[AM.update] User Previous Info", util.inspect(o) );
log("[AM.update] Update User Info", util.inspect(newData) );
if (newData.pass == ''){
AM.accounts.save(o); callback(o);
} else if ( !newData.hasOwnProperty("pass") ){
AM.accounts.update({_id:o._id},{$set:newData}); callback(o);
} else{
AM.saltAndHash(newData.pass, function(hash){
o.pass = hash;
AM.accounts.save(o); callback(o);
});
}
}
AM.updateUserObj = function(userID, userInfoObj, callback){
log("[AM.updateUserObj] object", util.inspect(userInfoObj) );
var userObjID = DB.convertToObjID(userID);
if (userInfoObj.pass === '' || !userInfoObj.hasOwnProperty('pass') ){
// Remove password parameter
delete userInfoObj.pass;
AM.accounts.update({_id: userObjID}, {$set:userInfoObj}, {safe:true}, function(err, o){
callback(err, o);
});
} else{
AM.saltAndHash(userInfoObj.pass, function(hash){
userInfoObj.pass = hash;
AM.accounts.update({_id: userObjID}, {$set:userInfoObj}, {safe:true}, function(err, o){
callback(err, o);
});
});
}
};
AM.saltAndHash = function(pass, callback) {
bcrypt.genSalt(10, function(err, salt) {
bcrypt.hash(pass, salt, function(err, hash) {
callback(hash);
});
});
}
AM.delete = function(id, callback) {
AM.accounts.remove({_id: this.getObjectId(id)}, callback);
}
// auxiliary methods //
AM.getEmail = function(email, callback) {
AM.accounts.findOne({email:email}, function(e, o){ callback(o); });
}
AM.getObjectId = function(id) {
// this is necessary for id lookups, just passing the id fails for some reason //
return AM.accounts.db.bson_serializer.ObjectID.createFromHexString(id)
}
AM.getAllRecords = function(callback) {
AM.accounts.find().toArray(
function(e, res) {
if (e) callback(e)
else callback(null, res)
});
};
AM.delAllRecords = function(id, callback) {
AM.accounts.remove(); // reset accounts collection for testing //
}
// just for testing - these are not actually being used //
AM.findById = function(id, callback) {
AM.accounts.findOne({_id: this.getObjectId(id)},
function(e, res) {
if (e) callback(e)
else callback(null, res)
});
};
AM.findByMultipleFields = function(a, callback) {
// this takes an array of name/val pairs to search against {fieldName : 'value'} //
AM.accounts.find( { $or : a } ).toArray(
function(e, results) {
if (e) callback(e)
else callback(null, results)
});
}
AM.passwordRecovery = function(req, callback){
var emailAddress = req.body.email;
log("Password Recovery requested for",emailAddress);
// Query for email
AM.accounts.findOne({email:emailAddress}, function(err, doc){
if(err) {
logger.errorPair("[AM.passwordRecovery] DB",err);
callback(err);
} else if( !isEmptyObj(doc) ) {
logger.debugPair('Found Email sending password link for ', emailAddress);
// Generate Account File for One-time access
generatePasswordResetDB(doc, function(err, resetObj){
if( !err ) {
// Create link
var link = req.headers.referer + "reset-password/"+resetObj.linkID;
logger.debugPair('Password Email sent to ' + emailAddress, link);
// Send One-time password link
EM.sendPassword(emailAddress, link, function(err){
if(err) logger.errorPair("[AM.passwordRecovery] Password Email was not sent", err);
callback(err);
});
} else {
callback(err);
}
})
} else {
logger.warnPair('[AM.passwordRecovery] User not found', emailAddress);
callback("[404] Email not found");
}
})
};
var generatePasswordResetDB = function(userObj, callback){
var fnName = "[AM>generatePasswordResetDB]";
logger.debugPair(fnName + " User ID", userObj._id );
// Check for userObj
if( !userObj ) {
GLOBALS.error.callbackAndLog("Can not identify User", null, fnName, callback)
callback(errMsg + fnName);
return false;
}
// Generate UUID
var linkString = GLOBALS.helperFn.randomAlphaNum(25);
// Check for Random String
if( !linkString ) {
GLOBALS.error.callbackAndLog("Link ID was not generated", null, fnName, callback)
return false;
}
// Generate Expire Time for 5 minutes
var time = new Date();
expire = new Date( time.getTime() + 5*60000);
// Check for Random String
if( !expire ) {
GLOBALS.error.callbackAndLog("Expiration time could not be generated", null, fnName, callback)
return false;
}
var resetDoc = {
resetID:userObj._id,
linkID: linkString,
expireTime: expire
}
logger.debugPair("[AM>generatePasswordResetDB] Password Reset Access", util.inspect(resetDoc));
// Save Record in DB
AM.passReset.insert(resetDoc,{safe:true}, function(err){
callback(err, resetDoc)
});
};
AM.validateLink = function(guid, callback) {
// Generate Expire Time for 5 minutes
var time = new Date();
expire = new Date( time.getTime() + 5*60000);
// Update Expiration Time
AM.passReset.findAndModify({linkID:guid}, [], {$set:{expireTime:expire}}, {}, function(err, doc){
callback(err, doc);
});
}
AM.setPassword = function(guid, newpass, callback) {
var fnName = "AM.setPassword"
logger.warnPair("[AM.setPassword] GUID - " + guid, newpass);
// Verify GUID
if(!guid){
GLOBALS.error.callbackAndLog("Can not determine reset ID", null, fnName, callback);
return false;
}
// Verify new password
if(!newpass){
GLOBALS.error.callbackAndLog("Can not determine new password", null, fnName, callback);
return false;
}
// Find Reset Doc Object
AM.passReset.findOne({linkID:guid}, function(err, resetObj){
// Hash Password
AM.saltAndHash(newpass, function(hash){
// Update User Password
AM.accounts.update({_id:resetObj.resetID}, {$set:{pass:hash}}, {safe:true}, function(err, cnt){
// Remove reset doc
if(!err) AM.passReset.remove({linkID:guid});
callback(err, cnt);
});
});
});
}
AM.routeAuthUser = function(o, req, res, callback){
if (o != null){
req.session.user = o;
req.session.type = "presenter";
logger.debugPair("[AM.routeAuthUser] " + o.user, req.url);
// AM.renderAuthRoute(route, pageData, res);
callback();
} else{
logger.debug("[AM.routeAuthUser] No user object found redirecting to login");
res.redirect('/');
}
}
AM.userAuth = function(authObj, req, res, callback){
if (req.session.user == null){
// if user is not logged-in redirect back to login page //
res.redirect('/');
} else {
var session = req.session.user;
AM.autoLogin(session.user, session.pass, function(userObj){
logger.debugPair("AM.userAuth Object", util.inspect(userObj) );
if( userObj != null ){
if( isEmptyObj(authObj) ) {
AM.routeAuthUser(userObj, req, res, function(){
callback(null, userObj);
});
} else {
// Authorization Object needs to be checked
logger.debugPair("[AM.userAuth] Checking Permissions", util.inspect(authObj) );
AM.authCheck(authObj, userObj, req, res, callback);
}
} else {
logger.errorPair("AM.userAuth Error", "No User Object found");
res.redirect('/');
//callback("No User Object found [AM.userAuth]");
}
});
}
}
AM.authCheck = function(authObj, userObj, req, res, callback){
logger.debugPair("[AM.authCheck] check on", authObj.type);
switch (authObj.type) {
case 'session':
authCheckSession(authObj, userObj, req, res, callback);
break;
case 'permissions':
authCheckPermissions(authObj, userObj, req, res, callback);
break;
}
}
authCheckSession = function(authObj, userObj, req, res, callback){
// Make sure that there is are needed properties
if( typeof authObj !== 'object') callback('ERROR :: authCheckSession not object.');
if( !userObj.hasOwnProperty('_id') || userObj._id === '') callback('ERROR :: authCheckSession no user defined.');
if( !authObj.hasOwnProperty('element') || authObj.element === '') callback('ERROR :: authCheckSession no user defined.');
// Query db for rights
logger.debugPair('[authCheckSession] element', authObj.element);
// Convert string to objectID
elemID = DB.convertToObjID(authObj.element);
DB.init.collection('sessions').findOne({'_id':elemID}, function(err, record){
if( !err ) {
if(!record){
callback("ERROR: No record returned in authCheckSession");
}
logger.debugPair('[authCheckSession] Records', util.inspect(record));
logger.debugPair('[authCheckSession] User', userObj._id );
logger.debugPair('[authCheckSession] Author', record.author );
if( record.author.toString() === userObj._id.toString() ) callback(null, userObj, record);
else callback("ERROR: authCheckSession user does not have rights for content.");
}
else callback(err);
})
}
authCheckPermissions = function(authObj, userObj, req, res, callback){
logger.debugPair("[AM-authCheckPermissions] Permission Object to verify",util.inspect(authObj) );
var permission = false;
// Look up role permission
AM.permissions.findOne({name:authObj.component}, function(err, record){
if( err ) {
callback(err);
return;
}
if( !record ){
callback("No permission found for component");
return;
}
// Compare role to role permissions
logger.debugPair("AuthCheckPermissions Record", util.inspect(record));
var roles = record.roles;
if( typeof roles === "undefined") {
callback("Component's roles not defined.");
return;
}
roles.forEach(function(item, index){
// Exit loop if found role match
if( permission ) return;
else if( item === 'presenter'){
permission = true;
// Log Permission
logger.debugPair('Permission Status for ' + item, permission);
return
} else if( typeof userObj.roles !== 'undefined'){
userObj.roles.forEach(function(authRole){
// Exit loop if found role match
if( permission ) return;
if( item === 'presenter' || item == authRole ) {
// Role Found
permission = true;
}
});
}
// Log Permission
logger.debugPair('Permission Status for ' + item, permission);
})
if( permission ) {
AM.routeAuthUser(userObj, req, res, function(){
callback(null, userObj);
});
}
else callback('You do not have permission to access component: ' + authObj.component);
})
};
AM.logout = function(req, res, callback){
res.clearCookie('connect.sid');
req.session.destroy( callback );
}
isEmptyObj = function(obj) {
for(var key in obj) {
if (obj.hasOwnProperty(key)) return false;
}
return true;
};