diff --git a/.travis.yml b/.travis.yml
index dcd028fd9e..915e677edf 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,11 +1,13 @@
language: node_js
node_js:
- - '0.10'
+ - '4.1'
before_install:
+ - "npm install -g npm@3"
- "sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10"
- - "echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list"
- - "sudo apt-get update"
- - "sudo apt-get install mongodb-org-server"
+ - "sudo wget http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.0.4.tgz -O /tmp/mongodb.tgz"
+ - "sudo tar -xvf /tmp/mongodb.tgz"
+ - "sudo mkdir /tmp/data"
+ - "sudo ${PWD}/mongodb-linux-x86_64-3.0.4/bin/mongod --dbpath /tmp/data --bind_ip 127.0.0.1 &> /dev/null &"
before_script:
- 'npm install -g grunt-cli mocha'
- cp config.json.example config.json
diff --git a/package.json b/package.json
index 57b8a4cbc9..4a4eea0542 100644
--- a/package.json
+++ b/package.json
@@ -4,20 +4,25 @@
"version": "0.0.0-152",
"main": "./website/src/server.js",
"dependencies": {
+ "accepts": "^1.2.12",
"amazon-payments": "0.0.4",
"amplitude": "^2.0.1",
"async": "~0.9.0",
"aws-sdk": "^2.0.25",
"babel": "^5.5.4",
"gulp-babel": "^5.2.1",
+ "body-parser": "^1.13.3",
"bower": "~1.3.12",
"browserify": "~3.30.2",
"coffee-script": "1.6.x",
"coffeeify": "0.6.0",
+ "compression": "^1.5.2",
"connect-ratelimit": "0.0.7",
+ "cookie-parser": "^1.3.5",
+ "cookie-session": "^1.2.0",
"coupon-code": "~0.3.0",
"domain-middleware": "~0.1.0",
- "express": "~3.17.5",
+ "express": "~4.13.3",
"express-csv": "~0.6.0",
"firebase": "^2.2.9",
"firebase-token-generator": "^2.0.0",
@@ -53,10 +58,11 @@
"merge-stream": "^1.0.0",
"method-override": "~2.2.0",
"moment": "~2.8.3",
- "mongoose": "~3.8.23",
+ "mongoose": "~4.2.3",
"mongoose-id-autoinc": "~2013.7.14-4",
+ "morgan": "^1.6.1",
"nconf": "~0.6.9",
- "newrelic": "~1.11.2",
+ "newrelic": "~1.22.2",
"nib": "~1.0.1",
"nodemailer": "~0.5.2",
"pageres": "^1.0.1",
@@ -71,6 +77,7 @@
"qs": "^2.3.2",
"request": "~2.44.0",
"s3-upload-stream": "^1.0.6",
+ "serve-favicon": "^2.3.0",
"stripe": "*",
"superagent": "~1.4.0",
"swagger-node-express": "lefnire/swagger-node-express#habitrpg",
@@ -81,13 +88,9 @@
"winston-newrelic": "~0.1.4"
},
"private": true,
- "subdomain": "habitrpg",
- "domains": [
- "habitrpg.com",
- "www.habitrpg.com"
- ],
"engines": {
- "node": "0.10.x"
+ "node": "~4.1.1",
+ "npm": "^3.3.8"
},
"scripts": {
"test": "gulp test",
diff --git a/test/api-legacy/challenges.coffee b/test/api-legacy/challenges.coffee
index 8fa3afe9ce..30e5ccad8d 100644
--- a/test/api-legacy/challenges.coffee
+++ b/test/api-legacy/challenges.coffee
@@ -161,7 +161,7 @@ describe "Challenges", ->
# Now let's handle if challenge was deleted, but didn't get to update all the users (an error)
unset = $unset: {}
unset["$unset"]["dailys." + len + ".challenge.broken"] = 1
- User.findByIdAndUpdate user._id, unset, (err, user) ->
+ User.findByIdAndUpdate user._id, unset, {new: true}, (err, user) ->
expect(err).to.not.exist
expect(user.dailys[len].challenge.broken).to.not.exist
request.post(baseURL + "/user/tasks/" + daily.id + "/up").end (err, res) ->
@@ -175,6 +175,7 @@ describe "Challenges", ->
User.findByIdAndUpdate user._id,
$set:
"contributor.admin": true
+ , {new: true}
, (err, _user) ->
expect(err).to.not.exist
async.parallel [
@@ -205,7 +206,8 @@ describe "Challenges", ->
it "User creates a non-tavern challenge with prize, deletes it, gets refund", (done) ->
User.findByIdAndUpdate user._id,
$set:
- "balance": 8
+ "balance": 8,
+ , {new: true}
, (err, user) ->
expect(err).to.not.be.ok
request.post(baseURL + "/challenges").send(
@@ -234,7 +236,8 @@ describe "Challenges", ->
it "User creates a tavern challenge with prize, deletes it, and does not get refund", (done) ->
User.findByIdAndUpdate user._id,
$set:
- "balance": 8
+ "balance": 8,
+ , {new: true}
, (err, user) ->
expect(err).to.not.be.ok
request.post(baseURL + "/challenges").send(
@@ -312,7 +315,7 @@ describe "Challenges", ->
context "non-owner that is an admin", () ->
beforeEach (done) ->
- User.findByIdAndUpdate(user._id, { 'contributor.admin': true }, done)
+ User.findByIdAndUpdate(user._id, { 'contributor.admin': true }, {new: true}, done)
it 'can edit challenge', (done) ->
challenge.name = 'foobar'
diff --git a/test/api-legacy/coupons.coffee b/test/api-legacy/coupons.coffee
index c6fb4f36d7..e0fd380bf5 100644
--- a/test/api-legacy/coupons.coffee
+++ b/test/api-legacy/coupons.coffee
@@ -6,7 +6,7 @@ Coupon = require("../../website/src/models/coupon").model
makeSudoUser = (usr, cb) ->
registerNewUser ->
sudoUpdate = { "$set" : { "contributor.sudo" : true } }
- User.findByIdAndUpdate user._id, sudoUpdate, (err, _user) ->
+ User.findByIdAndUpdate user._id, sudoUpdate, {new: true}, (err, _user) ->
usr = _user
cb()
, true
diff --git a/test/api-legacy/party.coffee b/test/api-legacy/party.coffee
index 83adc7df0c..93dadf5b12 100644
--- a/test/api-legacy/party.coffee
+++ b/test/api-legacy/party.coffee
@@ -58,6 +58,7 @@ describe "Party", ->
User.findByIdAndUpdate user._id,
$set:
"stats.lvl": 50
+ , {new: true}
, (err, _user) ->
cb(null, _user)
(_user, cb) ->
@@ -142,6 +143,7 @@ describe "Party", ->
User.findByIdAndUpdate user._id,
$set:
"items.quests.vice3": 1
+ , {new: true}
, cb
(_user, cb) ->
@@ -281,6 +283,7 @@ describe "Party", ->
Group.findByIdAndUpdate group._id,
$set:
"quest.progress.hp": 0
+ , {new: true}
, cb2
], cb
(_group, cb) ->
diff --git a/test/api-legacy/users.coffee b/test/api-legacy/users.coffee
index 5e992cbcf6..69411cf792 100644
--- a/test/api-legacy/users.coffee
+++ b/test/api-legacy/users.coffee
@@ -27,6 +27,7 @@ describe "Users", ->
User.findByIdAndUpdate userToDelete._id,
$set:
"balance": 4
+ , {new: true}
, (err, _user) ->
cb()
@@ -88,6 +89,7 @@ describe "Users", ->
User.findByIdAndUpdate user._id,
$set:
"balance": 4
+ , {new: true}
, (err, _user) ->
async.waterfall [
(cb) ->
diff --git a/website/src/controllers/challenges.js b/website/src/controllers/challenges.js
index a8bc4dc244..6ac40b2571 100644
--- a/website/src/controllers/challenges.js
+++ b/website/src/controllers/challenges.js
@@ -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){
diff --git a/website/src/controllers/groups.js b/website/src/controllers/groups.js
index c96a6420b1..800aa0eabf 100644
--- a/website/src/controllers/groups.js
+++ b/website/src/controllers/groups.js
@@ -521,6 +521,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);
});
};
@@ -683,7 +684,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"});
}
}
diff --git a/website/src/controllers/unsubscription.js b/website/src/controllers/unsubscription.js
index d0ef89f920..f839b11551 100644
--- a/website/src/controllers/unsubscription.js
+++ b/website/src/controllers/unsubscription.js
@@ -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.n !== 1) return res.json(404, {err: 'User not found'});
res.send('
' + i18n.t('unsubscribedSuccessfully', null, req.language) + '
' + i18n.t('unsubscribedTextUsers', null, req.language));
});
diff --git a/website/src/controllers/user.js b/website/src/controllers/user.js
index 71cd4c5450..f33c65970e 100644
--- a/website/src/controllers/user.js
+++ b/website/src/controllers/user.js
@@ -364,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;
diff --git a/website/src/i18n.js b/website/src/i18n.js
index 2f0f14eeb6..19444f89f0 100644
--- a/website/src/i18n.js
+++ b/website/src/i18n.js
@@ -1,9 +1,10 @@
-var fs = require('fs'),
- path = require('path'),
- _ = require('lodash'),
- User = require('./models/user').model,
- shared = require('../../common'),
- translations = {};
+var fs = require('fs');
+var path = require('path');
+var _ = require('lodash');
+var accepts = require('accepts');
+var User = require('./models/user').model;
+var shared = require('../../common');
+var translations = {};
var localePath = path.join(__dirname, "/../../common/locales/")
@@ -74,7 +75,9 @@ var chineseVersions = ['zh-tw'];
var getUserLanguage = function(req, res, next){
var getFromBrowser = function(){
- var acceptable = _(req.acceptedLanguages).map(function(lang){
+ var acceptedLanguages = accepts(req).languages();
+
+ var acceptable = _(acceptedLanguages).map(function(lang){
return lang.slice(0, 2);
}).uniq().value();
@@ -83,7 +86,7 @@ var getUserLanguage = function(req, res, next){
var iAcceptedCompleteLang = (matches.length > 0) ? multipleVersionsLanguages.indexOf(matches[0].toLowerCase()) : -1;
if(iAcceptedCompleteLang !== -1){
- var acceptedCompleteLang = _.find(req.acceptedLanguages, function(accepted){
+ var acceptedCompleteLang = _.find(acceptedLanguages, function(accepted){
return accepted.slice(0, 2) == multipleVersionsLanguages[iAcceptedCompleteLang];
});
diff --git a/website/src/routes/apiv1.js b/website/src/routes/apiv1.js
index 5a28ef8f3b..f2876fedc4 100644
--- a/website/src/routes/apiv1.js
+++ b/website/src/routes/apiv1.js
@@ -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
diff --git a/website/src/routes/apiv2.coffee b/website/src/routes/apiv2.coffee
index 8edc2277aa..746423684f 100644
--- a/website/src/routes/apiv2.coffee
+++ b/website/src/routes/apiv2.coffee
@@ -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:
@@ -774,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:
diff --git a/website/src/server.js b/website/src/server.js
index 52919d523c..92f5df84e1 100644
--- a/website/src/server.js
+++ b/website/src/server.js
@@ -1,5 +1,5 @@
// Only do the minimal amount of work before forking just in case of a dyno restart
-var cluster = require("cluster");
+var cluster = require('cluster');
var _ = require('lodash');
var nconf = require('nconf');
var utils = require('./utils');
@@ -8,11 +8,13 @@ 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;
+var cores = +nconf.get('WEB_CONCURRENCY') || 0;
if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
// Fork workers. If config.json has CORES=x, use that - otherwise, use all cpus-1 (production)
- _.times(cores, cluster.fork);
+ for (var i = 0, n = cores; i < n; i += 1) {
+ cluster.fork();
+ }
cluster.on('disconnect', function(worker, code, signal) {
var w = cluster.fork(); // replace the dead worker
@@ -21,10 +23,10 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
} else {
require('coffee-script'); // remove this once we've fully converted over
- var express = require("express");
- var http = require("http");
- var path = require("path");
- var swagger = require("swagger-node-express");
+ var express = require('express');
+ var http = require('http');
+ var path = require('path');
+ var swagger = require('swagger-node-express');
var autoinc = require('mongoose-id-autoinc');
var shared = require('../../common');
@@ -77,8 +79,8 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
// This auth strategy is no longer used. It's just kept around for auth.js#loginFacebook() (passport._strategies.facebook.userProfile)
// The proper fix would be to move to a general OAuth module simply to verify accessTokens
passport.use(new FacebookStrategy({
- clientID: nconf.get("FACEBOOK_KEY"),
- clientSecret: nconf.get("FACEBOOK_SECRET"),
+ clientID: nconf.get('FACEBOOK_KEY'),
+ clientSecret: nconf.get('FACEBOOK_SECRET'),
//callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback"
},
function(accessToken, refreshToken, profile, done) {
@@ -87,61 +89,75 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
));
// ------------ Server Configuration ------------
- var publicDir = path.join(__dirname, "/../public");
+ var publicDir = path.join(__dirname, '/../public');
- app.set("port", nconf.get('PORT'));
+ app.set('port', nconf.get('PORT'));
require('./middlewares/apiThrottle')(app);
app.use(require('./middlewares/domain')(server,mongoose));
- if (!isProd && !DISABLE_LOGGING) app.use(express.logger("dev"));
- app.use(express.compress());
- app.set("views", __dirname + "/../views");
- app.set("view engine", "jade");
- app.use(express.favicon(publicDir + '/favicon.ico'));
+ if (!isProd && !DISABLE_LOGGING) app.use(require('morgan')('dev'));
+ app.use(require('compression')());
+ app.set('views', __dirname + '/../views');
+ app.set('view engine', 'jade');
+ app.use(require('serve-favicon')(publicDir + '/favicon.ico'));
app.use(require('./middlewares/cors'));
var redirects = require('./middlewares/redirects');
app.use(redirects.forceHabitica);
app.use(redirects.forceSSL);
- app.use(express.urlencoded());
- app.use(express.json());
+
+ var bodyParser = require('body-parser');
+ // Default limit is 100kb, need that because we actually send whole groups to the server
+ // FIXME as soon as possible (need to move on the client from $resource -> $http)
+ app.use(bodyParser.urlencoded({
+ limit: '1mb',
+ parameterLimit: 10000, // Upped for safety from 1k, FIXME as above
+ extended: true // Uses 'qs' library as old connect middleware
+ }));
+ app.use(bodyParser.json({
+ limit: '1mb'
+ }));
+
app.use(require('method-override')());
//app.use(express.cookieParser(nconf.get('SESSION_SECRET')));
- app.use(express.cookieParser());
- app.use(express.cookieSession({ secret: nconf.get('SESSION_SECRET'), httpOnly: false, cookie: { maxAge: TWO_WEEKS }}));
- //app.use(express.session());
+ app.use(require('cookie-parser')());
+ app.use(require('cookie-session')({
+ name: 'connect:sess', // Used to keep backward compatibility with Express 3 cookies
+ secret: nconf.get('SESSION_SECRET'),
+ httpOnly: false,
+ maxAge: TWO_WEEKS
+ }));
// Initialize Passport! Also use passport.session() middleware, to support
// persistent login sessions (recommended).
app.use(passport.initialize());
app.use(passport.session());
- app.use(app.router);
+ // Custom Directives
+ app.use(require('./routes/pages'));
+ app.use(require('./routes/payments'));
+ app.use(require('./routes/auth'));
+ app.use(require('./routes/coupon'));
+ app.use(require('./routes/unsubscription'));
+ var v2 = express();
+ app.use('/api/v2', v2);
+ app.use('/api/v1', require('./routes/apiv1'));
+ app.use('/export', require('./routes/dataexport'));
+ require('./routes/apiv2.coffee')(swagger, v2);
var maxAge = isProd ? 31536000000 : 0;
// Cache emojis without copying them to build, they are too many
- app.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge }));
- app.use('/common/dist', express['static'](publicDir + "/../../common/dist", { maxAge: maxAge }));
- app.use('/common/audio', express['static'](publicDir + "/../../common/audio", { maxAge: maxAge }));
- app.use('/common/script/public', express['static'](publicDir + "/../../common/script/public", { maxAge: maxAge }));
- app.use('/common/img', express['static'](publicDir + "/../../common/img", { maxAge: maxAge }));
- app.use(express['static'](publicDir));
+ app.use(express.static(path.join(__dirname, '/../build'), { maxAge: maxAge }));
+ app.use('/common/dist', express.static(publicDir + '/../../common/dist', { maxAge: maxAge }));
+ app.use('/common/audio', express.static(publicDir + '/../../common/audio', { maxAge: maxAge }));
+ app.use('/common/script/public', express.static(publicDir + '/../../common/script/public', { maxAge: maxAge }));
+ app.use('/common/img', express.static(publicDir + '/../../common/img', { maxAge: maxAge }));
+ app.use(express.static(publicDir));
- // Custom Directives
- app.use(require('./routes/pages').middleware);
- app.use(require('./routes/payments').middleware);
- app.use(require('./routes/auth').middleware);
- app.use(require('./routes/coupon').middleware);
- app.use(require('./routes/unsubscription').middleware);
- var v2 = express();
- app.use('/api/v2', v2);
- app.use('/api/v1', require('./routes/apiv1').middleware);
- app.use('/export', require('./routes/dataexport').middleware);
- require('./routes/apiv2.coffee')(swagger, v2);
app.use(require('./middlewares/errorHandler'));
server.on('request', app);
- server.listen(app.get("port"), function() {
- return logging.info("Express server listening on port " + app.get("port"));
+ server.listen(app.get('port'), function() {
+ return logging.info('Express server listening on port ' + app.get('port'));
});
module.exports = server;
diff --git a/website/src/utils.js b/website/src/utils.js
index 4f17cc1c9d..f83d730b31 100644
--- a/website/src/utils.js
+++ b/website/src/utils.js
@@ -174,8 +174,8 @@ module.exports.setupConfig = function(){
if (nconf.get('NODE_ENV') === "development")
Error.stackTraceLimit = Infinity;
- //if (nconf.get('NODE_ENV') === 'production')
- // require('newrelic');
+ if (nconf.get('NODE_ENV') === 'production')
+ require('newrelic');
isProd = nconf.get('NODE_ENV') === 'production';
baseUrl = nconf.get('BASE_URL');