diff --git a/migrations/mysteryitems.js b/migrations/mysteryitems.js index 473b2ff43d..5b2cf2ce33 100644 --- a/migrations/mysteryitems.js +++ b/migrations/mysteryitems.js @@ -2,7 +2,7 @@ var _id = ''; var update = { $push: { 'purchased.plan.mysteryItems':{ - $each:['head_mystery_201406','armor_mystery_201406'] + $each:['head_mystery_201407','armor_mystery_201407'] } } }; diff --git a/package.json b/package.json index 9445060c7f..a30fed03cf 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "karma-ng-html2js-preprocessor": "~0.1.0", "karma-chai-plugins": "~0.1.0", "mocha": "~1.12.1", - "karma-mocha": "~0.1.0", + "karma-mocha": "0.1.3", "csv": "~0.3.6", "mongoskin": "~0.6.1", "expect.js": "~0.2.0", diff --git a/public/js/app.js b/public/js/app.js index d0fa54dc41..1fa78a24df 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -228,7 +228,12 @@ window.habitrpg = angular.module('habitrpg', } else if (response.needRefresh) { $rootScope.$broadcast('responseError', "The site has been updated and the page needs to refresh. The last action has not been recorded, please refresh and try again."); - // 400 range? + } else if (response.data.code && response.data.code === 'ACCOUNT_SUSPENDED') { + confirm(response.data.err); + localStorage.clear(); + window.location.href = '/logout'; + + // 400 range? } else if (response < 500) { $rootScope.$broadcast('responseText', response.data.err || response.data); diff --git a/public/js/controllers/rootCtrl.js b/public/js/controllers/rootCtrl.js index 7d5dd189b4..15b3a994f2 100644 --- a/public/js/controllers/rootCtrl.js +++ b/public/js/controllers/rootCtrl.js @@ -28,6 +28,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ $rootScope.env = window.env; $rootScope.Math = Math; $rootScope.Groups = Groups; + $rootScope.toJson = angular.toJson; // Angular UI Router $rootScope.$state = $state; @@ -82,6 +83,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ scope: options.scope, // optional keyboard: (options.keyboard === undefined ? true : options.keyboard), // optional backdrop: (options.backdrop === undefined ? true : options.backdrop) // optional + }); } @@ -121,6 +123,26 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ }); } + $rootScope.showStripeEdit = function(){ + StripeCheckout.open({ + key: window.env.STRIPE_PUB_KEY, + address: false, + name: 'Update', + description: 'Update the card to be charged for your subscription', + panelLabel: 'Update Card', + token: function(data) { + var url = '/stripe/subscribe/edit?plan=basic_earned'; + $scope.$apply(function(){ + $http.post(url, data).success(function() { + window.location.reload(true); + }).error(function(err) { + alert(err); + }); + }) + } + }); + } + $rootScope.cancelSubscription = function(){ if (!confirm(window.env.t('sureCancelSub'))) return; window.location.href = '/' + user.purchased.plan.paymentMethod.toLowerCase() + '/subscribe/cancel?_id=' + user._id + '&apiToken=' + user.apiToken; diff --git a/public/js/controllers/settingsCtrl.js b/public/js/controllers/settingsCtrl.js index 5bbb13be86..bff15f4911 100644 --- a/public/js/controllers/settingsCtrl.js +++ b/public/js/controllers/settingsCtrl.js @@ -65,6 +65,20 @@ habitrpg.controller('SettingsCtrl', $rootScope.$state.go('tasks'); } + $scope.changeUsername = function(changeUser){ + if (!changeUser.newUsername || !changeUser.password) { + return alert(window.env.t('fillAll')); + } + $http.post(API_URL + '/api/v2/user/change-username', changeUser) + .success(function(){ + alert(window.env.t('usernameSuccess')); + $scope.changeUser = {}; + }) + .error(function(data){ + alert(data); + }); + } + $scope.changePassword = function(changePass){ if (!changePass.oldPassword || !changePass.newPassword || !changePass.confirmNewPassword) { return alert(window.env.t('fillAll')); diff --git a/src/controllers/auth.js b/src/controllers/auth.js index 4e8b3cb34c..ac5bac6696 100644 --- a/src/controllers/auth.js +++ b/src/controllers/auth.js @@ -14,6 +14,12 @@ var api = module.exports; var NO_TOKEN_OR_UID = { err: "You must include a token and uid (user id) in your request"}; var NO_USER_FOUND = {err: "No user found."}; var NO_SESSION_FOUND = { err: "You must be logged in." }; +var accountSuspended = function(uuid){ + return { + err: 'Account has been suspended, please contact leslie@habitrpg.com with your UUID ('+uuid+') for assistance.', + code: 'ACCOUNT_SUSPENDED' + }; +} api.auth = function(req, res, next) { var uid = req.headers['x-api-user']; @@ -22,6 +28,7 @@ api.auth = function(req, res, next) { 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; @@ -121,9 +128,10 @@ api.loginLocal = function(req, res, next) { var username = req.body.username; var password = req.body.password; if (!(username && password)) return res.json(401, {err:'Missing :username or :password in request body, please provide both'}); - User.findOne({'auth.local.username': username}, function(err, user){ + User.findOne({'auth.local.username': username}, {auth:1}, function(err, user){ if (err) return next(err); if (!user) return res.json(401, {err:"Username or password incorrect. Click 'Forgot Password' for help with either. (Note: usernames are case-sensitive)"}); + 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({ 'auth.local.username': username, @@ -144,30 +152,17 @@ api.loginLocal = function(req, res, next) { api.loginFacebook = function(req, res, next) { var email, facebook_id, name, _ref; _ref = req.body, facebook_id = _ref.facebook_id, email = _ref.email, name = _ref.name; - if (!facebook_id) { - return res.json(401, { - err: 'No facebook id provided' - }); - } - return User.findOne({ - 'auth.facebook.id': facebook_id - }, function(err, user) { + if (!facebook_id) + return res.json(401, {err: 'No facebook id provided'}); + User.findOne({'auth.facebook.id': facebook_id}, function(err, user) { if (err) { - return res.json(401, { - err: err - }); - } - if (user) { - return res.json(200, { - id: user.id, - token: user.apiToken - }); + return res.json(401, {err: err}); + } else if (user) { + if (user.auth.blocked) return res.json(401, accountSuspended(user._id)); + return res.json(200, {id: user.id,token: user.apiToken}); } else { /* FIXME: create a new user instead*/ - - return res.json(403, { - err: "Please register with Facebook on https://habitrpg.com, then come back here and log in." - }); + return res.json(403, {err: "Please register with Facebook on https://habitrpg.com, then come back here and log in."}); } }); }; @@ -195,6 +190,31 @@ api.resetPassword = function(req, res, next){ }); }; +api.changeUsername = function(req, res, next) { + var user = res.locals.user, + password = req.body.password + newUsername = req.body.newUsername; + + User.findOne({'auth.local.username': newUsername}, function(err, result) { + if (err) next(err); + + if(result) + return res.json(401, {err: "Username already taken"}); + + var salt = user.auth.local.salt, + hashed_password = utils.encryptPassword(password, salt); + + if (hashed_password !== user.auth.local.hashed_password) + return res.json(401, {err:"Incorrect password"}); + + user.auth.local.username = newUsername; + user.save(function(err, saved){ + if (err) next(err); + res.send(200); + }) + }); +} + api.changePassword = function(req, res, next) { var user = res.locals.user, oldPassword = req.body.oldPassword, diff --git a/src/controllers/coupon.js b/src/controllers/coupon.js index 6e332f4093..233be59078 100644 --- a/src/controllers/coupon.js +++ b/src/controllers/coupon.js @@ -20,11 +20,11 @@ api.generateCoupons = function(req,res,next) { api.getCoupons = function(req,res,next) { var options = {sort:'seq'}; if (req.query.limit) options.limit = req.query.limit; - if (req.query.skip) options.limit = req.query.skip; + 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 ['HabitRPG - To redeem your code, go to goo.gl/azsGaH and enter '+c._id]; + return [c._id]; }))); }); } diff --git a/src/controllers/hall.js b/src/controllers/hall.js index 0edbd9e867..d3a2796476 100644 --- a/src/controllers/hall.js +++ b/src/controllers/hall.js @@ -38,7 +38,8 @@ api.getPatrons = function(req,res,next){ api.getHero = function(req,res,next) { User.findById(req.params.uid) - .select('contributor balance profile.name purchased') + .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'}); @@ -61,6 +62,12 @@ api.updateHero = function(req,res,next) { 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){ diff --git a/src/controllers/payments.js b/src/controllers/payments.js index 86b45bcbdf..fb3f5cdf81 100644 --- a/src/controllers/payments.js +++ b/src/controllers/payments.js @@ -142,6 +142,32 @@ api.stripeSubscribeCancel = function(req, res, next) { }); } +api.stripeSubscribeEdit = function(req, res, next) { + var stripe = require("stripe")(nconf.get('STRIPE_API_KEY')); + var token = req.body.id; + var user = res.locals.user; + var user_id = user.purchased.plan.customerId; + var sub_id; + + async.waterfall([ + function(cb){ + stripe.customers.listSubscriptions(user_id, cb); + }, + function(response, cb) { + sub_id = response.data[0].id; + console.warn(sub_id); + console.warn([user_id, sub_id, { card: token }]); + stripe.customers.updateSubscription(user_id, sub_id, { card: token }, cb); + }, + function(response, cb) { + user.save(cb); + } + ], function(err, saved){ + if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors + res.send(200); + }); +} + api.paypalSubscribe = function(req,res,next) { var uuid = res.locals.user._id; // Authenticate a future subscription of ~5 USD diff --git a/src/controllers/user.js b/src/controllers/user.js index b43f259240..4988fff71c 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -29,6 +29,13 @@ api.getContent = function(req, res, next) { 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 diff --git a/src/logging.js b/src/logging.js index f8e56da7cf..5e1449db3d 100644 --- a/src/logging.js +++ b/src/logging.js @@ -9,17 +9,19 @@ 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' - }); + if (!nconf.get('DISABLE_ERROR_EMAILS')) { + 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'}); diff --git a/src/models/user.js b/src/models/user.js index 4dd29c35ec..fbae36106b 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -46,6 +46,7 @@ var UserSchema = new Schema({ valentine: Number }, auth: { + blocked: Boolean, facebook: Schema.Types.Mixed, local: { email: String, @@ -405,6 +406,7 @@ UserSchema.pre('save', function(next) { //our own version incrementer this._v++; + next(); }); diff --git a/src/routes/apiv2.coffee b/src/routes/apiv2.coffee index c126702d13..d40ca53e1e 100644 --- a/src/routes/apiv2.coffee +++ b/src/routes/apiv2.coffee @@ -46,6 +46,10 @@ module.exports = (swagger, v2) -> ] action: user.getContent + '/content/paths': + spec: + description: "Show user model tree" + action: user.getModelPaths "/export/history": spec: diff --git a/src/routes/auth.js b/src/routes/auth.js index edc54bc81d..616f9cf7d6 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -10,6 +10,7 @@ router.post('/api/v2/user/auth/local', i18n.getUserLanguage, auth.loginLocal); router.post('/api/v2/user/auth/facebook', i18n.getUserLanguage, auth.loginFacebook); router.post('/api/v2/user/reset-password', i18n.getUserLanguage, auth.resetPassword); router.post('/api/v2/user/change-password', i18n.getUserLanguage, auth.auth, auth.changePassword); +router.post('/api/v2/user/change-username', i18n.getUserLanguage, auth.auth, auth.changeUsername); router.post('/api/v1/register', i18n.getUserLanguage, auth.registerUser); router.post('/api/v1/user/auth/local', i18n.getUserLanguage, auth.loginLocal); diff --git a/src/routes/payments.js b/src/routes/payments.js index c290e79b73..c34d77b24b 100644 --- a/src/routes/payments.js +++ b/src/routes/payments.js @@ -13,6 +13,7 @@ router.get('/paypal/subscribe/cancel', auth.authWithUrl, i18n.getUserLanguage, p router.post('/paypal/ipn', i18n.getUserLanguage, payments.paypalIPN); // misc ipn handling router.post("/stripe/checkout", auth.auth, i18n.getUserLanguage, payments.stripeCheckout); +router.post("/stripe/subscribe/edit", auth.auth, i18n.getUserLanguage, payments.stripeSubscribeEdit) //router.get("/stripe/subscribe", auth.authWithUrl, i18n.getUserLanguage, payments.stripeSubscribe); // checkout route is used (above) with ?plan= instead router.get("/stripe/subscribe/cancel", auth.authWithUrl, i18n.getUserLanguage, payments.stripeSubscribeCancel); diff --git a/views/options/settings.jade b/views/options/settings.jade index e82bb5aa31..12fa09d5dc 100644 --- a/views/options/settings.jade +++ b/views/options/settings.jade @@ -83,6 +83,18 @@ script(type='text/ng-template', id='partials/options.settings.settings.html') input.form-control(type='text', ng-model='_couponCode', placeholder='Enter Coupon Code') button.btn.btn-primary(type='submit') Submit + div(ng-if='user.auth.local.username') + .panel.panel-default + .panel-heading + span=env.t('changeUsername') + .panel-body + form(ng-submit='changeUsername(changeUser)', ng-show='user.auth.local') + .form-group + input.form-control(type='text', placeholder=env.t('newUsername'), ng-model='changeUser.newUsername', required) + .form-group + input.form-control(type='password', placeholder=env.t('password'), ng-model='changeUser.password', required) + input.btn.btn-default(type='submit', value=env.t('submit')) + //- Why is ng-if='user.auth.local' validating for users *without* user.auth.local (facebook users)? adding .username here for extra div(ng-if='user.auth.local.username') .panel.panel-default @@ -207,4 +219,5 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template') |   span.glyphicon.glyphicon-ok div(ng-include="'partials/options.settings.subscription.perks.html'") - .btn.btn-sm.btn-danger(ng-if='!user.purchased.plan.dateTerminated', ng-click='cancelSubscription()')=env.t('cancelSub') \ No newline at end of file + .btn.btn-primary(ng-if=':: !user.purchased.plan.dateTerminated && user.purchased.plan.paymentMethod=="Stripe"', ng-click='showStripeEdit()') Update Card + .btn.btn-sm.btn-danger(ng-if=':: !user.purchased.plan.dateTerminated', ng-click='cancelSubscription()')=env.t('cancelSub') \ No newline at end of file diff --git a/views/options/social/group.jade b/views/options/social/group.jade index 6727f89eb3..78092b630a 100644 --- a/views/options/social/group.jade +++ b/views/options/social/group.jade @@ -3,7 +3,7 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title=env.t('guild span.task-action-btn.tile.flush.neutral .Pet_Currency_Gem2x.Gems | {{group.balance * 4 | number:0 }} - =env.t('guildGems') + =' ' + env.t('guildGems') .container-fluid .row diff --git a/views/options/social/hall.jade b/views/options/social/hall.jade index 9d348ca7b9..59c6235ceb 100644 --- a/views/options/social/hall.jade +++ b/views/options/social/hall.jade @@ -37,24 +37,41 @@ script(type='text/ng-template', id='partials/options.social.hall.heroes.html') label input(type='checkbox', ng-model='hero.contributor.admin') =env.t('admin') + hr - hr + .form-group + label=env.t('balance') + input.form-control(type='number', step="any", ng-model='hero.balance') + small!= '`user.balance`' + env.t('notGems') + .form-group + .checkbox + label + input(type='checkbox', ng-model='hero.purchased.ads') + =env.t('hideAds') + accordion + accordion-group(heading='Items') + h4 Update Item + .form-group.well + input.form-control(type='text',placeholder='Path (eg, items.pets.BearCub-Base)',ng-model='hero.itemPath') + small.muted Enter the item path. E.g., items.pets.BearCub-Zombie or items.gear.owned.head_special_0 or items.gear.equipped.head. See all paths here. When in doubt, ask Tyler. + br + input.form-control(type='text',placeholder='Value (eg, 5)',ng-model='hero.itemVal') + small.muted Enter the item value. E.g., 5 or false or head_warrior_3 (respectively from above examples). + h4 Current Items + pre {{::toJson(hero.items,true)}} + accordion-group(heading='Auth') + h4 Auth + pre {{::toJson(hero.auth)}} + .form-group + .checkbox + label + input(type='checkbox', ng-model='hero.auth.blocked') + | Blocked - .form-group - label=env.t('balance') - input.form-control(type='number', step="any", ng-model='hero.balance') - small!= '`user.balance`' + env.t('notGems') - - .form-group - .checkbox - label - input(type='checkbox', ng-model='hero.purchased.ads') - =env.t('hideAds') - - // h4 Backer Status - // Add backer stuff like tier, disable adds, etcs - .form-group - input.form-control.btn.btn-primary(type='submit')=env.t('save') + // h4 Backer Status + // Add backer stuff like tier, disable adds, etcs + .form-group + input.form-control.btn.btn-primary(type='submit')=env.t('save') table.table.table-striped diff --git a/views/shared/modals/settings.jade b/views/shared/modals/settings.jade index dc453d9662..b5728fb551 100644 --- a/views/shared/modals/settings.jade +++ b/views/shared/modals/settings.jade @@ -8,7 +8,7 @@ script(type='text/ng-template', id='modals/reset.html') button.btn.btn-default(ng-click='$close();')=env.t('neverMind') button.btn.btn-danger(ng-click='$close(); reset()')=env.t('resetDo') -script(type='text/ng-template', id='modals/restore.html') +script(type='text/ng-template', id='modals/restore.html') .modal-header h4=env.t('fixValues') .modal-body diff --git a/views/shared/new-stuff.jade b/views/shared/new-stuff.jade index 8f4747cd35..4fc9347087 100644 --- a/views/shared/new-stuff.jade +++ b/views/shared/new-stuff.jade @@ -11,21 +11,29 @@ table table.table.table-striped tr td - h5 Mobile App Update - p We’ve released another update to the mobile app! Now you can feed and select pets from the app. Carry your cute pets with you everywhere you go! The app is available for iOS here, and Android here. We’re continuing to release updates on a regular basis, so if you like the direction that we’ve been taking the app, please do consider leaving us a review. Thank you! - tr - td - h5 Neglect Strike: Tavern Art Swap - p The Dread Drag'on's Rage Bar has filled, and it has unleashed its Neglect Strike, leading to a new look for the Tavern! As a reminder, the Drag'on's rage will NEVER hurt any users or interfere with their ability to be productive, so the chat and inn are still functional. Even so... poor Daniel! - p All users are automatically damaging the Drag'on with their tasks. There is nothing bad that can happen to you or your account by being in this fight! - tr - td - h5 Dread Drag'on Prize Change: Food Reward! - p We've received a lot of feedback due to the weekend's confusion, and it seems that awarding GP and XP for defeating the world boss significantly unbalanced the game for newer players. Based on your feedback, XP and GP will no longer be awarded. Instead, players will receive an assortment of food! The Mantis Shrimps will still be awarded. - p If you were looking forward to receiving the 900XP and 90 GP upon completion of the battle, feel free to award it to yourself using Settings > Site > Fix Character Values when the battle is done! - p Thank you for bearing with us through the confusion. We love you guys. - small.muted 7/16/2014 + h5 July Subscriber Item + .promo_mystery_201407.pull-right + p The July Subscriber Item has been revealed: the Undersea Explorer Item Set! All July subscribers will receive the Undersea Explorer Helm and the Undersea Explorer Suit. You still have six days to subscribe and receive the item set! Thank you so much for your support - we really do rely on you to keep HabitRPG free to use and running smoothly. + small.muted 7/25/2014 +h5 7/16/2014 +table.table.table-striped + tr + td + h5 Mobile App Update + p We’ve released another update to the mobile app! Now you can feed and select pets from the app. Carry your cute pets with you everywhere you go! The app is available for iOS here, and Android here. We’re continuing to release updates on a regular basis, so if you like the direction that we’ve been taking the app, please do consider leaving us a review. Thank you! + tr + td + h5 Neglect Strike: Tavern Art Swap + p The Dread Drag'on's Rage Bar has filled, and it has unleashed its Neglect Strike, leading to a new look for the Tavern! As a reminder, the Drag'on's rage will NEVER hurt any users or interfere with their ability to be productive, so the chat and inn are still functional. Even so... poor Daniel! + p All users are automatically damaging the Drag'on with their tasks. There is nothing bad that can happen to you or your account by being in this fight! + tr + td + h5 Dread Drag'on Prize Change: Food Reward! + p We've received a lot of feedback due to the weekend's confusion, and it seems that awarding GP and XP for defeating the world boss significantly unbalanced the game for newer players. Based on your feedback, XP and GP will no longer be awarded. Instead, players will receive an assortment of food! The Mantis Shrimps will still be awarded. + p If you were looking forward to receiving the 900XP and 90 GP upon completion of the battle, feel free to award it to yourself using Settings > Site > Fix Character Values when the battle is done! + p Thank you for bearing with us through the confusion. We love you guys. +hr h5 7/12/2014 table.table.table-striped tr @@ -116,7 +124,7 @@ table.table.table-striped tr td h5 June Subscriber Item - img.pull-right(src='/bower_components/habitrpg-shared/img/sprites/spritesmith/gear/events/mystery_201406/promo_mystery_201406.png') + .pull-right.promo_mystery_201406.png p The June Subscriber Item has been revealed: the Octomage Item Set! All June subscribers will receive the Octopus Robe and the Crown of Tentacles. You still have six days to subscribe and receive the item set! Thank you so much for your support - we really do rely on you to keep HabitRPG free to use and running smoothly. h5 Mobile App Update p There's a new mobile app update available! In addition to bug fixes, there are many improvements, including a new button-based menu, tap-and-hold to edit tasks, and the return of stats and in-app avatar customization! Working on the mobile app is our biggest To-Do this summer, so expect more in the coming months. If you feel that the app is improving, we'd love it if you would take the time to give us a review and let us know what you think! @@ -185,7 +193,7 @@ table.table.table-striped tr td h5 May Mystery Outfit Revealed! - img.pull-right(src='/bower_components/habitrpg-shared/img/sprites/spritesmith/gear/events/mystery_201405/promo_mystery_201405.png') + .pull-right.promo_mystery_201405.png p The May Mystery Item Set has been revealed for all subscribers... Flame Wielder Item Set! All people who are subscribed this May will receive two items: ul li Flame of Mind (helm) @@ -262,7 +270,7 @@ table.table.table-striped tr td h5 April Mystery Outfit Revealed! - img.pull-right(src='/marketing/promos/April14SAMPLE2.png') + //-img.pull-right(src='/marketing/promos/April14SAMPLE2.png') p The April Mystery Item Set has been revealed for all subscribers... Twilight Butterfly Armor Set! All people who are subscribed this April will receive two items: ul li Twilight Butterfly Antennae @@ -317,7 +325,7 @@ table.table.table-striped tr td h5 March Mystery Item Set - img.pull-right(src='/marketing/promos/201403_Forest_Walker.png') + //-img.pull-right(src='/marketing/promos/201403_Forest_Walker.png') p The March Mystery Item Set has been revealed for all subscribers... The Forest Walker Set! All people who are subscribed this March will receive two items: Forest Walker Armor and Forest Walker Antlers! br p The antlers are a head accessory, so they can be worn with any helmet. diff --git a/views/shared/profiles/achievements.jade b/views/shared/profiles/achievements.jade index 7ea8eabee0..ff58cbcc57 100644 --- a/views/shared/profiles/achievements.jade +++ b/views/shared/profiles/achievements.jade @@ -154,3 +154,11 @@ div(ng-if='::profile.achievements.valentine') small =env.t('adoringFriendsText', {cards: "{{::profile.achievements.valentine}}"}) hr + +div(ng-if='::profile.achievements.quests.dilatory') + .achievement.achievement-dilatory + h5=env.t('achievementDilatory') + small + =env.t('achievementDilatoryText') + hr + diff --git a/views/static/api.jade b/views/static/api.jade index d9ee4e4334..d33d4e370c 100644 --- a/views/static/api.jade +++ b/views/static/api.jade @@ -80,7 +80,7 @@ html h2 Extensions / Scripts p HabitRPG has a simple API for up-scoring and down-scoring third party Habits: POST /api/v2/user/tasks/{id}/{direction} (headers x-api-user and x-api-key required). h4 Example - p curl -X POST -H "x-api-key: YOUR_API_TOKEN" -H "x-api-user: YOUR_USER_ID" https://beta.habitrpg.com/api/v2/user/tasks/productivity/up + p curl -X POST -H "x-api-key: YOUR_API_TOKEN" -H "x-api-user: YOUR_USER_ID" https://habitrpg.com/api/v2/user/tasks/productivity/up p Note: You may need to add --compressed -H "Content-Type:application/json" to your curl if you get errors. ul li POST to the URL /api/v2/user/tasks/{id}/{direction} @@ -91,7 +91,7 @@ html h2 Full API p All API requests should be prefaced by https://habitrpg.com. Every authenticated request should include two headers. Your api key (x-api-key) and your user id (x-api-user). Do not include {} braces in your header (-H 'x-api-user: a94b6d9d-6b64-43ae-856c-2c3f211bd426') h2 Requirements: - p The base-url for all routes is /api/v2. So /user actions will be at https://beta.habitrpg.com/api/v2/user/*. You need to send x-api-user and x-api-key headers for each request. + p The base-url for all routes is /api/v2. So /user actions will be at https://habitrpg.com/api/v2/*. You need to send x-api-user and x-api-key headers for each request. p For create & edit paths (PUT & POST), you'll need to know the schema of the object you're trying to create or edit. See Schema definitions here p If any of the documentation is lacking or you're having trouble with it, please post an issue to Github #message-bar.swagger-ui-wrap