Merge branch 'develop' of github.com:HabitRPG/habitrpg into common-convert

Conflicts:
	src/controllers/auth.js
	src/controllers/challenges.js
	src/controllers/groups.js
	src/controllers/members.js
	src/controllers/payments/index.js
	src/controllers/user.js
	src/middleware.js
	src/models/user.js
This commit is contained in:
Blade Barringer
2015-02-03 21:13:55 -06:00
25 changed files with 600 additions and 250 deletions
+96 -68
View File
@@ -23,6 +23,10 @@ var accountSuspended = function(uuid){
code: 'ACCOUNT_SUSPENDED'
};
}
// escape email for regex, then search case-insensitive. See http://stackoverflow.com/a/3561711/362790
var mongoEmailRegex = function(email){
return new RegExp('^' + email.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$', 'i');
}
api.auth = function(req, res, next) {
var uid = req.headers['x-api-user'];
@@ -61,55 +65,57 @@ api.authWithUrl = function(req, res, next) {
}
api.registerUser = function(req, res, next) {
var confirmPassword = req.body.confirmPassword,
email = req.body.email,
password = req.body.password,
username = req.body.username;
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"});
if (!validator.isEmail(email)) return res.json(401, {err: ":email invalid"});
async.waterfall([
function(cb) {
User.findOne({'auth.local.email': email}, cb);
async.auto({
validate: function(cb) {
if (!(req.body.username && req.body.password && req.body.email))
return cb({code:401, err: ":username, :email, :password, :confirmPassword required"});
if (req.body.password !== req.body.confirmPassword)
return cb({code:401, err: ":password and :confirmPassword don't match"});
if (!validator.isEmail(req.body.email))
return cb({code:401, err: ":email invalid"});
cb();
},
function(found, cb) {
if (found) return cb("Email already taken");
User.findOne({'auth.local.username': username}, cb);
}, function(found, cb) {
var newUser, salt, user;
if (found) return cb("Username already taken");
salt = utils.makeSalt();
newUser = {
findEmail: function(cb) {
User.findOne({'auth.local.email': req.body.email}, cb);
},
findUname: function(cb) {
User.findOne({'auth.local.username': req.body.username}, cb);
},
findFacebook: function(cb){
User.findOne({_id: req.headers['x-api-user'], apiToken: req.headers['x-api-key']}, {auth:1}, cb);
},
register: ['validate', 'findEmail', 'findUname', 'findFacebook', function(cb, data) {
if (data.findEmail) return cb({code:401, err:"Email already taken"});
if (data.findUname) return cb({code:401, err:"Username already taken"});
var salt = utils.makeSalt();
var newUser = {
auth: {
local: {
username: username,
email: email,
username: req.body.username,
email: req.body.email,
salt: salt,
hashed_password: utils.encryptPassword(password, salt)
hashed_password: utils.encryptPassword(req.body.password, salt)
},
timestamps: {created: +new Date(), loggedIn: +new Date()}
}
};
newUser.preferences = newUser.preferences || {};
newUser.preferences.language = req.language; // User language detected from browser, not saved
user = new User(newUser);
// temporary for conventions
if (req.subdomains[0] == 'con') {
_.each(user.dailys, function(h){
h.repeat = {m:false,t:false,w:false,th:false,f:false,s:false,su:false};
})
user.extra = {signupEvent: 'wondercon'};
// existing user, allow them to add local authentication
if (data.findFacebook) {
data.findFacebook.auth.local = newUser.auth.local;
data.findFacebook.save(cb);
// new user, register them
} else {
newUser.preferences = newUser.preferences || {};
newUser.preferences.language = req.language; // User language detected from browser, not saved
var user = new User(newUser);
utils.txnEmail(user, 'welcome');
ga.event('register', 'Local').send();
user.save(cb);
}
user.save(cb);
if(isProd) utils.txnEmail({name:username, email:email}, 'welcome');
ga.event('register', 'Local').send()
}
], function(err, saved) {
if (err) return res.json(401, {err: err});
res.json(200, saved);
email = password = username = null;
}]
}, function(err, data) {
if (err) return err.code ? res.json(err.code, err) : next(err);
res.json(200, data.register[0]);
});
};
@@ -173,9 +179,7 @@ api.loginSocial = function(req, res, next) {
user = new User(user);
user.save(cb);
if (isProd && prof.emails && prof.emails[0] && prof.emails[0].value) {
utils.txnEmail({name: prof.displayName || prof.username, email: prof.emails[0].value}, 'welcome');
}
utils.txnEmail(user, 'welcome');
ga.event('register', network).send();
}]
}, function(err, results){
@@ -188,9 +192,18 @@ api.loginSocial = function(req, res, next) {
/**
* DELETE /user/auth/social
* TODO implement
*/
api.deleteSocial = function(req,res,next){next()}
api.deleteSocial = function(req,res,next){
if (!res.locals.user.auth.local.username)
return res.json(401, {err:"Account lacks another authentication method, can't detach Facebook"});
//FIXME for some reason, the following gives https://gist.github.com/lefnire/f93eb306069b9089d123
//res.locals.user.auth.facebook = null;
//res.locals.user.auth.save(function(err, saved){
User.update({_id:res.locals.user._id}, {$unset:{'auth.facebook':1}}, function(err){
if (err) return next(err);
res.send(200);
})
}
api.resetPassword = function(req, res, next){
var email = req.body.email,
@@ -198,9 +211,7 @@ api.resetPassword = function(req, res, next){
newPassword = utils.makeSalt(), // use a salt as the new password too (they'll change it later)
hashed_password = utils.encryptPassword(newPassword, salt);
// escape email for regex, then search case-insensitive. See http://stackoverflow.com/a/3561711/362790
var emailRegExp = new RegExp('^' + email.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$', 'i');
User.findOne({'auth.local.email':emailRegExp}, function(err, user){
User.findOne({'auth.local.email':mongoEmailRegex(email)}, function(err, user){
if (err) return next(err);
if (!user) return res.send(500, {err:"Couldn't find a user registered for email " + email});
user.auth.local.salt = salt;
@@ -217,28 +228,45 @@ api.resetPassword = function(req, res, next){
});
};
var invalidPassword = function(user, password){
var hashed_password = utils.encryptPassword(password, user.auth.local.salt);
if (hashed_password !== user.auth.local.hashed_password)
return {code:401, err:"Incorrect password"};
return false;
}
api.changeUsername = function(req, res, next) {
var user = res.locals.user,
password = req.body.password,
newUsername = req.body.newUsername;
async.waterfall([
function(cb){
User.findOne({'auth.local.username': req.body.username}, {auth:1}, cb);
},
function(found, cb){
if (found) return cb({code:401, err: "Username already taken"});
if (invalidPassword(res.locals.user, req.body.password)) return cb(invalidPassword(res.locals.user, req.body.password));
res.locals.user.auth.local.username = req.body.username;
res.locals.user.save(cb);
}
], function(err){
if (err) return err.code ? res.json(err.code, err) : next(err);
res.send(200);
})
}
User.findOne({'auth.local.username': newUsername}, function(err, result) {
if (err) next(err);
if(result) return res.json(401, {err: "Username already taken"});
var salt = user.auth.local.salt;
var hashed_password = utils.encryptPassword(password, salt);
if (hashed_password !== user.auth.local.hashed_password)
return res.json(401, {err:"Incorrect password"});
user.auth.local.username = newUsername;
user.save(function(err, saved){
if (err) next(err);
res.send(200);
user = password = newUsername = null;
})
});
api.changeEmail = function(req, res, next){
async.waterfall([
function(cb){
User.findOne({'auth.local.email': mongoEmailRegex(req.body.email)}, {auth:1}, cb);
},
function(found, cb){
if(found) return cb({code:401, err: "Email already taken"});
if (invalidPassword(res.locals.user, req.body.password)) return cb(invalidPassword(res.locals.user, req.body.password));
res.locals.user.auth.local.email = req.body.email;
res.locals.user.save(cb);
}
], function(err){
if (err) return err.code ? res.json(err.code,err) : next(err);
res.send(200);
})
}
api.changePassword = function(req, res, next) {
+6
View File
@@ -9,6 +9,7 @@ var Group = require('./../models/group').model;
var Challenge = require('./../models/challenge').model;
var logging = require('./../logging');
var csv = require('express-csv');
var utils = require('../utils');
var api = module.exports;
@@ -335,6 +336,11 @@ api.selectWinner = function(req, res, next) {
winner.save(cb);
},
function(saved, num, cb) {
if(saved.preferences.emailNotifications.wonChallenge !== false){
utils.txnEmail(saved, 'won-challenge', [
{name: 'CHALLENGE_NAME', content: chal.name}
]);
}
closeChal(cid, {broken: 'CHALLENGE_CLOSED', winner: saved.profile.name}, cb);
}
], function(err){
+25 -6
View File
@@ -301,13 +301,11 @@ api.flagChatMessage = function(req, res, next){
group.markModified('chat');
group.save(function(err,_saved){
if(err) return next(err);
if (isProd){
var addressesToSendTo = JSON.parse(nconf.get('FLAG_REPORT_EMAIL'));
if(Array.isArray(addressesToSendTo)){
addressesToSendTo = addressesToSendTo.map(function(email){
return {email: email}
return {email: email, canSend: true}
});
}else{
addressesToSendTo = {email: addressesToSendTo}
@@ -332,7 +330,7 @@ api.flagChatMessage = function(req, res, next){
{name: "GROUP_ID", content: group._id},
{name: "GROUP_URL", content: group._id == 'habitrpg' ? (nconf.get('BASE_URL') + '/#/options/groups/tavern') : (group.type === 'guild' ? (nconf.get('BASE_URL')+ '/#/options/groups/guilds/' + group._id) : 'party')},
]);
}
return res.send(204);
});
});
@@ -556,6 +554,26 @@ api.invite = function(req, res, next) {
], function(err, results){
if (err) return next(err);
if(invite.preferences.emailNotifications['invited' + (group.type == 'guild' ? 'Guild' : 'Party')] !== false){
var emailVars = [
{name: 'INVITER', content: utils.getUserInfo(res.locals.user, ['name']).name}
];
if(group.type == 'guild'){
emailVars.push(
{name: 'GUILD_NAME', content: group.name},
{name: 'GUILD_URL', content: nconf.get('BASE_URL') + '/#/options/groups/guilds/public'}
);
}else{
emailVars.push(
{name: 'PARTY_NAME', content: group.name},
{name: 'PARTY_URL', content: nconf.get('BASE_URL') + '/#/options/groups/party'}
)
}
utils.txnEmail(invite, ('invited-' + (group.type == 'guild' ? 'guild' : 'party')), emailVars);
}
// Have to return whole group and its members for angular to show the invited user
res.json(results[2]);
group = uuid = null;
@@ -629,7 +647,7 @@ questStart = function(req, res, next) {
var group = res.locals.group;
var force = req.query.force;
// if (group.quest.active) return res.json(400,{err:'Quest already began.'});
// if (group.quest.active) return res.json(400,{err:'Quest already began.'});
// temporarily send error email, until we know more about this issue (then remove below, uncomment above).
if (group.quest.active) return next('Quest already began.');
@@ -720,8 +738,9 @@ api.questAccept = function(req, res, next) {
if (m == user._id) {
group.quest.members[m] = true;
group.quest.leader = user._id;
} else
} else {
group.quest.members[m] = undefined;
}
});
// Party member accepting the invitation
+18
View File
@@ -5,6 +5,8 @@ var api = module.exports;
var async = require('async');
var _ = require('lodash');
var shared = require('../../../common');
var utils = require('../utils');
var nconf = require('nconf');
var fetchMember = function(uuid, restrict){
return function(cb){
@@ -48,9 +50,11 @@ api.sendMessage = function(user, member, data){
}
api.sendPrivateMessage = function(req, res, next){
var fetchedMember;
async.waterfall([
fetchMember(req.params.uuid),
function(member, cb) {
fetchedMember = member;
if (~member.inbox.blocks.indexOf(res.locals.user._id) // can't send message if that user blocked me
|| ~res.locals.user.inbox.blocks.indexOf(member._id) // or if I blocked them
|| member.inbox.optOut) { // or if they've opted out of messaging
@@ -64,6 +68,14 @@ api.sendPrivateMessage = function(req, res, next){
}
], function(err){
if (err) return sendErr(err, res, next);
if(fetchedMember.preferences.emailNotifications.newPM !== false){
utils.txnEmail(fetchedMember, 'new-pm', [
{name: 'SENDER', content: utils.getUserInfo(res.locals.user, ['name']).name},
{name: 'PMS_INBOX_URL', content: nconf.get('BASE_URL') + '/#/options/groups/inbox'}
]);
}
res.send(200);
})
}
@@ -84,6 +96,12 @@ api.sendGift = function(req, res, next){
member.balance += amt;
user.balance -= amt;
api.sendMessage(user, member, req.body);
if(member.preferences.emailNotifications.giftedGems !== false){
utils.txnEmail(member, 'gifted-gems', [
{name: 'GIFTER', content: utils.getUserInfo(user, ['name']).name},
{name: 'X_GEMS_GIFTED', content: req.body.gems.amount}
]);
}
return async.parallel([
function (cb2) { member.save(cb2) },
function (cb2) { user.save(cb2) }
+20 -4
View File
@@ -73,7 +73,15 @@ exports.createSubscription = function(data, cb) {
utils.ga.transaction(data.user._id, block.price).item(block.price, 1, data.paymentMethod.toLowerCase() + '-subscription', data.paymentMethod).send();
}
data.user.purchased.txnCount++;
if (data.gift) members.sendMessage(data.user, data.gift.member, data.gift);
if (data.gift){
members.sendMessage(data.user, data.gift.member, data.gift);
if(data.gift.member.preferences.emailNotifications.giftedSubscription !== false){
utils.txnEmail(member, 'gifted-subscription', [
{name: 'GIFTER', content: utils.getUserInfo(data.user, ['name']).name},
{name: 'X_MONTHS_SUBSCRIPTION', content: months}
]);
}
}
async.parallel([
function(cb2){data.user.save(cb2)},
function(cb2){data.gift ? data.gift.member.save(cb2) : cb2(null);}
@@ -96,7 +104,7 @@ exports.cancelSubscription = function(data, cb) {
p.extraMonths = 0; // clear extra time. If they subscribe again, it'll be recalculated from p.dateTerminated
data.user.save(cb);
if(isProduction) utils.txnEmail(data.user, 'cancel-subscription');
utils.txnEmail(data.user, 'cancel-subscription');
utils.ga.event('unsubscribe', data.paymentMethod).send();
}
@@ -110,7 +118,15 @@ exports.buyGems = function(data, cb) {
//TODO ga.transaction to reflect whether this is gift or self-purchase
utils.ga.transaction(data.user._id, amt).item(amt, 1, data.paymentMethod.toLowerCase() + "-checkout", "Gems > " + data.paymentMethod).send();
}
if (data.gift) members.sendMessage(data.user, data.gift.member, data.gift);
if (data.gift){
members.sendMessage(data.user, data.gift.member, data.gift);
if(data.gift.member.preferences.emailNotifications.giftedGems !== false){
utils.txnEmail(member, 'gifted-gems', [
{name: 'GIFTER', content: utils.getUserInfo(data.user, ['name']).name},
{name: 'X_GEMS_GIFTED', content: data.gift.gems.amount || 20}
]);
}
}
async.parallel([
function(cb2){data.user.save(cb2)},
function(cb2){data.gift ? data.gift.member.save(cb2) : cb2(null);}
@@ -137,4 +153,4 @@ exports.paypalCheckoutSuccess = paypal.executePayment;
exports.paypalIPN = paypal.ipn;
exports.iapAndroidVerify = iap.androidVerify;
exports.iapIosVerify = iap.iosVerify;
exports.iapIosVerify = iap.iosVerify;
+55 -59
View File
@@ -261,58 +261,36 @@ api.update = function(req, res, next) {
};
api.cron = function(req, res, next) {
try{
var user = res.locals.user,
progress = user.fns.cron(),
ranCron = user.isModified(),
quest = shared.content.quests[user.party.quest.key];
var user = res.locals.user,
progress = user.fns.cron(),
ranCron = user.isModified(),
quest = shared.content.quests[user.party.quest.key];
if (ranCron) res.locals.wasModified = true;
if (!ranCron) return next(null,user);
Group.tavernBoss(user,progress);
if (!quest) return user.save(next);
// FOR DEBUGGING, PLEASE IGNORE
var opStatus = null;
// If user is on a quest, roll for boss & player, or handle collections
// FIXME this saves user, runs db updates, loads user. Is there a better way to handle this?
async.waterfall([
function(cb){
opStatus = 'saveUser';
user.save(cb); // make sure to save the cron effects
},
function(saved, count, cb){
opStatus = 'runQuest';
var type = quest.boss ? 'boss' : 'collect';
Group[type+'Quest'](user,progress,cb);
},
function(){
var cb = arguments[arguments.length-1];
// User has been updated in boss-grapple, reload
User.findById(user._id, cb);
}
], function(err, saved) {
if(err) logging.loggly({
error: "Cron caught",
stack: (err.stack || err.message || err),
body: req.body, headers: req.header,
auth: req.headers['x-api-user'],
originalUrl: req.originalUrl,
opStatus: opStatus
});
res.locals.user = saved;
next(err,saved);
user = progress = quest = null;
});
}catch(e){
logging.loggly({
error: "Cron uncaught",
stack: e.stack || e
});
throw e;
}
if (ranCron) res.locals.wasModified = true;
if (!ranCron) return next(null,user);
Group.tavernBoss(user,progress);
if (!quest) return user.save(next);
// If user is on a quest, roll for boss & player, or handle collections
// FIXME this saves user, runs db updates, loads user. Is there a better way to handle this?
async.waterfall([
function(cb){
user.save(cb); // make sure to save the cron effects
},
function(saved, count, cb){
var type = quest.boss ? 'boss' : 'collect';
Group[type+'Quest'](user,progress,cb);
},
function(){
var cb = arguments[arguments.length-1];
// User has been updated in boss-grapple, reload
User.findById(user._id, cb);
}
], function(err, saved) {
res.locals.user = saved;
next(err,saved);
user = progress = quest = null;
});
};
// api.reroll // Shared.ops
@@ -437,16 +415,34 @@ api.cast = function(req, res, next) {
api.inviteFriends = function(req, res, next) {
Group.findOne({type:'party', members:{'$in': [res.locals.user._id]}}).select('_id name').exec(function(err,party){
if (err) return next(err);
var link = nconf.get('BASE_URL')+'?partyInvite='+ utils.encrypt(JSON.stringify({id:party._id, inviter:res.locals.user._id, name:party.name}));
_.each(req.body.emails, function(invite){
if (invite.email) {
var variables = [
{name: 'LINK', content: link},
{name: 'INVITER', content: req.body.inviter || res.locals.user.profile.name},
{name: 'INVITEE', content: invite.name}
];
// TODO implement "users can only be invited once"
utils.txnEmail(invite, 'invite-friend', variables);
User.findOne({$or: [
{'auth.local.email': invite.email},
{'auth.facebook.emails.value': invite.email}
]}).select({_id: true, 'preferences.emailNotifications': true})
.exec(function(err, userToContact){
if(err) return next(err);
var link = nconf.get('BASE_URL')+'?partyInvite='+ utils.encrypt(JSON.stringify({id:party._id, inviter:res.locals.user._id, name:party.name}));
var variables = [
{name: 'LINK', content: link},
{name: 'INVITER', content: req.body.inviter || utils.getUserInfo(res.locals.user, ['name']).name}
];
invite.canSend = true;
// We check for unsubscribeFromAll here because don't pass through utils.getUserInfo
if(!userToContact || (userToContact.preferences.emailNotifications.invitedParty !== false &&
userToContact.preferences.emailNotifications.unsubscribeFromAll !== true)){
// TODO implement "users can only be invited once"
utils.txnEmail(invite, 'invite-friend', variables);
}
});
}
});
res.send(200);
@@ -477,7 +473,7 @@ api.sessionPartyInvite = function(req,res,next){
}
/**
* All other user.ops which can easily be mapped to ../../common/scripts/index.coffee, not requiring custom API-wrapping
* All other user.ops which can easily be mapped to habitrpg-shared/index.coffee, not requiring custom API-wrapping
*/
_.each(shared.wrap({}).ops, function(op,k){
if (!api[k]) {