diff --git a/.gitignore b/.gitignore index bb5ec66ebf..4a6eb3925d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ newrelic_agent.log .bower-tmp .bower-registry .bower-cache + +*.log src/*/*.map src/*/*/*.map test/*.js diff --git a/.nodemonignore b/.nodemonignore index f4e0b53926..d104457dfc 100644 --- a/.nodemonignore +++ b/.nodemonignore @@ -6,4 +6,5 @@ Gruntfile.js CHANGELOG.md .idea* .git* +*.log newrelic_agent.log diff --git a/Gruntfile.js b/Gruntfile.js index e5fa1ed3ed..0d7247aa83 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -78,7 +78,7 @@ module.exports = function(grunt) { nodemon: { dev: { - ignoredFiles: ['public/*', 'Gruntfile.js', 'CHANGELOG.md', 'views/*', 'build/*', '.idea*', '.git*'] + ignoredFiles: ['public/*', 'Gruntfile.js', 'CHANGELOG.md', 'views/*', 'build/*', '.idea*', '.git*', '*.log'] } }, diff --git a/config.json.example b/config.json.example index 634d22c7a7..f63a71703e 100644 --- a/config.json.example +++ b/config.json.example @@ -11,6 +11,9 @@ "SMTP_USER":"user@domain.com", "SMTP_PASS":"password", "SMTP_SERVICE":"Gmail", + "SMTP_HOST":"smtp.gmail.com", + "SMTP_PORT": 587, + "SMTP_TLS": true, "STRIPE_API_KEY":"aaaabbbbccccddddeeeeffff00001111", "STRIPE_PUB_KEY":"22223333444455556666777788889999", "PAYPAL_MERCHANT":"paypal-merchant@gmail.com", diff --git a/package.json b/package.json index 338485de34..172b6e48c9 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,10 @@ "passport": "~0.1.18", "passport-facebook": "~1.0.2", "newrelic": "~1.3.0", - "connect-ratelimit": "0.0.6" + "connect-ratelimit": "0.0.6", + "winston": "~0.7.2", + "winston-mail": "~0.2.7", + "winston-newrelic": "~0.1.4" }, "private": true, "subdomain": "habitrpg", diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index 3c66eabdcd..d1f97a8a08 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -386,7 +386,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' } $scope.questAbort = function(){ - if (!confirm("Are you sure you want to abort this mission? It will abort it for everyone in your party, and you'll lose your quest scroll.")) return; + if (!confirm("Are you sure you want to abort this mission? It will abort it for everyone in your party, all progress will be lost.")) return; if (!confirm("Are you double sure? Make sure they won't hate you forever!")) return; $rootScope.party.$questAbort(); } diff --git a/src/controllers/challenges.js b/src/controllers/challenges.js index c0be3cc8e6..81f4e4e2c7 100644 --- a/src/controllers/challenges.js +++ b/src/controllers/challenges.js @@ -7,6 +7,7 @@ var shared = require('habitrpg-shared'); var User = require('./../models/user').model; var Group = require('./../models/group').model; var Challenge = require('./../models/challenge').model; +var logging = require('./../logging'); var csv = require('express-csv'); var api = module.exports; @@ -214,7 +215,7 @@ api.update = function(req, res){ // 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){ - console.log('Challenge updated, sync to subscribers'); + logging.info('Challenge updated, sync to subscribers'); if (err) throw err; _.each(users, function(user){ saved.syncToUser(user); diff --git a/src/controllers/user.js b/src/controllers/user.js index f0f1f72cbe..a44f5937da 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -12,6 +12,7 @@ var sanitize = validator.sanitize; var User = require('./../models/user').model; var Group = require('./../models/group').model; var Challenge = require('./../models/challenge').model; +var logging = require('./../logging'); var acceptablePUTPaths; var api = module.exports; @@ -349,7 +350,7 @@ api.buyGemsPaypalIPN = function(req, res, next) { user.balance += 5; //user.purchased.ads = true; user.save(); - console.log('PayPal transaction completed and user updated'); + logging.info('PayPal transaction completed and user updated'); }); } }); @@ -518,4 +519,4 @@ api.batchUpdate = function(req, res, next) { res.json(200, {_v: response._v}); } }); -}; \ No newline at end of file +}; diff --git a/src/logging.js b/src/logging.js new file mode 100644 index 0000000000..f8e56da7cf --- /dev/null +++ b/src/logging.js @@ -0,0 +1,49 @@ +var nconf = require('nconf'); +var winston = require('winston'); +require('winston-mail').Mail; +require('winston-newrelic'); + +var logger; + +if (logger == null) { + logger = new (winston.Logger)({}); + if (nconf.get('NODE_ENV') == 'production') { + logger.add(winston.transports.newrelic, {}); + logger.add(winston.transports.Mail, { + to: nconf.get('ADMIN_EMAIL') || nconf.get('SMTP_USER'), + from: "HabitRPG <" + nconf.get('SMTP_USER') + ">", + subject: "HabitRPG Error", + host: nconf.get('SMTP_HOST'), + port: nconf.get('SMTP_PORT'), + tls: nconf.get('SMTP_TLS'), + username: nconf.get('SMTP_USER'), + password: nconf.get('SMTP_PASS'), + level: 'error' + }); + } else { + logger.add(winston.transports.Console, {colorize:true}); + logger.add(winston.transports.File, {filename: 'habitrpg.log'}); + } +} + +// A custom log function that wraps Winston. Makes it easy to instrument code +// and still possible to replace Winston in the future. +module.exports.log = function(/* variable args */) { + if (logger) + logger.log.apply(logger, arguments); +}; + +module.exports.info = function(/* variable args */) { + if (logger) + logger.info.apply(logger, arguments); +}; + +module.exports.warn = function(/* variable args */) { + if (logger) + logger.warn.apply(logger, arguments); +}; + +module.exports.error = function(/* variable args */) { + if (logger) + logger.error.apply(logger, arguments); +}; diff --git a/src/middleware.js b/src/middleware.js index 19ada70aaf..781b6f7202 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -4,6 +4,7 @@ var fs = require('fs'); var path = require('path'); var User = require('./models/user').model var limiter = require('connect-ratelimit'); +var logging = require('./logging'); module.exports.apiThrottle = function(app) { app.use(limiter({ @@ -16,7 +17,7 @@ module.exports.apiThrottle = function(app) { } } })).use(function(req,res,next){ - //console.log(res.ratelimit); + //logging.info(res.ratelimit); if (res.ratelimit.exceeded) return res.json(429,{err:'Rate limit exceeded'}); next(); }); @@ -92,15 +93,15 @@ var getManifestFiles = function(page){ var css = ''; _.each(files.css, function(file){ - css += ''; + css += ''; }); if(nconf.get('NODE_ENV') === 'production'){ - return css + ''; + return css + ''; }else{ var results = css; _.each(files.js, function(file){ - results += ''; + results += ''; }); return results; } @@ -184,16 +185,16 @@ var getUserLanguage = function(req, callback){ } }); }else{ - return callback(null, _.find(avalaibleLanguages, {code: getFromBrowser()})); + return callback(null, _.find(avalaibleLanguages, {code: getFromBrowser()})); } } module.exports.locals = function(req, res, next) { getUserLanguage(req, function(err, language){ - if(err) return res.json(500, {err: err}); + if(err) return res.json(500, {err: err}); language.momentLang = (momentLangs[language.code] || undefined); - + res.locals.habitrpg = { NODE_ENV: nconf.get('NODE_ENV'), BASE_URL: nconf.get('BASE_URL'), @@ -209,11 +210,11 @@ module.exports.locals = function(req, res, next) { var string = translations[language.code][stringName]; if(!string) return _.template(translations[language.code].stringNotFound, {string: stringName}); - return vars === undefined ? string : _.template(string, vars); + return vars === undefined ? string : _.template(string, vars); }, siteVersion: siteVersion } - next(); + next(); }); -} \ No newline at end of file +} diff --git a/src/routes/apiv1.js b/src/routes/apiv1.js index 53207631d2..ce09e8a24a 100644 --- a/src/routes/apiv1.js +++ b/src/routes/apiv1.js @@ -6,6 +6,7 @@ var icalendar = require('icalendar'); var api = require('./../controllers/user'); var auth = require('./../controllers/auth'); var middleware = require('../middleware'); +var logging = require('./../logging'); /* ---------- Deprecated API ------------*/ @@ -78,7 +79,7 @@ var batchUpdate = function(req, res, next) { req.body = action.data; res.send = res.json = function(code, data) { if (_.isNumber(code) && code >= 400) { - console.error({ + logging.error({ code: code, data: data }); @@ -168,4 +169,4 @@ router.get('*', deprecated); router.post('*', deprecated); router.put('*', deprecated); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/src/seed.js b/src/seed.js index becd3cf94b..111dfee1df 100644 --- a/src/seed.js +++ b/src/seed.js @@ -1,6 +1,7 @@ require('coffee-script') // for habitrpg-shared var nconf = require('nconf'); var utils = require('./utils'); +var logging = require('./logging'); utils.setupConfig(); var async = require('async'); var mongoose = require('mongoose'); @@ -15,7 +16,7 @@ async.waterfall([ Group.findById('habitrpg', cb); }, function(tavern, cb){ - console.log({tavern:tavern,cb:cb}); + logging.info({tavern:tavern,cb:cb}); if (!tavern) { tavern = new Group({ _id: 'habitrpg', @@ -31,6 +32,6 @@ async.waterfall([ } ],function(err){ if (err) throw err; - console.log("Done initializing database"); + logging.info("Done initializing database"); mongoose.disconnect(); }) diff --git a/src/server.js b/src/server.js index 8d95852ad0..aa734c7d3a 100644 --- a/src/server.js +++ b/src/server.js @@ -4,7 +4,7 @@ var _ = require('lodash'); var nconf = require('nconf'); var utils = require('./utils'); utils.setupConfig(); - +var logging = require('./logging'); var isProd = nconf.get('NODE_ENV') === 'production'; var isDev = nconf.get('NODE_ENV') === 'development'; @@ -12,11 +12,11 @@ if (cluster.isMaster && (isDev || isProd)) { // Fork workers. _.times(require('os').cpus().length, function(){ cluster.fork(); - }) + }); - cluster.on('exit', function(worker, code, signal) { + cluster.on('disconnect', function(worker, code, signal) { var w = cluster.fork(); // replace the dead worker - console.error('[%s] [master:%s] worker:%s disconnect! new worker:%s fork', new Date(), process.pid, worker.process.pid, w.process.pid); + logging.info('[%s] [master:%s] worker:%s disconnect! new worker:%s fork', new Date(), process.pid, worker.process.pid, w.process.pid); }); } else { @@ -40,7 +40,7 @@ if (cluster.isMaster && (isDev || isProd)) { require('./models/challenge'); mongoose.connect(nconf.get('NODE_DB_URI'), {auto_reconnect:true}, function(err) { if (err) throw err; - console.info('Connected with Mongoose'); + logging.info('Connected with Mongoose'); }); @@ -135,14 +135,15 @@ if (cluster.isMaster && (isDev || isProd)) { app.use('/api/v1', require('./routes/apiv1').middleware); app.use('/export', require('./routes/dataexport').middleware); + app.use(utils.crashWorker(server)); app.use(utils.errorHandler); require('./routes/apiv2.coffee')(swagger, v2); server.on('request', app); server.listen(app.get("port"), function() { - return console.log("Express server listening on port " + app.get("port")); + return logging.info("Express server listening on port " + app.get("port")); }); module.exports = server; -} \ No newline at end of file +} diff --git a/src/utils.js b/src/utils.js index eacb9e7c59..4ff8f2a895 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,18 +2,20 @@ var nodemailer = require('nodemailer'); var nconf = require('nconf'); var crypto = require('crypto'); var path = require("path"); +var cluster = require("cluster"); module.exports.sendEmail = function(mailData) { var smtpTransport = nodemailer.createTransport("SMTP",{ - service: nconf.get('SMTP_SERVICE'), + service: nconf.get('SMTP_SERVICE'), auth: { - user: nconf.get('SMTP_USER'), - pass: nconf.get('SMTP_PASS') + user: nconf.get('SMTP_USER'), + pass: nconf.get('SMTP_PASS') } }); smtpTransport.sendMail(mailData, function(error, response){ - if(error) console.log(error); - else console.log("Message sent: " + response.message); + var logging = require('./logging'); + if(error) logging.error(error); + else logging.info("Message sent: " + response.message); smtpTransport.close(); // shut down the connection pool, no more messages }); } @@ -48,7 +50,8 @@ module.exports.setupConfig = function(){ // // * https://developers.google.com/chrome-developer-tools/docs/heap-profiling // // * https://developers.google.com/chrome-developer-tools/docs/memory-analysis-101 // agent = require('webkit-devtools-agent'); -// console.log("To debug memory leaks:" + +// var logging = require('./logging'); +// logging.info("To debug memory leaks:" + // "\n\t(1) Run `kill -SIGUSR2 " + process.pid + "`" + // "\n\t(2) open http://c4milo.github.com/node-webkit-agent/21.0.1180.57/inspector.html?host=localhost:1337&page=0"); // } @@ -59,6 +62,23 @@ module.exports.setupConfig = function(){ if (nconf.get('NODE_ENV') === 'production') require('newrelic'); }; +module.exports.crashWorker = function(server) { + return function(err, req, res, next) { + if (!cluster.isMaster) { + // make sure we close down within 30 seconds + var killtimer = setTimeout(function() { + process.exit(1); + }, 30000); + // But don't keep the process open just for that! + killtimer.unref(); + // stop taking new requests. + server.close(); + cluster.worker.disconnect(); + } + next(err); + }; +} + module.exports.errorHandler = function(err, req, res, next) { // when we hit an error, send it to admin as an email. If no ADMIN_EMAIL is present, just send it to yourself (SMTP_USER) @@ -69,15 +89,9 @@ module.exports.errorHandler = function(err, req, res, next) { "\n\nheaders: " + JSON.stringify(req.headers) + "\n\nbody: " + JSON.stringify(req.body) + (res.locals.ops ? "\n\ncompleted ops: " + JSON.stringify(res.locals.ops) : ""); - module.exports.sendEmail({ - from: "HabitRPG <" + nconf.get('SMTP_USER') + ">", - to: nconf.get('ADMIN_EMAIL') || nconf.get('SMTP_USER'), - subject: "HabitRPG Error", - text: stack - }); - console.error(stack); + var logging = require('./logging'); + logging.error(stack); var message = err.message ? err.message : err; message = (message.length < 200) ? message : message.substring(0,100) + message.substring(message.length-100,message.length); res.json(500,{err:message}); //res.end(err.message); - process.exit(0); -} \ No newline at end of file +} diff --git a/views/options/inventory/stable.jade b/views/options/inventory/stable.jade index 328be90ebb..d85fc484fc 100644 --- a/views/options/inventory/stable.jade +++ b/views/options/inventory/stable.jade @@ -54,7 +54,7 @@ script(type='text/ng-template', id='partials/options.inventory.pets.html') menu div(ng-repeat='potion in Content.hatchingPotions', popover-trigger='mouseenter', popover='{{potion.text}} {{egg.text}}', popover-placement='bottom', ng-init='pet = egg.key+"-"+potion.key') button(class="pet-button Pet-{{pet}}", ng-if='user.items.pets[pet]>0', ng-class='{active: user.items.currentPet == pet, selectableInventory: selectedFood}', ng-click='choosePet(egg.key, potion.key)') - .progress(ng-class='{"progress-success": user.items.pets[pet]<50}') + .progress(ng-show='!user.items.mounts[pet]', ng-class='{"progress-success": user.items.pets[pet]<50}') .bar(style="width: {{user.items.pets[pet]/.5}}%;") button(class="pet-button pet-not-owned", ng-if='!user.items.pets[pet]') .PixelPaw diff --git a/views/options/profile.jade b/views/options/profile.jade index f8e9775251..e72ef230a9 100644 --- a/views/options/profile.jade +++ b/views/options/profile.jade @@ -30,22 +30,15 @@ script(id='partials/options.profile.avatar.html', type='text/ng-template') | 2 / = ' ' + env.t('locked') menu(type='list') + // For special events code, see commit dfa27b3 + // Color li.customize-menu menu(label=env.t('color')) each v,k in {'c8c8c8':'white','903a00':'brown','cfb853':'blond','ec720f':'red','2e2e2e':'black'} button(type='button', class='customize-option', style='width: 40px; height: 40px; background-color:##{k};', ng-click='set({"preferences.hair.color": "#{v}"})') - - // Special Events - li.customize-menu.well.limited-edition - .label.label-info.pull-right(popover=env.t('limited31Jan'), popover-title=env.t('limitedEdition'), popover-placement='right', popover-trigger='mouseenter') - =env.t('limitedEdition') - | - span.glyphicon.glyphicon-question-sign - menu(label=env.t('winterColors')) each color in ['candycane','frost','winternight','holly'] - button(type='button', ng-class='{locked: !user.purchased.hair.color.#{color}}', class='customize-option hair_bangs_1_#{color}', style='width: 40px; height: 40px;', ng-click='unlock("hair.color.#{color}")') - button.btn.btn-small.btn-primary(ng-hide='user.purchased.hair.color.candycane && user.purchased.hair.color.frost && user.purchased.hair.color.winternight && user.purchased.hair.color.holly', ng-click='unlock("hair.color.candycane,hair.color.frost,hair.color.winternight,hair.color.holly")')!= env.t('unlockSet5') + ' ' + button(type='button', ng-if='user.purchased.hair.color.#{color}', class='customize-option hair_bangs_1_#{color}', style='width: 40px; height: 40px;', ng-click='unlock("hair.color.#{color}")') h5=env.t('bodyHair') // Bangs @@ -160,22 +153,23 @@ script(id='partials/options.profile.stats.html', type='text/ng-template') span.glyphicon.glyphicon-download   =env.t('distributePoints') - tr - td= env.t('allocateStr') + ' {{user.stats.str}}' - td - a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("str")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocateStrPop')) + tr td= env.t('allocateCon') + ' {{user.stats.con}}' td a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("con")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocateConPop')) + + tr + td= env.t('allocateInt') + ' {{user.stats.int}}' + td + a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("int")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocateIntPop')) + tr td= env.t('allocatePer') + ' {{user.stats.per}}' td a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("per")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocatePerPop')) + tr - td= env.t('allocateInt') + ' {{user.stats.int}}' + td= env.t('allocateStr') + ' {{user.stats.str}}' td - a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("int")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocateIntPop')) + + a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("str")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocateStrPop')) + + div(ng-class='user.flags.classSelected && !user.preferences.disableClasses ? "span4" : "span6"') include ../shared/profiles/achievements diff --git a/views/shared/profiles/stats.jade b/views/shared/profiles/stats.jade index 30b092e03d..11ffcb9316 100644 --- a/views/shared/profiles/stats.jade +++ b/views/shared/profiles/stats.jade @@ -63,15 +63,16 @@ table.table.table-striped |: {{profile.stats.buffs[k] || 0}}  tr(ng-if='profile.stats.buffs.stealth') td + i.icon-question-sign(popover-title=env.t('stealth'), popover-trigger='mouseenter', popover-placement='right', popover=env.t('stealthNewDay')) +   strong=env.t('stealth') - |: {{profile.stats.buffs.stealth}}  - span.glyphicon.glyphicon-question-sign(popover-title=env.t('stealth'), popover-trigger='mouseenter', popover-placement='right', popover=env.t('stealthNewDay')) + |: {{profile.stats.buffs.stealth}} td tr(ng-if='profile.stats.buffs.streaks') td + i.icon-question-sign(popover-title=env.t('streaksFrozen'), popover-trigger='mouseenter', popover-placement='right', popover=env.t('streaksFrozenText')) +   strong=env.t('streaksFrozen') - | - span.glyphicon.glyphicon-question-sign(popover-title=env.t('streaksFrozen'), popover-trigger='mouseenter', popover-placement='right', popover=env.t('streaksFrozenText')) td h4(ng-show='user.flags.dropsEnabled')=env.t('pets') diff --git a/views/shared/tasks/task.jade b/views/shared/tasks/task.jade index e1fddad6d3..7fbf934489 100644 --- a/views/shared/tasks/task.jade +++ b/views/shared/tasks/task.jade @@ -17,7 +17,7 @@ li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s a.badge(ng-if='task.checklist[0]', ng-class='{"badge-success":checklistCompletion(task.checklist) == task.checklist.length}', ng-click='collapseChecklist(task)', style='margin:1px') {{checklistCompletion(task.checklist)}}/{{task.checklist.length}} |   - span.glyphicon.glyphicon-tags(tooltip='{{appliedTags(user.tags, task.tags)}}', ng-hide='Shared.noTags(task.tags)') + span.glyphicon.glyphicon-tags(tooltip='{{Shared.appliedTags(user.tags, task.tags)}}', ng-hide='Shared.noTags(task.tags)') // edit a(ng-hide='task._editing', ng-click='editTask(task)', tooltip=env.t('edit')) |