Reorganize files under /src, separate express app in two apps, one for api v1 and v2 and the other \
for the upcoming api v3 Reorganize files under /src, separate express app in two apps, one for api v1 and v2 and the other for the upcoming api v3 move api v2 routes in a separate folder, rename apiv1 file for better readability, remove auth routes for api v1 move api-v2 controllers in subdirectory move unorganized files to /libs fix gulp requires and separate server in old (api v1 and v2) and new (api v3) app fix require paths fix require paths fix require paths put api v1 back Reorganize files under /src and separate express app in one for api v1 and v2 and the other for v3
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
var _ = require('lodash');
|
||||
var validator = require('validator');
|
||||
var passport = require('passport');
|
||||
var shared = require('../../../../common');
|
||||
var async = require('async');
|
||||
var utils = require('../../libs/utils');
|
||||
var nconf = require('nconf');
|
||||
var request = require('request');
|
||||
var FirebaseTokenGenerator = require('firebase-token-generator');
|
||||
var User = require('../../models/user').model;
|
||||
var EmailUnsubscription = require('../../models/emailUnsubscription').model;
|
||||
var analytics = utils.analytics;
|
||||
var i18n = require('./../../libs/i18n');
|
||||
|
||||
var isProd = nconf.get('NODE_ENV') === 'production';
|
||||
|
||||
var api = module.exports;
|
||||
|
||||
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'
|
||||
};
|
||||
}
|
||||
|
||||
api.auth = function(req, res, next) {
|
||||
var uid = req.headers['x-api-user'];
|
||||
var token = req.headers['x-api-key'];
|
||||
if (!(uid && token)) return res.json(401, NO_TOKEN_OR_UID);
|
||||
User.findOne({_id: uid, apiToken: token}, function(err, user) {
|
||||
if (err) return next(err);
|
||||
if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND);
|
||||
if (user.auth.blocked) return res.json(401, accountSuspended(user._id));
|
||||
|
||||
res.locals.wasModified = req.query._v ? +user._v !== +req.query._v : true;
|
||||
res.locals.user = user;
|
||||
req.session.userId = user._id;
|
||||
return next();
|
||||
});
|
||||
};
|
||||
|
||||
api.authWithSession = function(req, res, next) { //[todo] there is probably a more elegant way of doing this...
|
||||
if (!(req.session && req.session.userId))
|
||||
return res.json(401, NO_SESSION_FOUND);
|
||||
User.findOne({_id: req.session.userId}, function(err, user) {
|
||||
if (err) return next(err);
|
||||
if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND);
|
||||
res.locals.user = user;
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
api.authWithUrl = function(req, res, next) {
|
||||
User.findOne({_id:req.query._id, apiToken:req.query.apiToken}, function(err,user){
|
||||
if (err) return next(err);
|
||||
if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND);
|
||||
res.locals.user = user;
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
api.registerUser = function(req, res, next) {
|
||||
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 (!(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: shared.i18n.t('messageAuthPasswordMustMatch')});
|
||||
if (!validator.isEmail(email))
|
||||
return cb({code:401, err: ":email invalid"});
|
||||
cb();
|
||||
},
|
||||
findReg: function(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 (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: 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)
|
||||
},
|
||||
timestamps: {created: +new Date(), loggedIn: +new Date()}
|
||||
}
|
||||
};
|
||||
// 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);
|
||||
|
||||
var analyticsData = {
|
||||
category: 'acquisition',
|
||||
type: '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');
|
||||
});
|
||||
cb.apply(cb, arguments);
|
||||
});
|
||||
}
|
||||
}]
|
||||
}, function(err, data) {
|
||||
if (err) return err.code ? res.json(err.code, err) : next(err);
|
||||
res.json(200, data.register[0]);
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
Register new user with uname / password
|
||||
*/
|
||||
|
||||
|
||||
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.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\"."});
|
||||
if (user.auth.blocked) return res.json(401, accountSuspended(user._id));
|
||||
// We needed the whole user object first so we can get his salt to encrypt password comparison
|
||||
User.findOne(
|
||||
{$and: [login, {'auth.local.hashed_password': utils.encryptPassword(password, user.auth.local.salt)}]}
|
||||
, {_id:1, apiToken: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\"."});
|
||||
res.json({id: user._id,token: user.apiToken});
|
||||
password = null;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
POST /user/auth/social
|
||||
*/
|
||||
api.loginSocial = function(req, res, next) {
|
||||
var access_token = req.body.authResponse.access_token,
|
||||
network = req.body.network;
|
||||
if (network!=='facebook')
|
||||
return res.json(401, {err:"Only Facebook supported currently."});
|
||||
async.auto({
|
||||
profile: function (cb) {
|
||||
passport._strategies[network].userProfile(access_token, cb);
|
||||
},
|
||||
user: ['profile', function (cb, results) {
|
||||
var q = {};
|
||||
q['auth.' + network + '.id'] = results.profile.id;
|
||||
User.findOne(q, {_id: 1, apiToken: 1, auth: 1}, cb);
|
||||
}],
|
||||
register: ['profile', 'user', function (cb, results) {
|
||||
if (results.user) return cb(null, results.user);
|
||||
// Create new user
|
||||
var prof = results.profile;
|
||||
var user = {
|
||||
preferences: {
|
||||
language: req.language // User language detected from browser, not saved
|
||||
},
|
||||
auth: {
|
||||
timestamps: {created: +new Date(), loggedIn: +new Date()}
|
||||
}
|
||||
};
|
||||
user.auth[network] = prof;
|
||||
user = new User(user);
|
||||
user.save(function(err, savedUser){
|
||||
// Clean previous email preferences
|
||||
if(savedUser.auth.facebook.emails && savedUser.auth.facebook.emails[0] && savedUser.auth.facebook.emails[0].value){
|
||||
EmailUnsubscription.remove({email: savedUser.auth.facebook.emails[0].value}, function(){
|
||||
utils.txnEmail(savedUser, 'welcome');
|
||||
});
|
||||
}
|
||||
cb.apply(cb, arguments);
|
||||
});
|
||||
|
||||
var analyticsData = {
|
||||
category: 'acquisition',
|
||||
type: network,
|
||||
gaLabel: network,
|
||||
uuid: user._id,
|
||||
};
|
||||
analytics.track('register', analyticsData)
|
||||
}]
|
||||
}, function(err, results){
|
||||
if (err) return res.json(401, {err: err.toString ? err.toString() : err});
|
||||
var acct = results.register[0] ? results.register[0] : results.register;
|
||||
if (acct.auth.blocked) return res.json(401, accountSuspended(acct._id));
|
||||
return res.json(200, {id:acct._id, token:acct.apiToken});
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* DELETE /user/auth/social
|
||||
*/
|
||||
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 && 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);
|
||||
|
||||
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;
|
||||
user.auth.local.hashed_password = hashed_password;
|
||||
utils.sendEmail({
|
||||
from: "Habitica <admin@habitica.com>",
|
||||
to: email,
|
||||
subject: "Password Reset for Habitica",
|
||||
text: "Password for " + user.auth.local.username + " has been reset to " + newPassword + " Important! Both username and password are case-sensitive -- you must enter both exactly as shown here. We recommend copying and pasting both instead of typing them. Log in at " + nconf.get('BASE_URL') + ". After you've logged in, head to " + nconf.get('BASE_URL') + "/#/options/settings/settings and change your password.",
|
||||
html: "Password for <strong>" + user.auth.local.username + "</strong> has been reset to <strong>" + newPassword + "</strong><br /><br />Important! Both username and password are case-sensitive -- you must enter both exactly as shown here. We recommend copying and pasting both instead of typing them.<br /><br />Log in at " + nconf.get('BASE_URL') + ". After you've logged in, head to " + nconf.get('BASE_URL') + "/#/options/settings/settings and change your password."
|
||||
});
|
||||
user.save(function(err){
|
||||
if(err) return next(err);
|
||||
res.send('New password sent to '+ email);
|
||||
email = salt = newPassword = hashed_password = null;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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;
|
||||
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.lowerCaseUsername': lowerCaseUsername}, {auth:1}, cb);
|
||||
},
|
||||
function(found, cb){
|
||||
if (found) return cb({code:401, err: "Username already taken"});
|
||||
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);
|
||||
res.send(200);
|
||||
})
|
||||
}
|
||||
|
||||
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': email}, {auth:1}, cb);
|
||||
},
|
||||
function(found, cb){
|
||||
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 = 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) {
|
||||
var user = res.locals.user,
|
||||
oldPassword = req.body.oldPassword,
|
||||
newPassword = req.body.newPassword,
|
||||
confirmNewPassword = req.body.confirmNewPassword;
|
||||
|
||||
if (newPassword != confirmNewPassword)
|
||||
return res.json(401, {err: "Password & Confirm don't match"});
|
||||
|
||||
var salt = user.auth.local.salt,
|
||||
hashed_old_password = utils.encryptPassword(oldPassword, salt),
|
||||
hashed_new_password = utils.encryptPassword(newPassword, salt);
|
||||
|
||||
if (hashed_old_password !== user.auth.local.hashed_password)
|
||||
return res.json(401, {err:"Old password doesn't match"});
|
||||
|
||||
user.auth.local.hashed_password = hashed_new_password;
|
||||
user.save(function(err, saved){
|
||||
if (err) next(err);
|
||||
res.send(200);
|
||||
})
|
||||
};
|
||||
|
||||
var firebaseTokenGeneratorInstance = new FirebaseTokenGenerator(nconf.get('FIREBASE:SECRET'));
|
||||
api.getFirebaseToken = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
// Expires 24 hours after now (60*60*24*1000) (in milliseconds)
|
||||
var expires = new Date();
|
||||
expires.setTime(expires.getTime() + 86400000);
|
||||
|
||||
var token = firebaseTokenGeneratorInstance
|
||||
.createToken({
|
||||
uid: user._id,
|
||||
isHabiticaUser: true
|
||||
}, {
|
||||
expires: expires
|
||||
});
|
||||
|
||||
res.json(200, {
|
||||
token: token,
|
||||
expires: expires
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
Registers a new user. Only accepting username/password registrations, no Facebook
|
||||
*/
|
||||
|
||||
api.setupPassport = function(router) {
|
||||
|
||||
router.get('/logout', i18n.getUserLanguage, function(req, res) {
|
||||
req.logout();
|
||||
delete req.session.userId;
|
||||
res.redirect('/');
|
||||
})
|
||||
|
||||
};
|
||||
@@ -0,0 +1,445 @@
|
||||
// @see ../routes for routing
|
||||
|
||||
var _ = require('lodash');
|
||||
var nconf = require('nconf');
|
||||
var async = require('async');
|
||||
var shared = require('../../../../common');
|
||||
var User = require('./../../models/user').model;
|
||||
var Group = require('./../../models/group').model;
|
||||
var Challenge = require('./../../models/challenge').model;
|
||||
var logging = require('./../../libs/logging');
|
||||
var csv = require('express-csv');
|
||||
var utils = require('../../libs/utils');
|
||||
var api = module.exports;
|
||||
var pushNotify = require('./../pushNotifications');
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Challenges
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
api.list = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
// Get all available groups I belong to
|
||||
Group.find({members: {$in: [user._id]}}).select('_id').exec(cb);
|
||||
},
|
||||
function(gids, cb){
|
||||
// and their challenges
|
||||
Challenge.find({
|
||||
$or:[
|
||||
{leader: user._id},
|
||||
{members:{$in:[user._id]}}, // all challenges I belong to (is this necessary? thought is a left a group, but not its challenge)
|
||||
{group:{$in:gids}}, // all challenges in my groups
|
||||
{group: 'habitrpg'} // public group
|
||||
],
|
||||
_id:{$ne:'95533e05-1ff9-4e46-970b-d77219f199e9'} // remove the Spread the Word Challenge for now, will revisit when we fix the closing-challenge bug
|
||||
})
|
||||
.select('name leader description group memberCount prize official')
|
||||
.select({members:{$elemMatch:{$in:[user._id]}}})
|
||||
.sort('-official -timestamp')
|
||||
.populate('group', '_id name type')
|
||||
.populate('leader', 'profile.name')
|
||||
.exec(cb);
|
||||
}
|
||||
], function(err, challenges){
|
||||
if (err) return next(err);
|
||||
_.each(challenges, function(c){
|
||||
c._isMember = c.members.length > 0;
|
||||
})
|
||||
res.json(challenges);
|
||||
user = null;
|
||||
});
|
||||
}
|
||||
|
||||
// GET
|
||||
api.get = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
// TODO use mapReduce() or aggregate() here to
|
||||
// 1) Find the sum of users.tasks.values within the challnege (eg, {'profile.name':'tyler', 'sum': 100})
|
||||
// 2) Sort by the sum
|
||||
// 3) Limit 30 (only show the 30 users currently in the lead)
|
||||
Challenge.findById(req.params.cid)
|
||||
.populate('members', 'profile.name _id')
|
||||
.populate('group', '_id name type')
|
||||
.populate('leader', 'profile.name')
|
||||
.exec(function(err, challenge){
|
||||
if(err) return next(err);
|
||||
if (!challenge) return res.json(404, {err: 'Challenge ' + req.params.cid + ' not found'});
|
||||
challenge._isMember = !!(_.find(challenge.members, function(member) {
|
||||
return member._id === user._id;
|
||||
}));
|
||||
res.json(challenge);
|
||||
});
|
||||
}
|
||||
|
||||
api.csv = function(req, res, next) {
|
||||
var cid = req.params.cid;
|
||||
var challenge;
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
Challenge.findById(cid,cb)
|
||||
},
|
||||
function(_challenge,cb) {
|
||||
challenge = _challenge;
|
||||
if (!challenge) return cb('Challenge ' + cid + ' not found');
|
||||
User.aggregate([
|
||||
{$match:{'_id':{ '$in': challenge.members}}}, //yes, we want members
|
||||
{$project:{'profile.name':1,tasks:{$setUnion:["$habits","$dailys","$todos","$rewards"]}}},
|
||||
{$unwind:"$tasks"},
|
||||
{$match:{"tasks.challenge.id":cid}},
|
||||
{$sort:{'tasks.type':1,'tasks.id':1}},
|
||||
{$group:{_id:"$_id", "tasks":{$push:"$tasks"},"name":{$first:"$profile.name"}}}
|
||||
], cb);
|
||||
}
|
||||
],function(err,users){
|
||||
if(err) return next(err);
|
||||
var output = ['UUID','name'];
|
||||
_.each(challenge.tasks,function(t){
|
||||
//output.push(t.type+':'+t.text);
|
||||
//not the right order yet
|
||||
output.push('Task');
|
||||
output.push('Value');
|
||||
output.push('Notes');
|
||||
})
|
||||
output = [output];
|
||||
_.each(users, function(u){
|
||||
var uData = [u._id,u.name];
|
||||
_.each(u.tasks,function(t){
|
||||
uData = uData.concat([t.type+':'+t.text, t.value, t.notes]);
|
||||
})
|
||||
output.push(uData);
|
||||
});
|
||||
res.header('Content-disposition', 'attachment; filename='+cid+'.csv');
|
||||
res.csv(output);
|
||||
challenge = cid = null;
|
||||
})
|
||||
}
|
||||
|
||||
api.getMember = function(req, res, next) {
|
||||
var cid = req.params.cid;
|
||||
var uid = req.params.uid;
|
||||
|
||||
// We need to start using the aggregation framework instead of in-app filtering, see http://docs.mongodb.org/manual/aggregation/
|
||||
// See code at 32c0e75 for unwind/group example
|
||||
|
||||
//http://stackoverflow.com/questions/24027213/how-to-match-multiple-array-elements-without-using-unwind
|
||||
var proj = {'profile.name':'$profile.name'};
|
||||
_.each(['habits','dailys','todos','rewards'], function(type){
|
||||
proj[type] = {
|
||||
$setDifference: [{
|
||||
$map: {
|
||||
input: '$'+type,
|
||||
as: "el",
|
||||
in: {
|
||||
$cond: [{$eq: ["$$el.challenge.id", cid]}, '$$el', false]
|
||||
}
|
||||
}
|
||||
}, [false]]
|
||||
}
|
||||
});
|
||||
User.aggregate()
|
||||
.match({_id: uid})
|
||||
.project(proj)
|
||||
.exec(function(err, member){
|
||||
if (err) return next(err);
|
||||
if (!member) return res.json(404, {err: 'Member '+uid+' for challenge '+cid+' not found'});
|
||||
res.json(member[0]);
|
||||
uid = cid = null;
|
||||
});
|
||||
}
|
||||
|
||||
// CREATE
|
||||
api.create = function(req, res, next){
|
||||
var user = res.locals.user;
|
||||
|
||||
async.auto({
|
||||
get_group: function(cb){
|
||||
var q = {_id:req.body.group};
|
||||
if (req.body.group!='habitrpg') q.members = {$in:[user._id]}; // make sure they're a member of the group
|
||||
Group.findOne(q, cb);
|
||||
},
|
||||
save_chal: ['get_group', function(cb, results){
|
||||
var group = results.get_group,
|
||||
prize = +req.body.prize;
|
||||
if (!group)
|
||||
return cb({code:404, err:"Group." + req.body.group + " not found"});
|
||||
if (group.leaderOnly && group.leaderOnly.challenges && group.leader !== user._id)
|
||||
return cb({code:401, err: "Only the group leader can create challenges"});
|
||||
// If they're adding a prize, do some validation
|
||||
if (prize < 0)
|
||||
return cb({code:401, err: 'Challenge prize must be >= 0'});
|
||||
if (req.body.group=='habitrpg' && prize < 1)
|
||||
return cb({code:401, err: 'Prize must be at least 1 Gem for public challenges.'});
|
||||
if (prize > 0) {
|
||||
var groupBalance = ((group.balance && group.leader==user._id) ? group.balance : 0);
|
||||
var prizeCost = prize/4; // I really should have stored user.balance as gems rather than dollars... stupid...
|
||||
if (prizeCost > user.balance + groupBalance)
|
||||
return cb("You can't afford this prize. Purchase more gems or lower the prize amount.")
|
||||
|
||||
if (groupBalance >= prizeCost) {
|
||||
// Group pays for all of prize
|
||||
group.balance -= prizeCost;
|
||||
} else if (groupBalance > 0) {
|
||||
// User pays remainder of prize cost after group
|
||||
var remainder = prizeCost - group.balance;
|
||||
group.balance = 0;
|
||||
user.balance -= remainder;
|
||||
} else {
|
||||
// User pays for all of prize
|
||||
user.balance -= prizeCost;
|
||||
}
|
||||
}
|
||||
req.body.leader = user._id;
|
||||
req.body.official = user.contributor.admin && req.body.official;
|
||||
var chal = new Challenge(req.body); // FIXME sanitize
|
||||
chal.members.push(user._id);
|
||||
chal.save(cb);
|
||||
}],
|
||||
save_group: ['save_chal', function(cb, results){
|
||||
results.get_group.challenges.push(results.save_chal[0]._id);
|
||||
results.get_group.save(cb);
|
||||
}],
|
||||
sync_user: ['save_group', function(cb, results){
|
||||
// Auto-join creator to challenge (see members.push above)
|
||||
results.save_chal[0].syncToUser(user, cb);
|
||||
}]
|
||||
}, function(err, results){
|
||||
if (err) return err.code? res.json(err.code, err) : next(err);
|
||||
return res.json(results.save_chal[0]);
|
||||
user = null;
|
||||
})
|
||||
}
|
||||
|
||||
// UPDATE
|
||||
api.update = function(req, res, next){
|
||||
var cid = req.params.cid;
|
||||
var user = res.locals.user;
|
||||
var before;
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
// We first need the original challenge data, since we're going to compare against new & decide to sync users
|
||||
Challenge.findById(cid, cb);
|
||||
},
|
||||
function(_before, cb) {
|
||||
if (!_before) return cb('Challenge ' + cid + ' not found');
|
||||
if (_before.leader != user._id && !user.contributor.admin) return cb(shared.i18n.t('noPermissionEditChallenge', req.language));
|
||||
// Update the challenge, since syncing will need the updated challenge. But store `before` we're going to do some
|
||||
// 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}, {new: true}, cb);
|
||||
},
|
||||
function(saved, cb) {
|
||||
|
||||
// Compare whether any changes have been made to tasks. If so, we'll want to sync those changes to subscribers
|
||||
if (before.isOutdated(req.body)) {
|
||||
User.find({_id: {$in: saved.members}}, function(err, users){
|
||||
logging.info('Challenge updated, sync to subscribers');
|
||||
if (err) throw err;
|
||||
_.each(users, function(user){
|
||||
saved.syncToUser(user);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// after saving, we're done as far as the client's concerned. We kick off syncing (heavy task) in the background
|
||||
cb(null, saved);
|
||||
}
|
||||
], function(err, saved){
|
||||
if(err) next(err);
|
||||
res.json(saved);
|
||||
cid = user = before = null;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by either delete() or selectWinner(). Will delete the challenge and set the "broken" property on all users' subscribed tasks
|
||||
* @param {cid} the challenge id
|
||||
* @param {broken} the object representing the broken status of the challenge. Eg:
|
||||
* {broken: 'CHALLENGE_DELETED', id: CHALLENGE_ID}
|
||||
* {broken: 'CHALLENGE_CLOSED', id: CHALLENGE_ID, winner: USER_NAME}
|
||||
*/
|
||||
function closeChal(cid, broken, cb) {
|
||||
var removed;
|
||||
async.waterfall([
|
||||
function(cb2){
|
||||
Challenge.findOneAndRemove({_id:cid}, cb2)
|
||||
},
|
||||
function(_removed, cb2) {
|
||||
removed = _removed;
|
||||
var pull = {'$pull':{}}; pull['$pull'][_removed._id] = 1;
|
||||
Group.findByIdAndUpdate(_removed.group, {new: true}, pull);
|
||||
User.find({_id:{$in: removed.members}}, cb2);
|
||||
},
|
||||
function(users, cb2) {
|
||||
var parallel = [];
|
||||
_.each(users, function(user){
|
||||
var tag = _.find(user.tags, {id:cid});
|
||||
if (tag) tag.challenge = undefined;
|
||||
_.each(user.tasks, function(task){
|
||||
if (task.challenge && task.challenge.id == removed._id) {
|
||||
_.merge(task.challenge, broken);
|
||||
}
|
||||
})
|
||||
parallel.push(function(cb3){
|
||||
user.save(cb3);
|
||||
})
|
||||
})
|
||||
async.parallel(parallel, cb2);
|
||||
removed = null;
|
||||
}
|
||||
], cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete & close
|
||||
*/
|
||||
api.delete = function(req, res, next){
|
||||
var user = res.locals.user;
|
||||
var cid = req.params.cid;
|
||||
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
Challenge.findById(cid, cb);
|
||||
},
|
||||
function(chal, cb){
|
||||
if (!chal) return cb('Challenge ' + cid + ' not found');
|
||||
if (chal.leader != user._id && !user.contributor.admin) return cb(shared.i18n.t('noPermissionDeleteChallenge', req.language));
|
||||
if (chal.group != 'habitrpg') user.balance += chal.prize/4; // Refund gems to user if a non-tavern challenge
|
||||
user.save(cb);
|
||||
},
|
||||
function(save, num, cb){
|
||||
closeChal(req.params.cid, {broken: 'CHALLENGE_DELETED'}, cb);
|
||||
}
|
||||
], function(err){
|
||||
if (err) return next(err);
|
||||
res.send(200);
|
||||
user = cid = null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Select Winner & Close
|
||||
*/
|
||||
api.selectWinner = function(req, res, next) {
|
||||
if (!req.query.uid) return res.json(401, {err: 'Must select a winner'});
|
||||
var user = res.locals.user;
|
||||
var cid = req.params.cid;
|
||||
var chal;
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
Challenge.findById(cid, cb);
|
||||
},
|
||||
function(_chal, cb){
|
||||
chal = _chal;
|
||||
if (!chal) return cb('Challenge ' + cid + ' not found');
|
||||
if (chal.leader != user._id && !user.contributor.admin) return cb(shared.i18n.t('noPermissionCloseChallenge', req.language));
|
||||
User.findById(req.query.uid, cb)
|
||||
},
|
||||
function(winner, cb){
|
||||
if (!winner) return cb('Winner ' + req.query.uid + ' not found.');
|
||||
_.defaults(winner.achievements, {challenges: []});
|
||||
winner.achievements.challenges.push(chal.name);
|
||||
winner.balance += chal.prize/4;
|
||||
winner.save(cb);
|
||||
},
|
||||
function(saved, num, cb) {
|
||||
if(saved.preferences.emailNotifications.wonChallenge !== false){
|
||||
utils.txnEmail(saved, 'won-challenge', [
|
||||
{name: 'CHALLENGE_NAME', content: chal.name}
|
||||
]);
|
||||
}
|
||||
|
||||
pushNotify.sendNotify(saved, shared.i18n.t('wonChallenge'), chal.name);
|
||||
|
||||
closeChal(cid, {broken: 'CHALLENGE_CLOSED', winner: saved.profile.name}, cb);
|
||||
}
|
||||
], function(err){
|
||||
if (err) return next(err);
|
||||
res.send(200);
|
||||
user = cid = chal = null;
|
||||
})
|
||||
}
|
||||
|
||||
api.join = function(req, res, next){
|
||||
var user = res.locals.user;
|
||||
var cid = req.params.cid;
|
||||
|
||||
async.waterfall([
|
||||
function(cb) {
|
||||
Challenge.findByIdAndUpdate(cid, {$addToSet:{members:user._id}}, {new: true}, cb);
|
||||
},
|
||||
function(chal, cb) {
|
||||
|
||||
// Trigger updating challenge member count in the background. We can't do it above because we don't have
|
||||
// _.size(challenge.members). We can't do it in pre(save) because we're calling findByIdAndUpdate above.
|
||||
Challenge.update({_id:cid}, {$set:{memberCount:_.size(chal.members)}}).exec();
|
||||
|
||||
if (!~user.challenges.indexOf(cid))
|
||||
user.challenges.unshift(cid);
|
||||
// Add all challenge's tasks to user's tasks
|
||||
chal.syncToUser(user, function(err){
|
||||
if (err) return cb(err);
|
||||
cb(null, chal); // we want the saved challenge in the return results, due to ng-resource
|
||||
});
|
||||
}
|
||||
], function(err, chal){
|
||||
if(err) return next(err);
|
||||
chal._isMember = true;
|
||||
res.json(chal);
|
||||
user = cid = null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
api.leave = function(req, res, next){
|
||||
var user = res.locals.user;
|
||||
var cid = req.params.cid;
|
||||
// whether or not to keep challenge's tasks. strictly default to true if "keep-all" isn't provided
|
||||
var keep = (/^remove-all/i).test(req.query.keep) ? 'remove-all' : 'keep-all';
|
||||
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
Challenge.findByIdAndUpdate(cid, {$pull:{members:user._id}}, {new: true}, cb);
|
||||
},
|
||||
function(chal, cb){
|
||||
|
||||
// Trigger updating challenge member count in the background. We can't do it above because we don't have
|
||||
// _.size(challenge.members). We can't do it in pre(save) because we're calling findByIdAndUpdate above.
|
||||
if (chal)
|
||||
Challenge.update({_id:cid}, {$set:{memberCount:_.size(chal.members)}}).exec();
|
||||
|
||||
var i = user.challenges.indexOf(cid)
|
||||
if (~i) user.challenges.splice(i,1);
|
||||
user.unlink({cid:cid, keep:keep}, function(err){
|
||||
if (err) return cb(err);
|
||||
cb(null, chal);
|
||||
})
|
||||
}
|
||||
], function(err, chal){
|
||||
if(err) return next(err);
|
||||
if (chal) chal._isMember = false;
|
||||
res.json(chal);
|
||||
user = cid = keep = null;
|
||||
});
|
||||
}
|
||||
|
||||
api.unlink = function(req, res, next) {
|
||||
// they're scoring the task - commented out, we probably don't need it due to route ordering in api.js
|
||||
//var urlParts = req.originalUrl.split('/');
|
||||
//if (_.contains(['up','down'], urlParts[urlParts.length -1])) return next();
|
||||
|
||||
var user = res.locals.user;
|
||||
var tid = req.params.id;
|
||||
var cid = user.tasks[tid].challenge.id;
|
||||
if (!req.query.keep)
|
||||
return res.json(400, {err: 'Provide unlink method as ?keep=keep-all (keep, keep-all, remove, remove-all)'});
|
||||
user.unlink({cid:cid, keep:req.query.keep, tid:tid}, function(err, saved){
|
||||
if (err) return next(err);
|
||||
res.send(200);
|
||||
user = tid = cid = null;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
var _ = require('lodash');
|
||||
var Coupon = require('./../../models/coupon').model;
|
||||
var api = module.exports;
|
||||
var csv = require('express-csv');
|
||||
var async = require('async');
|
||||
|
||||
api.ensureAdmin = function(req, res, next) {
|
||||
if (!res.locals.user.contributor.sudo) return res.json(401, {err:"You don't have admin access"});
|
||||
next();
|
||||
}
|
||||
|
||||
api.generateCoupons = function(req,res,next) {
|
||||
Coupon.generate(req.params.event, req.query.count, function(err){
|
||||
if(err) return next(err);
|
||||
res.send(200);
|
||||
});
|
||||
}
|
||||
|
||||
api.getCoupons = function(req,res,next) {
|
||||
var options = {sort:'seq'};
|
||||
if (req.query.limit) options.limit = req.query.limit;
|
||||
if (req.query.skip) options.skip = req.query.skip;
|
||||
Coupon.find({},{}, options, function(err,coupons){
|
||||
//res.header('Content-disposition', 'attachment; filename=coupons.csv');
|
||||
res.csv([['code']].concat(_.map(coupons, function(c){
|
||||
return [c._id];
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
api.enterCode = function(req,res,next) {
|
||||
Coupon.apply(res.locals.user,req.params.code,function(err,user){
|
||||
if (err) return res.json(400,{err:err});
|
||||
res.json(user);
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
var _ = require('lodash');
|
||||
var nconf = require('nconf');
|
||||
var async = require('async');
|
||||
var shared = require('../../../../common');
|
||||
var User = require('./../../models/user').model;
|
||||
var Group = require('./../../models/group').model;
|
||||
var api = module.exports;
|
||||
|
||||
api.ensureAdmin = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
if (!(user.contributor && user.contributor.admin)) return res.json(401, {err:"You don't have admin access"});
|
||||
next();
|
||||
}
|
||||
|
||||
api.getHeroes = function(req,res,next) {
|
||||
User.find({'contributor.level':{$gt:0}})
|
||||
.select('contributor backer balance profile.name')
|
||||
.sort('-contributor.level')
|
||||
.exec(function(err, users){
|
||||
if (err) return next(err);
|
||||
res.json(users);
|
||||
});
|
||||
}
|
||||
|
||||
api.getPatrons = function(req,res,next){
|
||||
var page = req.query.page || 0,
|
||||
perPage = 50;
|
||||
User.find({'backer.tier':{$gt:0}})
|
||||
.select('contributor backer profile.name')
|
||||
.sort('-backer.tier')
|
||||
.skip(page*perPage)
|
||||
.limit(perPage)
|
||||
.exec(function(err, users){
|
||||
if (err) return next(err);
|
||||
res.json(users);
|
||||
});
|
||||
}
|
||||
|
||||
api.getHero = function(req,res,next) {
|
||||
User.findById(req.params.uid)
|
||||
.select('contributor balance profile.name purchased items')
|
||||
.select('auth.local.username auth.local.email auth.facebook auth.blocked')
|
||||
.exec(function(err, user){
|
||||
if (err) return next(err)
|
||||
if (!user) return res.json(400,{err:'User not found'});
|
||||
res.json(user);
|
||||
});
|
||||
}
|
||||
|
||||
api.updateHero = function(req,res,next) {
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
User.findById(req.params.uid, cb);
|
||||
},
|
||||
function(member, cb){
|
||||
if (!member) return res.json(404, {err: "User not found"});
|
||||
member.balance = req.body.balance || 0;
|
||||
var newTier = req.body.contributor.level; // tier = level in this context
|
||||
var oldTier = member.contributor && member.contributor.level || 0;
|
||||
if (newTier > oldTier) {
|
||||
member.flags.contributor = true;
|
||||
var gemsPerTier = {1:3, 2:3, 3:3, 4:4, 5:4, 6:4, 7:4, 8:0, 9:0}; // e.g., tier 5 gives 4 gems. Tier 8 = moderator. Tier 9 = staff
|
||||
var tierDiff = newTier - oldTier; // can be 2+ tier increases at once
|
||||
while (tierDiff) {
|
||||
member.balance += gemsPerTier[newTier] / 4; // balance is in $
|
||||
tierDiff--;
|
||||
newTier--; // give them gems for the next tier down if they weren't aready that tier
|
||||
}
|
||||
}
|
||||
member.contributor = req.body.contributor;
|
||||
member.purchased.ads = req.body.purchased.ads;
|
||||
if (member.contributor.level >= 6) member.items.pets['Dragon-Hydra'] = 5;
|
||||
if (req.body.itemPath && req.body.itemVal
|
||||
&& req.body.itemPath.indexOf('items.') === 0
|
||||
&& User.schema.paths[req.body.itemPath]) {
|
||||
shared.dotSet(member, req.body.itemPath, req.body.itemVal); // Sanitization at 5c30944 (deemed unnecessary)
|
||||
}
|
||||
if (_.isBoolean(req.body.auth.blocked)) member.auth.blocked = req.body.auth.blocked;
|
||||
member.save(cb);
|
||||
}
|
||||
], function(err, saved){
|
||||
if (err) return next(err);
|
||||
res.json(204);
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
var User = require('mongoose').model('User');
|
||||
var groups = require('../../models/group');
|
||||
var partyFields = require('./groups').partyFields
|
||||
var api = module.exports;
|
||||
var async = require('async');
|
||||
var _ = require('lodash');
|
||||
var shared = require('../../../../common');
|
||||
var utils = require('../../libs/utils');
|
||||
var nconf = require('nconf');
|
||||
var pushNotify = require('./../pushNotifications');
|
||||
|
||||
var fetchMember = function(uuid, restrict){
|
||||
return function(cb){
|
||||
var q = User.findById(uuid);
|
||||
if (restrict) q.select(partyFields);
|
||||
q.exec(function(err, member){
|
||||
if (err) return cb(err);
|
||||
if (!member) return cb({code:404, err: 'User not found'});
|
||||
return cb(null, member);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var sendErr = function(err, res, next){
|
||||
err.code ? res.json(err.code, {err: err.err}) : next(err);
|
||||
}
|
||||
|
||||
api.getMember = function(req, res, next) {
|
||||
fetchMember(req.params.uuid, true)(function(err, member){
|
||||
if (err) return sendErr(err, res, next);
|
||||
res.json(member);
|
||||
})
|
||||
}
|
||||
|
||||
api.sendMessage = function(user, member, data){
|
||||
var msg;
|
||||
if (!data.type) {
|
||||
msg = data.message
|
||||
} else {
|
||||
msg = "`Hello " + member.profile.name + ", " + user.profile.name + " has sent you ";
|
||||
msg += (data.type=='gems') ? data.gems.amount + " gems!`" : shared.content.subscriptionBlocks[data.subscription.key].months + " months of subscription!`";
|
||||
msg += data.message;
|
||||
}
|
||||
shared.refPush(member.inbox.messages, groups.chatDefaults(msg, user));
|
||||
member.inbox.newMessages++;
|
||||
member._v++;
|
||||
member.markModified('inbox.messages');
|
||||
|
||||
shared.refPush(user.inbox.messages, _.defaults({sent:true}, groups.chatDefaults(msg, member)));
|
||||
user.markModified('inbox.messages');
|
||||
}
|
||||
|
||||
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
|
||||
return cb({code: 401, err: "Can't send message to this user."});
|
||||
}
|
||||
api.sendMessage(res.locals.user, member, {message:req.body.message});
|
||||
async.parallel([
|
||||
function (cb2) { member.save(cb2) },
|
||||
function (cb2) { res.locals.user.save(cb2) }
|
||||
], cb);
|
||||
}
|
||||
], 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: '/#/options/groups/inbox'}
|
||||
]);
|
||||
}
|
||||
|
||||
res.send(200);
|
||||
})
|
||||
}
|
||||
|
||||
api.sendGift = function(req, res, next){
|
||||
async.waterfall([
|
||||
fetchMember(req.params.uuid),
|
||||
function(member, cb) {
|
||||
// Gems
|
||||
switch (req.body.type) {
|
||||
case "gems":
|
||||
var amt = req.body.gems.amount / 4,
|
||||
user = res.locals.user;
|
||||
if (member.id == user.id)
|
||||
return cb({code: 401, err: "Cannot send gems to yourself. Try a subscription instead."});
|
||||
if (!amt || amt <=0 || user.balance < amt)
|
||||
return cb({code: 401, err: "Amount must be within 0 and your current number of gems."});
|
||||
member.balance += amt;
|
||||
user.balance -= amt;
|
||||
api.sendMessage(user, member, req.body);
|
||||
|
||||
var byUsername = utils.getUserInfo(user, ['name']).name;
|
||||
|
||||
if(member.preferences.emailNotifications.giftedGems !== false){
|
||||
utils.txnEmail(member, 'gifted-gems', [
|
||||
{name: 'GIFTER', content: byUsername},
|
||||
{name: 'X_GEMS_GIFTED', content: req.body.gems.amount}
|
||||
]);
|
||||
}
|
||||
|
||||
pushNotify.sendNotify(member, shared.i18n.t('giftedGems'), shared.i18n.t('giftedGemsInfo', { amount: req.body.gems.amount, name: byUsername }));
|
||||
|
||||
return async.parallel([
|
||||
function (cb2) { member.save(cb2) },
|
||||
function (cb2) { user.save(cb2) }
|
||||
], cb);
|
||||
case "subscription":
|
||||
return cb();
|
||||
default:
|
||||
return cb({code:400, err:"Body must contain a gems:{amount,fromBalance} or subscription:{months} object"});
|
||||
}
|
||||
}
|
||||
], function(err) {
|
||||
if (err) return sendErr(err, res, next);
|
||||
res.send(200);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
var User = require('../../models/user').model;
|
||||
var EmailUnsubscription = require('../../models/emailUnsubscription').model;
|
||||
var utils = require('../../libs/utils');
|
||||
var i18n = require('../../../../common').i18n;
|
||||
|
||||
var api = module.exports = {};
|
||||
|
||||
api.unsubscribe = function(req, res, next){
|
||||
if(!req.query.code) return res.json(500, {err: 'Missing unsubscription code.'});
|
||||
|
||||
var data = JSON.parse(utils.decrypt(req.query.code));
|
||||
|
||||
if(data._id){
|
||||
User.update({_id: data._id}, {
|
||||
$set: {'preferences.emailNotifications.unsubscribeFromAll': true}
|
||||
}, {multi: false}, function(err, updateRes){
|
||||
if(err) return next(err);
|
||||
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));
|
||||
});
|
||||
}else{
|
||||
EmailUnsubscription.findOne({email: data.email}, function(err, doc){
|
||||
if(err) return next(err);
|
||||
var okRes = '<h1>' + i18n.t('unsubscribedSuccessfully', null, req.language) + '</h1>' + i18n.t('unsubscribedTextOthers', null, req.language);
|
||||
|
||||
if(doc) return res.send(okRes);
|
||||
|
||||
EmailUnsubscription.create({email: data.email}, function(err, doc){
|
||||
if(err) return next(err);
|
||||
|
||||
res.send(okRes);
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,665 @@
|
||||
/* @see ./routes.coffee for routing*/
|
||||
|
||||
var url = require('url');
|
||||
var ipn = require('paypal-ipn');
|
||||
var _ = require('lodash');
|
||||
var nconf = require('nconf');
|
||||
var async = require('async');
|
||||
var shared = require('../../../../common');
|
||||
var User = require('./../../models/user').model;
|
||||
var utils = require('./../../libs/utils');
|
||||
var analytics = utils.analytics;
|
||||
var Group = require('./../../models/group').model;
|
||||
var Challenge = require('./../../models/challenge').model;
|
||||
var moment = require('moment');
|
||||
var logging = require('./../../libs/logging');
|
||||
var acceptablePUTPaths;
|
||||
var api = module.exports;
|
||||
var qs = require('qs');
|
||||
var firebase = require('../../libs/firebase');
|
||||
var webhook = require('../../libs/webhook');
|
||||
|
||||
// api.purchase // Shared.ops
|
||||
|
||||
api.getContent = function(req, res, next) {
|
||||
var language = 'en';
|
||||
|
||||
if (typeof req.query.language != 'undefined')
|
||||
language = req.query.language.toString(); //|| 'en' in i18n
|
||||
|
||||
var content = _.cloneDeep(shared.content);
|
||||
var walk = function(obj, lang){
|
||||
_.each(obj, function(item, key, source){
|
||||
if (_.isPlainObject(item) || _.isArray(item)) return walk(item, lang);
|
||||
if (_.isFunction(item) && item.i18nLangFunc) source[key] = item(lang);
|
||||
});
|
||||
}
|
||||
walk(content, language);
|
||||
res.json(content);
|
||||
}
|
||||
|
||||
api.getModelPaths = function(req,res,next){
|
||||
res.json(_.reduce(User.schema.paths,function(m,v,k){
|
||||
m[k] = v.instance || 'Boolean';
|
||||
return m;
|
||||
},{}));
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Tasks
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
Local Methods
|
||||
---------------
|
||||
*/
|
||||
|
||||
var findTask = function(req, res) {
|
||||
return res.locals.user.tasks[req.params.id];
|
||||
};
|
||||
|
||||
/*
|
||||
API Routes
|
||||
---------------
|
||||
*/
|
||||
|
||||
/**
|
||||
This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login
|
||||
Export it also so we can call it from deprecated.coffee
|
||||
*/
|
||||
api.score = function(req, res, next) {
|
||||
var id = req.params.id,
|
||||
direction = req.params.direction,
|
||||
user = res.locals.user,
|
||||
task;
|
||||
|
||||
var clearMemory = function(){user = task = id = direction = null;}
|
||||
|
||||
// Send error responses for improper API call
|
||||
if (!id) return res.json(400, {err: ':id required'});
|
||||
if (direction !== 'up' && direction !== 'down') {
|
||||
if (direction == 'unlink' || direction == 'sort') return next();
|
||||
return res.json(400, {err: ":direction must be 'up' or 'down'"});
|
||||
}
|
||||
// If exists already, score it
|
||||
if (task = user.tasks[id]) {
|
||||
// Set completed if type is daily or todo and task exists
|
||||
if (task.type === 'daily' || task.type === 'todo') {
|
||||
task.completed = direction === 'up';
|
||||
}
|
||||
} else {
|
||||
// If it doesn't exist, this is likely a 3rd party up/down - create a new one, then score it
|
||||
// Defaults. Other defaults are handled in user.ops.addTask()
|
||||
task = {
|
||||
id: id,
|
||||
type: req.body && req.body.type,
|
||||
text: req.body && req.body.text,
|
||||
notes: (req.body && req.body.notes) || "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task."
|
||||
};
|
||||
|
||||
if (task.type === 'daily' || task.type === 'todo')
|
||||
task.completed = direction === 'up';
|
||||
|
||||
task = user.ops.addTask({body:task});
|
||||
}
|
||||
var delta = user.ops.score({params:{id:task.id, direction:direction}, language: req.language});
|
||||
|
||||
user.save(function(err, saved){
|
||||
if (err) return next(err);
|
||||
|
||||
var userStats = saved.toJSON().stats;
|
||||
var resJsonData = _.extend({ delta: delta, _tmp: user._tmp }, userStats);
|
||||
res.json(200, resJsonData);
|
||||
|
||||
var webhookData = _generateWebhookTaskData(
|
||||
task, direction, delta, userStats, user
|
||||
);
|
||||
webhook.sendTaskWebhook(user.preferences.webhooks, webhookData);
|
||||
|
||||
if (
|
||||
(!task.challenge || !task.challenge.id || task.challenge.broken) // If it's a challenge task, sync the score. Do it in the background, we've already sent down a response and the user doesn't care what happens back there
|
||||
|| (task.type == 'reward') // we don't want to update the reward GP cost
|
||||
) return clearMemory();
|
||||
|
||||
Challenge.findById(task.challenge.id, 'habits dailys todos rewards', function(err, chal) {
|
||||
if (err) return next(err);
|
||||
if (!chal) {
|
||||
task.challenge.broken = 'CHALLENGE_DELETED';
|
||||
user.save();
|
||||
return clearMemory();
|
||||
}
|
||||
var t = chal.tasks[task.id];
|
||||
// this task was removed from the challenge, notify user
|
||||
if (!t) {
|
||||
chal.syncToUser(user);
|
||||
return clearMemory();
|
||||
}
|
||||
|
||||
t.value += delta;
|
||||
if (t.type == 'habit' || t.type == 'daily')
|
||||
t.history.push({value: t.value, date: +new Date});
|
||||
chal.save();
|
||||
clearMemory();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all tasks
|
||||
*/
|
||||
api.getTasks = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
if (req.query.type) {
|
||||
return res.json(user[req.query.type+'s']);
|
||||
} else {
|
||||
return res.json(_.toArray(user.tasks));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Task
|
||||
*/
|
||||
api.getTask = function(req, res, next) {
|
||||
var task = findTask(req,res);
|
||||
if (!task) return res.json(404, {err: shared.i18n.t('messageTaskNotFound')});
|
||||
return res.json(200, task);
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
Update Task
|
||||
*/
|
||||
|
||||
//api.deleteTask // see Shared.ops
|
||||
// api.updateTask // handled in Shared.ops
|
||||
// api.addTask // handled in Shared.ops
|
||||
// api.sortTask // handled in Shared.ops #TODO updated api, mention in docs
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Items
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
// api.buy // handled in Shard.ops
|
||||
|
||||
api.getBuyList = function (req, res, next) {
|
||||
var list = shared.updateStore(res.locals.user);
|
||||
return res.json(200, list);
|
||||
};
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
User
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get User
|
||||
*/
|
||||
api.getUser = function(req, res, next) {
|
||||
var user = res.locals.user.toJSON();
|
||||
user.stats.toNextLevel = shared.tnl(user.stats.lvl);
|
||||
user.stats.maxHealth = shared.maxHealth;
|
||||
user.stats.maxMP = res.locals.user._statsComputed.maxMP;
|
||||
delete user.apiToken;
|
||||
if (user.auth && user.auth.local) {
|
||||
delete user.auth.local.hashed_password;
|
||||
delete user.auth.local.salt;
|
||||
}
|
||||
return res.json(200, user);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get anonymized User
|
||||
*/
|
||||
api.getUserAnonymized = function(req, res, next) {
|
||||
var user = res.locals.user.toJSON();
|
||||
user.stats.toNextLevel = shared.tnl(user.stats.lvl);
|
||||
user.stats.maxHealth = shared.maxHealth;
|
||||
user.stats.maxMP = res.locals.user._statsComputed.maxMP;
|
||||
|
||||
delete user.apiToken;
|
||||
|
||||
if (user.auth) {
|
||||
delete user.auth.local;
|
||||
delete user.auth.facebook;
|
||||
}
|
||||
|
||||
delete user.newMessages;
|
||||
|
||||
delete user.profile;
|
||||
delete user.purchased.plan;
|
||||
delete user.contributor;
|
||||
delete user.invitations;
|
||||
|
||||
delete user.items.special.nyeReceived;
|
||||
delete user.items.special.valentineReceived;
|
||||
|
||||
delete user.webhooks;
|
||||
delete user.achievements.challenges;
|
||||
|
||||
_.forEach(user.inbox.messages, function(msg){
|
||||
msg.text = "inbox message text";
|
||||
});
|
||||
|
||||
_.forEach(user.tags, function(tag){
|
||||
tag.name = "tag";
|
||||
tag.challenge = "challenge";
|
||||
});
|
||||
|
||||
function cleanChecklist(task){
|
||||
var checklistIndex = 0;
|
||||
|
||||
_.forEach(task.checklist, function(c){
|
||||
c.text = "item" + checklistIndex++;
|
||||
});
|
||||
}
|
||||
|
||||
_.forEach(user.habits, function(task){
|
||||
task.text = "task text";
|
||||
task.notes = "task notes";
|
||||
});
|
||||
|
||||
_.forEach(user.rewards, function(task){
|
||||
task.text = "task text";
|
||||
task.notes = "task notes";
|
||||
});
|
||||
|
||||
_.forEach(user.dailys, function(task){
|
||||
task.text = "task text";
|
||||
task.notes = "task notes";
|
||||
|
||||
cleanChecklist(task);
|
||||
});
|
||||
|
||||
_.forEach(user.todos, function(task){
|
||||
task.text = "task text";
|
||||
task.notes = "task notes";
|
||||
|
||||
cleanChecklist(task);
|
||||
});
|
||||
|
||||
return res.json(200, user);
|
||||
};
|
||||
|
||||
/**
|
||||
* This tells us for which paths users can call `PUT /user` (or batch-update equiv, which use `User.set()` on our client).
|
||||
* The trick here is to only accept leaf paths, not root/intermediate paths (see http://goo.gl/OEzkAs)
|
||||
* FIXME - one-by-one we want to widdle down this list, instead replacing each needed set path with API operations
|
||||
*/
|
||||
acceptablePUTPaths = _.reduce(require('./../../models/user').schema.paths, function(m,v,leaf){
|
||||
var found= _.find('achievements filters flags invitations lastCron party preferences profile stats inbox'.split(' '), function(root){
|
||||
return leaf.indexOf(root) == 0;
|
||||
});
|
||||
if (found) m[leaf]=true;
|
||||
return m;
|
||||
}, {})
|
||||
|
||||
_.each('stats.class'.split(' '), function(removePath){
|
||||
delete acceptablePUTPaths[removePath];
|
||||
})
|
||||
|
||||
/**
|
||||
* Update user
|
||||
* Send up PUT /user as `req.body={path1:val, path2:val, etc}`. Example:
|
||||
* PUT /user {'stats.hp':50, 'tasks.TASK_ID.repeat.m':false}
|
||||
* See acceptablePUTPaths for which user paths are supported
|
||||
*/
|
||||
api.update = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
var errors = [];
|
||||
if (_.isEmpty(req.body)) return res.json(200, user);
|
||||
|
||||
_.each(req.body, function(v, k) {
|
||||
if (acceptablePUTPaths[k])
|
||||
user.fns.dotSet(k, v);
|
||||
else
|
||||
errors.push(shared.i18n.t('messageUserOperationProtected', { operation: k }));
|
||||
return true;
|
||||
});
|
||||
user.save(function(err) {
|
||||
if (!_.isEmpty(errors)) return res.json(401, {err: errors});
|
||||
if (err) return next(err);
|
||||
res.json(200, user);
|
||||
user = errors = null;
|
||||
});
|
||||
};
|
||||
|
||||
api.cron = function(req, res, next) {
|
||||
var user = res.locals.user,
|
||||
progress = user.fns.cron({analytics:utils.analytics}),
|
||||
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);
|
||||
|
||||
// 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
|
||||
// api.reset // Shared.ops
|
||||
|
||||
api.delete = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
var plan = user.purchased.plan;
|
||||
|
||||
if (plan && plan.customerId && !plan.dateTerminated){
|
||||
return res.json(400,{err:"You have an active subscription, cancel your plan before deleting your account."});
|
||||
}
|
||||
|
||||
Group.find({
|
||||
members: {
|
||||
'$in': [user._id]
|
||||
}
|
||||
}, function(err, groups){
|
||||
if(err) return next(err);
|
||||
|
||||
async.each(groups, function(group, cb){
|
||||
group.leave(user, 'remove-all', cb);
|
||||
}, function(err){
|
||||
if(err) return next(err);
|
||||
|
||||
user.remove(function(err){
|
||||
if(err) return next(err);
|
||||
|
||||
firebase.deleteUser(user._id);
|
||||
res.send(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Development Only Operations
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
if (nconf.get('NODE_ENV') === 'development') {
|
||||
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
api.addHourglass = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
|
||||
user.purchased.plan.consecutive.trinkets += 1;
|
||||
|
||||
user.save(function(err){
|
||||
if (err) return next(err);
|
||||
res.send(204);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Tags
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
// api.deleteTag // handled in Shared.ops
|
||||
// api.addTag // handled in Shared.ops
|
||||
// api.updateTag // handled in Shared.ops
|
||||
// api.sortTag // handled in Shared.ops
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Spells
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
api.cast = function(req, res, next) {
|
||||
var user = res.locals.user,
|
||||
targetType = req.query.targetType,
|
||||
targetId = req.query.targetId,
|
||||
klass = shared.content.spells.special[req.params.spell] ? 'special' : user.stats.class,
|
||||
spell = shared.content.spells[klass][req.params.spell];
|
||||
|
||||
if (!spell) return res.json(404, {err: 'Spell "' + req.params.spell + '" not found.'});
|
||||
if (spell.mana > user.stats.mp) return res.json(400, {err: 'Not enough mana to cast spell'});
|
||||
|
||||
var done = function(){
|
||||
var err = arguments[0];
|
||||
var saved = _.size(arguments == 3) ? arguments[2] : arguments[1];
|
||||
if (err) return next(err);
|
||||
res.json(saved);
|
||||
user = targetType = targetId = klass = spell = null;
|
||||
}
|
||||
|
||||
switch (targetType) {
|
||||
case 'task':
|
||||
if (!user.tasks[targetId]) return res.json(404, {err: 'Task "' + targetId + '" not found.'});
|
||||
spell.cast(user, user.tasks[targetId]);
|
||||
user.save(done);
|
||||
break;
|
||||
|
||||
case 'self':
|
||||
spell.cast(user);
|
||||
user.save(done);
|
||||
break;
|
||||
|
||||
case 'party':
|
||||
case 'user':
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
Group.findOne({type: 'party', members: {'$in': [user._id]}}).populate('members', 'profile.name stats achievements items.special').exec(cb);
|
||||
},
|
||||
function(group, cb) {
|
||||
// Solo player? let's just create a faux group for simpler code
|
||||
var g = group ? group : {members:[user]};
|
||||
var series = [], found;
|
||||
if (targetType == 'party') {
|
||||
spell.cast(user, g.members);
|
||||
series = _.transform(g.members, function(m,v,k){
|
||||
m.push(function(cb2){v.save(cb2)});
|
||||
});
|
||||
} else {
|
||||
found = _.find(g.members, {_id: targetId})
|
||||
spell.cast(user, found);
|
||||
series.push(function(cb2){found.save(cb2)});
|
||||
}
|
||||
|
||||
if (group && !spell.silent) {
|
||||
series.push(function(cb2){
|
||||
var message = '`'+user.profile.name+' casts '+spell.text() + (targetType=='user' ? ' on '+found.profile.name : ' for the party')+'.`';
|
||||
group.sendChat(message);
|
||||
group.save(cb2);
|
||||
})
|
||||
}
|
||||
|
||||
series.push(function(cb2){g = group = series = found = null;cb2();})
|
||||
|
||||
async.series(series, cb);
|
||||
},
|
||||
function(whatever, cb){
|
||||
user.save(cb);
|
||||
}
|
||||
], done);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// It supports guild too now but we'll stick to partyInvite for backward compatibility
|
||||
api.sessionPartyInvite = function(req,res,next){
|
||||
if (!req.session.partyInvite) return next();
|
||||
var inv = res.locals.user.invitations;
|
||||
if (inv.party && inv.party.id) return next(); // already invited to a party
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
Group.findOne({_id:req.session.partyInvite.id, members:{$in:[req.session.partyInvite.inviter]}})
|
||||
.select('invites members type').exec(cb);
|
||||
},
|
||||
function(group, cb){
|
||||
if (!group){
|
||||
// Don't send error as it will prevent users from using the site
|
||||
delete req.session.partyInvite;
|
||||
return cb();
|
||||
}
|
||||
|
||||
if (group.type == 'guild'){
|
||||
inv.guilds.push(req.session.partyInvite);
|
||||
} else{
|
||||
//req.body.type in 'guild', 'party'
|
||||
inv.party = req.session.partyInvite;
|
||||
}
|
||||
inv.party = req.session.partyInvite;
|
||||
delete req.session.partyInvite;
|
||||
if (!~group.invites.indexOf(res.locals.user._id))
|
||||
group.invites.push(res.locals.user._id); //$addToSt
|
||||
group.save(cb);
|
||||
},
|
||||
function(saved, cb){
|
||||
res.locals.user.save(cb);
|
||||
}
|
||||
], next);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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]) {
|
||||
api[k] = function(req, res, next) {
|
||||
res.locals.user.ops[k](req,function(err, response){
|
||||
// If we want to send something other than 500, pass err as {code: 200, message: "Not enough GP"}
|
||||
if (err) {
|
||||
if (!err.code) return next(err);
|
||||
if (err.code >= 400) return res.json(err.code,{err:err.message});
|
||||
// In the case of 200s, they're friendly alert messages like "You're pet has hatched!" - still send the op
|
||||
}
|
||||
res.locals.user.save(function(err){
|
||||
if (err) return next(err);
|
||||
res.json(200,response);
|
||||
})
|
||||
}, analytics);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Batch Update
|
||||
Run a bunch of updates all at once
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
api.batchUpdate = function(req, res, next) {
|
||||
if (_.isEmpty(req.body)) req.body = []; // cases of {} or null
|
||||
if (req.body[0] && req.body[0].data)
|
||||
return res.json(501, {err: "API has been updated, please refresh your browser or upgrade your mobile app."})
|
||||
|
||||
var user = res.locals.user;
|
||||
var oldSend = res.send;
|
||||
var oldJson = res.json;
|
||||
|
||||
// Stash user.save, we'll queue the save op till the end (so we don't overload the server)
|
||||
var oldSave = user.save;
|
||||
user.save = function(cb){cb(null,user)}
|
||||
|
||||
// Setup the array of functions we're going to call in parallel with async
|
||||
res.locals.ops = [];
|
||||
var ops = _.transform(req.body, function(m,_req){
|
||||
if (_.isEmpty(_req)) return;
|
||||
_req.language = req.language;
|
||||
|
||||
m.push(function() {
|
||||
var cb = arguments[arguments.length-1];
|
||||
res.locals.ops.push(_req);
|
||||
res.send = res.json = function(code, data) {
|
||||
if (_.isNumber(code) && code >= 500)
|
||||
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);
|
||||
});
|
||||
})
|
||||
// Finally, save user at the end
|
||||
.concat(function(){
|
||||
user.save = oldSave;
|
||||
user.save(arguments[arguments.length-1]);
|
||||
});
|
||||
|
||||
// call all the operations, then return the user object to the requester
|
||||
async.waterfall(ops, function(err,_user) {
|
||||
res.json = oldJson;
|
||||
res.send = oldSend;
|
||||
if (err) return next(err);
|
||||
|
||||
var response = _user.toJSON();
|
||||
response.wasModified = res.locals.wasModified;
|
||||
|
||||
user.fns.nullify();
|
||||
user = res.locals.user = oldSend = oldJson = oldSave = null;
|
||||
|
||||
// return only drops & streaks
|
||||
if (response._tmp && response._tmp.drop){
|
||||
res.json(200, {_tmp: {drop: response._tmp.drop}, _v: response._v});
|
||||
|
||||
// Fetch full user object
|
||||
} else if (response.wasModified){
|
||||
// Preen 3-day past-completed To-Dos from Angular & mobile app
|
||||
response.todos = shared.preenTodos(response.todos);
|
||||
res.json(200, response);
|
||||
|
||||
// return only the version number
|
||||
} else{
|
||||
res.json(200, {_v: response._v});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function _generateWebhookTaskData(task, direction, delta, stats, user) {
|
||||
var extendedStats = _.extend(stats, {
|
||||
toNextLevel: shared.tnl(user.stats.lvl),
|
||||
maxHealth: shared.maxHealth,
|
||||
maxMP: user._statsComputed.maxMP
|
||||
});
|
||||
|
||||
var userData = {
|
||||
_id: user._id,
|
||||
_tmp: user._tmp,
|
||||
stats: extendedStats
|
||||
};
|
||||
|
||||
var taskData = {
|
||||
details: task,
|
||||
direction: direction,
|
||||
delta: delta
|
||||
}
|
||||
|
||||
return {
|
||||
task: taskData,
|
||||
user: userData
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user