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');