Merge branch 'develop' into srrvnn-develop

This commit is contained in:
Blade Barringer
2015-11-02 06:45:45 -06:00
654 changed files with 19170 additions and 15663 deletions
+7 -3
View File
@@ -31,7 +31,9 @@ function track(eventType, data) {
function _sendDataToAmplitude(eventType, data) {
var amplitudeData = _formatDataForAmplitude(data);
amplitudeData.event_type = eventType;
amplitude.track(amplitudeData);
amplitude.track(amplitudeData).catch(function(error) {
// @TODO log error with new relic
});
}
function _sendDataToGoogle(eventType, data) {
@@ -87,7 +89,9 @@ function _sendPurchaseDataToAmplitude(data) {
amplitudeData.event_type = 'purchase';
amplitudeData.revenue = data.purchaseValue;
amplitude.track(amplitudeData)
amplitude.track(amplitudeData).catch(function(error) {
// @TODO log error with new relic
});
}
function _formatDataForAmplitude(data) {
@@ -95,7 +99,7 @@ function _formatDataForAmplitude(data) {
var event_properties = _.omit(data, PROPERTIES_TO_SCRUB);
var ampData = {
user_id: data.uuid,
user_id: data.uuid || 'no-user-id-was-provided',
platform: 'server',
event_properties: event_properties
}
+52 -31
View File
@@ -16,19 +16,15 @@ var isProd = nconf.get('NODE_ENV') === 'production';
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."};
var NO_SESSION_FOUND = { err: "You must be logged in." };
var NO_TOKEN_OR_UID = { err: shared.i18n.t('messageAuthMustIncludeTokens') };
var NO_USER_FOUND = {err: shared.i18n.t('messageAuthNoUserFound') };
var NO_SESSION_FOUND = { err: shared.i18n.t('messageAuthMustBeLoggedIn') };
var accountSuspended = function(uuid){
return {
err: 'Account has been suspended, please contact leslie@habitica.com with your UUID ('+uuid+') for assistance.',
code: 'ACCOUNT_SUSPENDED'
};
}
// Allow case-insensitive regex searching for Mongo queries. See http://stackoverflow.com/a/3561711/362790
var RegexEscape = function(s){
return new RegExp('^' + s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$', 'i');
}
api.auth = function(req, res, next) {
var uid = req.headers['x-api-user'];
@@ -67,35 +63,42 @@ api.authWithUrl = function(req, res, next) {
}
api.registerUser = function(req, res, next) {
var regEmail = RegexEscape(req.body.email),
regUname = RegexEscape(req.body.username);
var email = req.body.email && req.body.email.toLowerCase();
var username = req.body.username;
// Get the lowercase version of username to check that we do not have duplicates
// So we can search for it in the database and then reject the choosen username if 1 or more results are found
var lowerCaseUsername = username && username.toLowerCase();
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 (!(username && req.body.password && email))
return cb({code:401, err: shared.i18n.t('messageAuthCredentialsRequired')});
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: shared.i18n.t('messageAuthPasswordMustMatch')});
if (!validator.isEmail(email))
return cb({code:401, err: ":email invalid"});
cb();
},
findReg: function(cb) {
User.findOne({$or:[{'auth.local.email': regEmail}, {'auth.local.username': regUname}]}, {'auth.local':1}, cb);
// Search for duplicates using lowercase version of username
User.findOne({$or:[{'auth.local.email': email}, {'auth.local.lowerCaseUsername': lowerCaseUsername}]}, {'auth.local':1}, cb);
},
findFacebook: function(cb){
User.findOne({_id: req.headers['x-api-user'], apiToken: req.headers['x-api-key']}, {auth:1}, cb);
},
register: ['validate', 'findReg', 'findFacebook', function(cb, data) {
if (data.findReg) {
if (regEmail.test(data.findReg.auth.local.email)) return cb({code:401, err:"Email already taken"});
if (regUname.test(data.findReg.auth.local.username)) return cb({code:401, err:"Username already taken"});
if (email === data.findReg.auth.local.email) return cb({code:401, err:"Email already taken"});
// Check that the lowercase username isn't already used
if (lowerCaseUsername === data.findReg.auth.local.lowerCaseUsername) return cb({code:401, err: shared.i18n.t('messageAuthUsernameTaken')});
}
var salt = utils.makeSalt();
var newUser = {
auth: {
local: {
username: req.body.username,
email: req.body.email,
username: username,
lowerCaseUsername: lowerCaseUsername, // Store the lowercase version of the username
email: email, // Store email as lowercase
salt: salt,
hashed_password: utils.encryptPassword(req.body.password, salt)
},
@@ -115,12 +118,14 @@ api.registerUser = function(req, res, next) {
var analyticsData = {
category: 'acquisition',
type: 'local',
gaLabel: 'local'
gaLabel: 'local',
uuid: user._id,
};
analytics.track('register', analyticsData)
user.save(function(err, savedUser){
// Clean previous email preferences
// TODO when emails added to EmailUnsubcription they should use lowercase version
EmailUnsubscription.remove({email: savedUser.auth.local.email}, function(){
utils.txnEmail(savedUser, 'welcome');
});
@@ -143,7 +148,10 @@ api.loginLocal = function(req, res, next) {
var username = req.body.username;
var password = req.body.password;
if (!(username && password)) return res.json(401, {err:'Missing :username or :password in request body, please provide both'});
var login = validator.isEmail(username) ? {'auth.local.email':username} : {'auth.local.username':username};
var login = validator.isEmail(username) ?
{'auth.local.email':username.toLowerCase()} : // Emails are all lowercase
{'auth.local.username':username}; // Use the username as the user typed it
User.findOne(login, {auth:1}, function(err, user){
if (err) return next(err);
if (!user) return res.json(401, {err:"Uh-oh - your username or password is incorrect.\n- Make sure your username or email is typed correctly.\n- You may have signed up with Facebook, not email. Double-check by trying Facebook login.\n- If you forgot your password, click \"Forgot Password\"."});
@@ -205,7 +213,8 @@ api.loginSocial = function(req, res, next) {
var analyticsData = {
category: 'acquisition',
type: network,
gaLabel: network
gaLabel: network,
uuid: user._id,
};
analytics.track('register', analyticsData)
}]
@@ -233,12 +242,14 @@ api.deleteSocial = function(req,res,next){
}
api.resetPassword = function(req, res, next){
var email = req.body.email,
var email = req.body.email && req.body.email.toLowerCase(), // Emails are all lowercase
salt = utils.makeSalt(),
newPassword = utils.makeSalt(), // use a salt as the new password too (they'll change it later)
hashed_password = utils.encryptPassword(newPassword, salt);
User.findOne({'auth.local.email': RegexEscape(email)}, function(err, user){
if(!email) return res.json(400, {err: "Email not provided"});
User.findOne({'auth.local.email': email}, function(err, user){
if (err) return next(err);
if (!user) return res.send(401, {err:"Sorry, we can't find a user registered with email " + email + "\n- Make sure your email address is typed correctly.\n- You may have signed up with Facebook, not email. Double-check by trying Facebook login."});
user.auth.local.salt = salt;
@@ -266,15 +277,22 @@ var invalidPassword = function(user, password){
}
api.changeUsername = function(req, res, next) {
var user = res.locals.user;
var username = req.body.username;
var lowerCaseUsername = username && username.toLowerCase(); // we search for the lowercased version to intercept duplicates
if(!username) return res.json(400, {err: "Username not provided"});
async.waterfall([
function(cb){
User.findOne({'auth.local.username': RegexEscape(req.body.username)}, {auth:1}, cb);
User.findOne({'auth.local.lowerCaseUsername': lowerCaseUsername}, {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);
if (invalidPassword(user, req.body.password)) return cb(invalidPassword(user, req.body.password));
user.auth.local.username = username;
user.auth.local.lowerCaseUsername = lowerCaseUsername;
user.save(cb);
}
], function(err){
if (err) return err.code ? res.json(err.code, err) : next(err);
@@ -283,14 +301,17 @@ api.changeUsername = function(req, res, next) {
}
api.changeEmail = function(req, res, next){
var email = req.body.email && req.body.email.toLowerCase(); // emails are all lowercase
if(!email) return res.json(400, {err: "Email not provided"});
async.waterfall([
function(cb){
User.findOne({'auth.local.email': RegexEscape(req.body.email)}, {auth:1}, cb);
User.findOne({'auth.local.email': email}, {auth:1}, cb);
},
function(found, cb){
if(found) return cb({code:401, err: "Email already taken"});
if(found) return cb({code:401, err: shared.i18n.t('messageAuthEmailTaken')});
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.auth.local.email = email;
res.locals.user.save(cb);
}
], function(err){
@@ -333,7 +354,7 @@ api.getFirebaseToken = function(req, res, next) {
.createToken({
uid: user._id,
isHabiticaUser: true
}, {
}, {
expires: expires
});
+5 -5
View File
@@ -230,7 +230,7 @@ api.update = function(req, res, next){
// before-save / after-save comparison to determine if we need to sync to users
before = _before;
var attrs = _.pick(req.body, 'name shortName description habits dailys todos rewards date'.split(' '));
Challenge.findByIdAndUpdate(cid, {$set:attrs}, cb);
Challenge.findByIdAndUpdate(cid, {$set:attrs}, {new: true}, cb);
},
function(saved, cb) {
@@ -271,7 +271,7 @@ function closeChal(cid, broken, cb) {
function(_removed, cb2) {
removed = _removed;
var pull = {'$pull':{}}; pull['$pull'][_removed._id] = 1;
Group.findByIdAndUpdate(_removed.group, pull);
Group.findByIdAndUpdate(_removed.group, {new: true}, pull);
User.find({_id:{$in: removed.members}}, cb2);
},
function(users, cb2) {
@@ -297,7 +297,7 @@ function closeChal(cid, broken, cb) {
/**
* Delete & close
*/
api['delete'] = function(req, res, next){
api.delete = function(req, res, next){
var user = res.locals.user;
var cid = req.params.cid;
@@ -370,7 +370,7 @@ api.join = function(req, res, next){
async.waterfall([
function(cb) {
Challenge.findByIdAndUpdate(cid, {$addToSet:{members:user._id}}, cb);
Challenge.findByIdAndUpdate(cid, {$addToSet:{members:user._id}}, {new: true}, cb);
},
function(chal, cb) {
@@ -403,7 +403,7 @@ api.leave = function(req, res, next){
async.waterfall([
function(cb){
Challenge.findByIdAndUpdate(cid, {$pull:{members:user._id}}, cb);
Challenge.findByIdAndUpdate(cid, {$pull:{members:user._id}}, {new: true}, cb);
},
function(chal, cb){
+24 -20
View File
@@ -143,7 +143,7 @@ api.get = function(req, res, next) {
q.exec(function(err, group){
if (err) return next(err);
if(!group){
if(gid !== 'party') return res.json(404,{err: "Group not found or you don't have access."});
if(gid !== 'party') return res.json(404,{err: shared.i18n.t('messageGroupNotFound')});
// Don't send a 404 when querying for a party even if it doesn't exist
// so that users with no party don't get a 404 on every access to the site
@@ -188,7 +188,7 @@ api.create = function(req, res, next) {
group.leader = user._id;
if(group.type === 'guild'){
if(user.balance < 1) return res.json(401, {err: 'Not enough gems!'});
if(user.balance < 1) return res.json(401, {err: shared.i18n.t('messageInsufficientGems')});
group.balance = 1;
user.balance--;
@@ -213,7 +213,7 @@ api.create = function(req, res, next) {
Group.findOne({type:'party',members:{$in:[user._id]}},cb);
},
function(found, cb){
if (found) return cb('Already in a party, try refreshing.');
if (found) return cb(shared.i18n.t('messageGroupAlreadyInParty'));
group.save(cb);
},
function(saved, count, cb){
@@ -222,7 +222,7 @@ api.create = function(req, res, next) {
saved.populate('members', nameFields, cb);
}
], function(err, populated){
if (err == 'Already in a party, try refreshing.') return res.json(400,{err:err});
if (err === shared.i18n.t('messageGroupAlreadyInParty')) return res.json(400,{err:err});
if (err) return next(err);
group = user = null;
return res.json(populated);
@@ -235,7 +235,7 @@ api.update = function(req, res, next) {
var user = res.locals.user;
if(group.leader !== user._id)
return res.json(401, {err: "Only the group leader can update the group!"});
return res.json(401, {err: shared.i18n.t('messageGroupOnlyLeaderCanUpdate')});
'name description logo logo leaderMessage leader leaderOnly'.split(' ').forEach(function(attr){
group[attr] = req.body[attr];
@@ -255,7 +255,7 @@ api.attachGroup = function(req, res, next) {
var q = (gid == 'party') ? Group.findOne({type: 'party', members: {'$in': [res.locals.user._id]}}) : Group.findById(gid);
q.exec(function(err, group){
if(err) return next(err);
if(!group) return res.json(404, {err: "Group not found"});
if(!group) return res.json(404, {err: shared.i18n.t('messageGroupNotFound')});
res.locals.group = group;
next();
});
@@ -274,7 +274,7 @@ api.getChat = function(req, res, next) {
populateQuery(gid, q);
q.exec(function(err, group){
if (err) return next(err);
if (!group && gid!=='party') return res.json(404,{err: "Group not found or you don't have access."});
if (!group && gid!=='party') return res.json(404,{err: shared.i18n.t('messageGroupNotFound')});
//Remove flagged messages if the user is not mod
if (!user.contributor.admin) {
group.chat = _.filter(group.chat, function(message) { return !message.flagCount || message.flagCount < 2; });
@@ -289,7 +289,7 @@ api.getChat = function(req, res, next) {
*/
api.postChat = function(req, res, next) {
if(!req.query.message) {
return res.json(400,{err:'You cannot send a blank message'});
return res.json(400,{err: shared.i18n.t('messageGroupChatBlankMessage')});
} else {
var user = res.locals.user
var group = res.locals.group;
@@ -337,15 +337,15 @@ api.flagChatMessage = function(req, res, next){
var group = res.locals.group;
var message = _.find(group.chat, {id: req.params.mid});
if(!message) return res.json(404, {err: "Message not found!"});
if(message.uuid == user._id) return res.json(401, {err: "Can't report your own message."});
if(!message) return res.json(404, {err: shared.i18n.t('messageGroupChatNotFound')});
if(message.uuid == user._id) return res.json(401, {err: shared.i18n.t('messageGroupChatFlagOwnMessage')});
User.findOne({_id: message.uuid}, {auth: 1}, function(err, author){
if(err) return next(err);
// Log user ids that have flagged the message
if(!message.flags) message.flags = {};
if(message.flags[user._id] && !user.contributor.admin) return res.json(401, {err: "You have already reported this message"});
if(message.flags[user._id] && !user.contributor.admin) return res.json(401, {err: shared.i18n.t('messageGroupChatFlagAlreadyReported')});
message.flags[user._id] = true;
// Log total number of flags (publicly viewable)
@@ -402,7 +402,7 @@ api.clearFlagCount = function(req, res, next){
var group = res.locals.group;
var message = _.find(group.chat, {id: req.params.mid});
if(!message) return res.json(404, {err: "Message not found!"});
if(!message) return res.json(404, {err: shared.i18n.t('messageGroupChatNotFound')});
if(user.contributor.admin){
message.flagCount = 0;
@@ -413,7 +413,7 @@ api.clearFlagCount = function(req, res, next){
return res.send(204);
});
}else{
return res.json(401, {err: "Only an admin can clear the flag count!"})
return res.json(401, {err: shared.i18n.t('messageGroupChatAdminClearFlagCount')})
}
}
@@ -433,8 +433,8 @@ api.likeChatMessage = function(req, res, next) {
var user = res.locals.user;
var group = res.locals.group;
var message = _.find(group.chat, {id: req.params.mid});
if (!message) return res.json(404, {err: "Message not found!"});
if (message.uuid == user._id) return res.json(401, {err: "Can't like your own message. Don't be that person."});
if (!message) return res.json(404, {err: shared.i18n.t('messageGroupChatNotFound')});
if (message.uuid == user._id) return res.json(401, {err: shared.i18n.t('messageGroupChatLikeOwnMessage')});
if (!message.likes) message.likes = {};
if (message.likes[user._id]) {
delete message.likes[user._id];
@@ -478,7 +478,7 @@ api.join = function(req, res, next) {
}
}
if(!isUserInvited) return res.json(401, {err: "Can't join a group you're not invited to."});
if(!isUserInvited) return res.json(401, {err: shared.i18n.t('messageGroupRequiresInvite')});
if (!_.contains(group.members, user._id)){
if (group.members.length === 0) {
@@ -529,6 +529,7 @@ api.leave = function(req, res, next) {
group.leave(user, keep, function(err){
if (err) return next(err);
user = group = keep = null;
return res.send(204);
});
};
@@ -691,7 +692,7 @@ api.invite = function(req, res, next){
} else if (req.body.emails) {
inviteByEmails(req.body.emails, group, req, res, next)
} else {
return res.json(400,{err: "Can invite only by email or uuid"});
return res.json(400, {err: "Can only invite by email or uuid"});
}
}
@@ -920,7 +921,8 @@ api.questAccept = function(req, res, next) {
owner: true,
response: 'accept',
gaLabel: 'accept',
questName: key
questName: key,
uuid: user._id,
};
analytics.track('quest',analyticsData);
group.quest.members[m] = true;
@@ -966,7 +968,8 @@ api.questAccept = function(req, res, next) {
owner: false,
response: 'accept',
gaLabel: 'accept',
questName: group.quest.key
questName: group.quest.key,
uuid: user._id,
};
analytics.track('quest',analyticsData);
group.quest.members[user._id] = true;
@@ -985,7 +988,8 @@ api.questReject = function(req, res, next) {
owner: false,
response: 'reject',
gaLabel: 'reject',
questName: group.quest.key
questName: group.quest.key,
uuid: user._id,
};
analytics.track('quest',analyticsData);
group.quest.members[user._id] = false;
+21 -3
View File
@@ -101,9 +101,27 @@ exports.iosVerify = function(req, res, next) {
if (iap.isValidated(appleRes)) {
var purchaseDataList = iap.getPurchaseData(appleRes);
if (purchaseDataList.length > 0) {
if (purchaseDataList[0].productId === 'com.habitrpg.ios.Habitica.20gems') {
//Correct receipt
payments.buyGems({user:user, paymentMethod:'IAP AppleStore'});
var correctReceipt = true;
for (var index in purchaseDataList) {
switch (purchaseDataList[index].productId) {
case 'com.habitrpg.ios.Habitica.4gems':
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 1});
break;
case 'com.habitrpg.ios.Habitica.8gems':
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 2});
break;
case 'com.habitrpg.ios.Habitica.20gems':
case 'com.habitrpg.ios.Habitica.21gems':
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 5.25});
break;
case 'com.habitrpg.ios.Habitica.42gems':
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 10.5});
break;
default:
correctReceipt = false;
}
}
if (correctReceipt) {
var resObj = {
ok: true,
data: appleRes
+2 -1
View File
@@ -137,7 +137,8 @@ exports.cancelSubscription = function(data, cb) {
}
exports.buyGems = function(data, cb) {
var amt = data.gift ? data.gift.gems.amount/4 : 5;
var amt = data.amount || 5;
amt = data.gift ? data.gift.gems.amount/4 : amt;
(data.gift ? data.gift.member : data.user).balance += amt;
data.user.purchased.txnCount++;
if(isProduction) {
+3 -3
View File
@@ -13,14 +13,14 @@ var gcm = gcmApiKey ? pushNotify.gcm({
if(gcm){
gcm.on('transmitted', function (result, message, registrationId) {
console.info("transmitted", result, message, registrationId);
//console.info("transmitted", result, message, registrationId);
});
gcm.on('transmissionError', function (error, message, registrationId) {
console.info("transmissionError", error, message, registrationId);
//console.info("transmissionError", error, message, registrationId);
});
gcm.on('updated', function (result, registrationId) {
console.info("updated", result, registrationId);
//console.info("updated", result, registrationId);
});
}
+2 -2
View File
@@ -13,9 +13,9 @@ api.unsubscribe = function(req, res, next){
if(data._id){
User.update({_id: data._id}, {
$set: {'preferences.emailNotifications.unsubscribeFromAll': true}
}, {multi: false}, function(err, nAffected){
}, {multi: false}, function(err, updateRes){
if(err) return next(err);
if(nAffected !== 1) return res.json(404, {err: 'User not found'});
if(updateRes !== 1) return res.json(404, {err: 'User not found'});
res.send('<h1>' + i18n.t('unsubscribedSuccessfully', null, req.language) + '</h1>' + i18n.t('unsubscribedTextUsers', null, req.language));
});
+26 -34
View File
@@ -164,7 +164,7 @@ api.getTasks = function(req, res, next) {
*/
api.getTask = function(req, res, next) {
var task = findTask(req,res);
if (!task) return res.json(404, {err: "No task found."});
if (!task) return res.json(404, {err: shared.i18n.t('messageTaskNotFound')});
return res.json(200, task);
};
@@ -298,10 +298,9 @@ acceptablePUTPaths = _.reduce(require('./../models/user').schema.paths, function
return m;
}, {})
//// Uncomment this if we we want to disable GP-restoring (eg, holiday events)
//_.each('stats.gp'.split(' '), function(removePath){
// delete acceptablePUTPaths[removePath];
//})
_.each('stats.class'.split(' '), function(removePath){
delete acceptablePUTPaths[removePath];
})
/**
* Update user
@@ -318,7 +317,7 @@ api.update = function(req, res, next) {
if (acceptablePUTPaths[k])
user.fns.dotSet(k, v);
else
errors.push("path `" + k + "` was not saved, as it's a protected path. See https://github.com/HabitRPG/habitrpg/blob/develop/API.md for PUT /api/v2/user.");
errors.push(shared.i18n.t('messageUserOperationProtected', { operation: k }));
return true;
});
user.save(function(err) {
@@ -365,7 +364,7 @@ api.cron = function(req, res, next) {
// api.reroll // Shared.ops
// api.reset // Shared.ops
api['delete'] = function(req, res, next) {
api.delete = function(req, res, next) {
var user = res.locals.user;
var plan = user.purchased.plan;
@@ -397,38 +396,32 @@ api['delete'] = function(req, res, next) {
/*
------------------------------------------------------------------------
Gems
Development Only Operations
------------------------------------------------------------------------
*/
if (nconf.get('NODE_ENV') === 'development') {
// api.unlock // see Shared.ops
api.addTenGems = function(req, res, next) {
var user = res.locals.user;
api.addTenGems = function(req, res, next) {
var user = res.locals.user;
user.balance += 2.5;
user.save(function(err){
if (err) return next(err);
res.send(204);
})
}
user.balance += 2.5;
/*
------------------------------------------------------------------------
Hourglass
------------------------------------------------------------------------
*/
user.save(function(err){
if (err) return next(err);
res.send(204);
});
};
// api.unlock // see Shared.ops
api.addHourglass = function(req, res, next) {
var user = res.locals.user;
api.addHourglass = function(req, res, next) {
var user = res.locals.user;
user.purchased.plan.consecutive.trinkets += 1;
user.purchased.plan.consecutive.trinkets += 1;
user.save(function(err){
if (err) return next(err);
res.send(204);
})
user.save(function(err){
if (err) return next(err);
res.send(204);
});
};
}
/*
@@ -607,6 +600,7 @@ api.batchUpdate = function(req, res, next) {
return cb(code+": "+ (data.message ? data.message : data.err ? data.err : JSON.stringify(data)));
return cb();
};
if(!api[_req.op]) { return cb(shared.i18n.t('messageUserOperationNotFound', { operation: _req.op })); }
api[_req.op](_req, res, cb);
});
})
@@ -635,9 +629,7 @@ api.batchUpdate = function(req, res, next) {
// Fetch full user object
} else if (response.wasModified){
// Preen 3-day past-completed To-Dos from Angular & mobile app
response.todos = _.where(response.todos, function(t) {
return !t.completed || (t.challenge && t.challenge.id) || moment(t.dateCompleted).isAfter(moment().subtract({days:3}));
});
response.todos = shared.preenTodos(response.todos);
res.json(200, response);
// return only the version number
+1 -1
View File
@@ -155,4 +155,4 @@ module.exports.enTranslations = function(){ // stringName and vars are the allow
var args = Array.prototype.slice.call(arguments, 0);
args.push(language.code);
return shared.i18n.t.apply(null, args);
};
};
+8 -3
View File
@@ -12,8 +12,9 @@ module.exports = function(server,mongoose) {
useAvg = false, // use average over 3 minutes, or simply the last minute's report
url = 'https://api.newrelic.com/v2/applications/'+nconf.get('NEW_RELIC_APPLICATION_ID')+'/metrics/data.json?names[]=Apdex&values[]=score';
setInterval(function(){
// TODO, DISABLED UNTIL NEW RELIC IS ADDED AGAIN
// see https://docs.newrelic.com/docs/apm/apis/api-v2-examples/average-response-time-examples-api-v2, https://rpm.newrelic.com/api/explore/applications/data
request({
/*request({
url: useAvg ? url+'&from='+moment().subtract({minutes:mins}).utc().format()+'&to='+moment().utc().format()+'&summarize=true' : url,
headers: {'X-Api-Key': nconf.get('NEW_RELIC_API_KEY')}
}, function(err, response, body){
@@ -22,8 +23,12 @@ module.exports = function(server,mongoose) {
apdexBad = score < .75 || score == 1,
memory = os.freemem() / os.totalmem(),
memoryHigh = memory < 0.1;
if (/*apdexBad || */memoryHigh) throw '[Memory Leak] Apdex='+score+' Memory='+parseFloat(memory).toFixed(3)+' Time='+moment().format();
});
if (apdexBad || memoryHigh) throw '[Memory Leak] Apdex='+score+' Memory='+parseFloat(memory).toFixed(3)+' Time='+moment().format();
});*/
var memory = os.freemem() / os.totalmem(),
memoryHigh = memory < 0.1;
if (memoryHigh) throw '[Memory Leak] Memory='+parseFloat(memory).toFixed(3)+' Time='+moment().format();
}, mins*60*1000);
}
+27 -3
View File
@@ -66,7 +66,8 @@ var UserSchema = new Schema({
email: String,
hashed_password: String,
salt: String,
username: String
username: String,
lowerCaseUsername: String // Store a lowercase version of username to check for duplicates
},
timestamps: {
created: {type: Date,'default': Date.now},
@@ -138,6 +139,29 @@ var UserSchema = new Schema({
hall: {type: Number, 'default': -1},
equipment: {type: Number, 'default': -1}
},
tutorial: {
common: {
habits: {type: Boolean, 'default': false},
dailies: {type: Boolean, 'default': false},
todos: {type: Boolean, 'default': false},
rewards: {type: Boolean, 'default': false},
party: {type: Boolean, 'default': false},
pets: {type: Boolean, 'default': false},
gems: {type: Boolean, 'default': false},
skills: {type: Boolean, 'default': false},
classes: {type: Boolean, 'default': false},
tavern: {type: Boolean, 'default': false},
equipment: {type: Boolean, 'default': false},
items: {type: Boolean, 'default': false},
},
ios: {
addTask: {type: Boolean, 'default': false},
editTask: {type: Boolean, 'default': false},
deleteTask: {type: Boolean, 'default': false},
filterTask: {type: Boolean, 'default': false},
groupPets: {type: Boolean, 'default': false},
}
},
dropsEnabled: {type: Boolean, 'default': false},
itemsEnabled: {type: Boolean, 'default': false},
newStuff: {type: Boolean, 'default': false},
@@ -171,7 +195,6 @@ var UserSchema = new Schema({
todos: Array //[{data: Date, value: Number}] // big peformance issues if these are defined
},
// FIXME remove?
invitations: {
guilds: {type: Array, 'default': []},
party: Schema.Types.Mixed
@@ -324,6 +347,7 @@ var UserSchema = new Schema({
language: String,
automaticAllocation: Boolean,
allocationMode: {type:String, enum: ['flat','classbased','taskbased'], 'default': 'flat'},
autoEquip: {type: Boolean, 'default': true},
costume: Boolean,
dateFormat: {type: String, enum:['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'], 'default': 'MM/dd/yyyy'},
sleep: {type: Boolean, 'default': false},
@@ -579,7 +603,7 @@ UserSchema.methods.unlink = function(options, cb) {
module.exports.schema = UserSchema;
module.exports.model = mongoose.model("User", UserSchema);
// Initially export an empty object so external requires will get
// Initially export an empty object so external requires will get
// the right object by reference when it's defined later
// Otherwise it would remain undefined if requested before the query executes
module.exports.mods = [];
+1 -1
View File
@@ -155,7 +155,7 @@ router.post('/user/tasks/:id/:direction', auth.auth, i18n.getUserLanguage, cron,
// Tasks
router.get('/user/tasks', auth.auth, i18n.getUserLanguage, cron, api.getTasks);
router.get('/user/task/:id', auth.auth, i18n.getUserLanguage, cron, api.getTask);
router["delete"]('/user/task/:id', auth.auth, i18n.getUserLanguage, cron, api.deleteTask);
router.delete('/user/task/:id', auth.auth, i18n.getUserLanguage, cron, api.deleteTask);
router.post('/user/task', auth.auth, i18n.getUserLanguage, cron, api.addTask);
// User
+21 -3
View File
@@ -265,7 +265,7 @@ module.exports = (swagger, v2) ->
method: 'DELETE'
description: "Delete a user object entirely, USE WITH CAUTION!"
middleware: [auth.auth, i18n.getUserLanguage]
action: user["delete"]
action: user.delete
"/user/revive":
spec:
@@ -347,8 +347,19 @@ module.exports = (swagger, v2) ->
action: user.batchUpdate
# Tags
"/user/tags":
"/user/tags/{id}:GET":
spec:
path: '/user/tags/{id}'
method: 'GET'
description: "Get a tag"
parameters: [
path 'id','The id of the tag to get','string'
]
action: user.getTag
"/user/tags:POST":
spec:
path: "/user/tags"
method: 'POST'
description: 'Create a new tag'
parameters: [
@@ -356,6 +367,13 @@ module.exports = (swagger, v2) ->
]
action: user.addTag
"/user/tags:GET":
spec:
path: "/user/tags"
method: 'GET'
description: 'List all of a user\'s tags'
action: user.getTags
"/user/tags/sort":
spec:
method: 'POST'
@@ -756,7 +774,7 @@ module.exports = (swagger, v2) ->
description: "Delete a challenge"
parameters: [path('cid','Challenge id','string')]
middleware: [auth.auth, i18n.getUserLanguage]
action: challenges["delete"]
action: challenges.delete
"/challenges/{cid}/close":
spec:
+3 -2
View File
@@ -7,6 +7,7 @@ utils.setupConfig();
var logging = require('./logging');
var isProd = nconf.get('NODE_ENV') === 'production';
var isDev = nconf.get('NODE_ENV') === 'development';
var DISABLE_LOGGING = nconf.get('DISABLE_REQUEST_LOGGING');
var cores = +nconf.get("WEB_CONCURRENCY") || 0;
if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
@@ -91,7 +92,7 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
app.set("port", nconf.get('PORT'));
require('./middlewares/apiThrottle')(app);
app.use(require('./middlewares/domain')(server,mongoose));
if (!isProd) app.use(express.logger("dev"));
if (!isProd && !DISABLE_LOGGING) app.use(express.logger("dev"));
app.use(express.compress());
app.set("views", __dirname + "/../views");
app.set("view engine", "jade");
@@ -144,4 +145,4 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
});
module.exports = server;
}
}
+12 -4
View File
@@ -159,6 +159,10 @@ module.exports.makeSalt = function() {
return crypto.randomBytes(Math.ceil(len / 2)).toString('hex').substring(0, len);
}
// Prepare to export analytics object
// Export emoty methods until the right ones are ready
module.exports.analytics = { track: function() { }, trackPurchase: function() { } };
/**
* Load nconf and define default configuration values if config.json or ENV vars are not found
*/
@@ -171,7 +175,7 @@ module.exports.setupConfig = function(){
if (nconf.get('NODE_ENV') === "development")
Error.stackTraceLimit = Infinity;
//if (nconf.get('NODE_ENV') === 'production')
// require('newrelic');
//require('newrelic');
isProd = nconf.get('NODE_ENV') === 'production';
baseUrl = nconf.get('BASE_URL');
@@ -182,9 +186,13 @@ module.exports.setupConfig = function(){
googleAnalytics: nconf.get('GA_ID')
}
module.exports.analytics = analytics
? analytics(analyticsTokens)
: { track: function() { }, trackPurchase: function() { } };
if(analytics){
analytics = analytics(analyticsTokens);
// Use the right analytics methods, don't substitute the entire object
// or all the require() across the code will keep the empty methods
module.exports.analytics.track = analytics.track;
module.exports.analytics.trackPurchase = analytics.trackPurchase;
}
};
var algorithm = 'aes-256-ctr';