diff --git a/API.md b/API.md new file mode 100644 index 0000000000..401a8569a1 --- /dev/null +++ b/API.md @@ -0,0 +1,4 @@ +## APIv2 + +## APIv1 (Deprecated) +Make sure to send `PUT /api/v1/user` request bodies as `{'set.this.path':value}` instead of `{set:{this:{path:value}}}` \ No newline at end of file diff --git a/public/js/controllers/adminCtrl.js b/public/js/controllers/adminCtrl.js index 5145d338c8..af29e301f5 100644 --- a/public/js/controllers/adminCtrl.js +++ b/public/js/controllers/adminCtrl.js @@ -2,7 +2,7 @@ habitrpg.controller("AdminCtrl", ['$scope', '$rootScope', 'User', 'Notification', 'API_URL', '$resource', function($scope, $rootScope, User, Notification, API_URL, $resource) { - var Contributor = $resource(API_URL + '/api/v1/admin/members/:uid', {uid:'@_id'}); + var Contributor = $resource(API_URL + '/api/v2/admin/members/:uid', {uid:'@_id'}); $scope.profile = undefined; $scope.loadUser = function(uuid){ diff --git a/public/js/controllers/authCtrl.js b/public/js/controllers/authCtrl.js index f1d4edecb3..b4e195d893 100644 --- a/public/js/controllers/authCtrl.js +++ b/public/js/controllers/authCtrl.js @@ -37,7 +37,7 @@ habitrpg.controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$loca if ($scope.registrationForm.$invalid) { return; } - $http.post(API_URL + "/api/v1/register", $scope.registerVals).success(function(data, status, headers, config) { + $http.post(API_URL + "/api/v2/register", $scope.registerVals).success(function(data, status, headers, config) { runAuth(data.id, data.apiToken); }).error(function(data, status, headers, config) { if (status === 0) { @@ -68,7 +68,7 @@ habitrpg.controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$loca if ($scope.useUUID) { runAuth($scope.loginUsername, $scope.loginPassword); } else { - $http.post(API_URL + "/api/v1/user/auth/local", data) + $http.post(API_URL + "/api/v2/user/auth/local", data) .success(function(data, status, headers, config) { runAuth(data.id, data.token); }).error(errorAlert); @@ -84,7 +84,7 @@ habitrpg.controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$loca } $scope.passwordReset = function(email){ - $http.post(API_URL + '/api/v1/user/reset-password', {email:email}) + $http.post(API_URL + '/api/v2/user/reset-password', {email:email}) .success(function(){ alert('New password sent.'); }) diff --git a/public/js/controllers/footerCtrl.js b/public/js/controllers/footerCtrl.js index 4f15ba55b0..543d0c4719 100644 --- a/public/js/controllers/footerCtrl.js +++ b/public/js/controllers/footerCtrl.js @@ -51,7 +51,7 @@ habitrpg.controller("FooterCtrl", ['$scope', '$rootScope', 'User', '$http', 'Not Notification.text('-1 day, remember to refresh'); } $scope.addTenGems = function(){ - $http.post(API_URL + '/api/v1/user/addTenGems').success(function(){ + $http.post(API_URL + '/api/v2/user/addTenGems').success(function(){ User.log({}); }) } diff --git a/public/js/controllers/rootCtrl.js b/public/js/controllers/rootCtrl.js index d0aa99878c..c73438c440 100644 --- a/public/js/controllers/rootCtrl.js +++ b/public/js/controllers/rootCtrl.js @@ -81,7 +81,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ panelLabel: "Checkout", token: function(data) { $scope.$apply(function(){ - $http.post("/api/v1/user/buy-gems", data) + $http.post("/api/v2/user/buy-gems", data) .success(function() { window.location.href = "/"; }).error(function(err) { @@ -158,7 +158,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ $scope.castEnd = function(target, type, $event){ if ($scope.spell.target != type) return Notification.text("Invalid target"); $scope.spell.cast(User.user, target); - $http.post('/api/v1/user/cast/' + $scope.spell.name, {target:target, type:type}).success(function(){ + $http.post('/api/v2/user/cast/' + $scope.spell.name, {target:target, type:type}).success(function(){ var msg = "You cast " + $scope.spell.text; switch (type) { case 'task': msg += ' on ' + target.text;break; diff --git a/public/js/controllers/settingsCtrl.js b/public/js/controllers/settingsCtrl.js index b4ea919b79..cfb589031a 100644 --- a/public/js/controllers/settingsCtrl.js +++ b/public/js/controllers/settingsCtrl.js @@ -54,7 +54,7 @@ habitrpg.controller('SettingsCtrl', if (!changePass.oldPassword || !changePass.newPassword || !changePass.confirmNewPassword) { return alert("Please fill out all fields"); } - $http.post(API_URL + '/api/v1/user/change-password', changePass) + $http.post(API_URL + '/api/v2/user/change-password', changePass) .success(function(){ alert("Password successfully changed"); $scope.changePass = {}; @@ -92,7 +92,7 @@ habitrpg.controller('SettingsCtrl', } $scope['delete'] = function(){ - $http['delete'](API_URL + '/api/v1/user') + $http['delete'](API_URL + '/api/v2/user') .success(function(){ localStorage.clear(); window.location.href = '/logout'; diff --git a/public/js/controllers/tasksCtrl.js b/public/js/controllers/tasksCtrl.js index 42442a3b9b..3c6649f2b8 100644 --- a/public/js/controllers/tasksCtrl.js +++ b/public/js/controllers/tasksCtrl.js @@ -64,7 +64,7 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N $scope.unlink = function(task, keep) { // TODO move this to userServices, turn userSerivces.user into ng-resource - $http.post(API_URL + '/api/v1/user/tasks/' + task.id + '/unlink?keep=' + keep) + $http.post(API_URL + '/api/v2/user/tasks/' + task.id + '/unlink?keep=' + keep) .success(function(){ User.log({}); }); diff --git a/public/js/services/authServices.js b/public/js/services/authServices.js index 4efa0efd46..d1667c959a 100644 --- a/public/js/services/authServices.js +++ b/public/js/services/authServices.js @@ -29,7 +29,7 @@ factory('Facebook', email: response.email } - $http.post(API_URL + '/api/v1/user/auth/facebook', data).success(function(data, status, headers, config) { + $http.post(API_URL + '/api/v2/user/auth/facebook', data).success(function(data, status, headers, config) { User.authenticate(data.id, data.token, function(err) { if (!err) { alert('Login successful!'); diff --git a/public/js/services/challengeServices.js b/public/js/services/challengeServices.js index f21cbc9d82..cf4adf9579 100644 --- a/public/js/services/challengeServices.js +++ b/public/js/services/challengeServices.js @@ -7,14 +7,14 @@ angular.module('challengeServices', ['ngResource']). factory('Challenges', ['API_URL', '$resource', 'User', '$q', 'Members', function(API_URL, $resource, User, $q, Members) { - var Challenge = $resource(API_URL + '/api/v1/challenges/:cid', + var Challenge = $resource(API_URL + '/api/v2/challenges/:cid', {cid:'@_id'}, { //'query': {method: "GET", isArray:false} - join: {method: "POST", url: API_URL + '/api/v1/challenges/:cid/join'}, - leave: {method: "POST", url: API_URL + '/api/v1/challenges/:cid/leave'}, - close: {method: "POST", params: {uid:''}, url: API_URL + '/api/v1/challenges/:cid/close'}, - getMember: {method: "GET", url: API_URL + '/api/v1/challenges/:cid/member/:uid'} + join: {method: "POST", url: API_URL + '/api/v2/challenges/:cid/join'}, + leave: {method: "POST", url: API_URL + '/api/v2/challenges/:cid/leave'}, + close: {method: "POST", params: {uid:''}, url: API_URL + '/api/v2/challenges/:cid/close'}, + getMember: {method: "GET", url: API_URL + '/api/v2/challenges/:cid/member/:uid'} }); //var challenges = []; diff --git a/public/js/services/groupServices.js b/public/js/services/groupServices.js index bce061f374..335e0b9d74 100644 --- a/public/js/services/groupServices.js +++ b/public/js/services/groupServices.js @@ -7,16 +7,16 @@ angular.module('groupServices', ['ngResource']). factory('Groups', ['API_URL', '$resource', '$q', function(API_URL, $resource, $q) { - var Group = $resource(API_URL + '/api/v1/groups/:gid', + var Group = $resource(API_URL + '/api/v2/groups/:gid', {gid:'@_id', messageId: '@_messageId'}, { //query: {method: "GET", isArray:false}, - postChat: {method: "POST", url: API_URL + '/api/v1/groups/:gid/chat'}, - deleteChatMessage: {method: "DELETE", url: API_URL + '/api/v1/groups/:gid/chat/:messageId'}, - join: {method: "POST", url: API_URL + '/api/v1/groups/:gid/join'}, - leave: {method: "POST", url: API_URL + '/api/v1/groups/:gid/leave'}, - invite: {method: "POST", url: API_URL + '/api/v1/groups/:gid/invite'}, - removeMember: {method: "POST", url: API_URL + '/api/v1/groups/:gid/removeMember'} + postChat: {method: "POST", url: API_URL + '/api/v2/groups/:gid/chat'}, + deleteChatMessage: {method: "DELETE", url: API_URL + '/api/v2/groups/:gid/chat/:messageId'}, + join: {method: "POST", url: API_URL + '/api/v2/groups/:gid/join'}, + leave: {method: "POST", url: API_URL + '/api/v2/groups/:gid/leave'}, + invite: {method: "POST", url: API_URL + '/api/v2/groups/:gid/invite'}, + removeMember: {method: "POST", url: API_URL + '/api/v2/groups/:gid/removeMember'} }); // Defer loading everything until they're requested diff --git a/public/js/services/memberServices.js b/public/js/services/memberServices.js index 3b375eca65..0ffea1e953 100644 --- a/public/js/services/memberServices.js +++ b/public/js/services/memberServices.js @@ -8,7 +8,7 @@ angular.module('memberServices', ['ngResource']). factory('Members', ['$rootScope', 'API_URL', '$resource', function($rootScope, API_URL, $resource) { var members = {}; - var Member = $resource(API_URL + '/api/v1/members/:uid', {uid:'@_id'}); + var Member = $resource(API_URL + '/api/v2/members/:uid', {uid:'@_id'}); var memberServices = { Member: Member, diff --git a/public/js/services/userServices.js b/public/js/services/userServices.js index 830d13b074..62972074a3 100644 --- a/public/js/services/userServices.js +++ b/public/js/services/userServices.js @@ -56,7 +56,7 @@ angular.module('userServices', []). sent.push(queue.shift()); }); - $http.post(API_URL + '/api/v1/user/batch-update', sent, {params: {data:+new Date, _v:user._v, siteVersion: $window.env && $window.env.siteVersion}}) + $http.post(API_URL + '/api/v2/user/batch-update', sent, {params: {data:+new Date, _v:user._v, siteVersion: $window.env && $window.env.siteVersion}}) .success(function (data, status, heacreatingders, config) { //make sure there are no pending actions to sync. If there are any it is not safe to apply model from server as we may overwrite user data. if (!queue.length) { diff --git a/public/js/static.js b/public/js/static.js index 4eaee31a96..8e4269fe80 100644 --- a/public/js/static.js +++ b/public/js/static.js @@ -1,6 +1,6 @@ "use strict"; -window.habitrpg = angular.module('habitrpg', ['userServices', 'chieffancypants.loadingBar']) +window.habitrpg = angular.module('habitrpg', ['notificationServices', 'userServices', 'chieffancypants.loadingBar']) .constant("API_URL", "") .constant("STORAGE_USER_ID", 'habitrpg-user') .constant("STORAGE_SETTINGS_ID", 'habit-mobile-settings') \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json index 5ec5938837..a3e0f0736d 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -72,8 +72,8 @@ "bower_components/angular-loading-bar/build/loading-bar.js", "js/static.js", - "js/services/userServices.js", "js/services/notificationServices.js", + "js/services/userServices.js", "js/controllers/authCtrl.js" ], "css": [ diff --git a/src/controllers/deprecated.js b/src/controllers/deprecated.js deleted file mode 100644 index e38e0a5f7c..0000000000 --- a/src/controllers/deprecated.js +++ /dev/null @@ -1,76 +0,0 @@ -var express = require('express'); -var router = new express.Router(); -var _ = require('lodash'); -var icalendar = require('icalendar'); -var api = require('./user'); -var auth = require('./auth'); - -/* ---------- Deprecated Paths ------------*/ - - -var deprecatedMessage = 'This API is no longer supported, see https://github.com/lefnire/habitrpg/wiki/API for new protocol'; - -router.get('/:uid/up/:score?', function(req, res) { - return res.send(500, deprecatedMessage); -}); - -router.get('/:uid/down/:score?', function(req, res) { - return res.send(500, deprecatedMessage); -}); - -router.post('/users/:uid/tasks/:taskId/:direction', function(req, res) { - return res.send(500, deprecatedMessage); -}); - -/* Redirect to new API*/ - - -var initDeprecated = function(req, res, next) { - req.headers['x-api-user'] = req.params.uid; - req.headers['x-api-key'] = req.body.apiToken; - return next(); -}; - -router.post('/v1/users/:uid/tasks/:taskId/:direction', initDeprecated, auth.auth, api.score); - -router.get('/v1/users/:uid/calendar.ics', function(req, res, next) { - return next() //disable for now - - var apiToken, model, query, uid; - uid = req.params.uid; - apiToken = req.query.apiToken; - model = req.getModel(); - query = model.query('users').withIdAndToken(uid, apiToken); - return query.fetch(function(err, result) { - var formattedIcal, ical, tasks, tasksWithDates; - if (err) { - return res.send(500, err); - } - tasks = result.get('tasks'); - /* tasks = result[0].tasks*/ - - tasksWithDates = _.filter(tasks, function(task) { - return !!task.date; - }); - if (_.isEmpty(tasksWithDates)) { - return res.send(500, "No events found"); - } - ical = new icalendar.iCalendar(); - ical.addProperty('NAME', 'HabitRPG'); - _.each(tasksWithDates, function(task) { - var d, event; - event = new icalendar.VEvent(task.id); - event.setSummary(task.text); - d = new Date(task.date); - d.date_only = true; - event.setDate(d); - ical.addComponent(event); - return true; - }); - res.type('text/calendar'); - formattedIcal = ical.toString().replace(/DTSTART\:/g, 'DTSTART;VALUE=DATE:'); - return res.send(200, formattedIcal); - }); -}); - -module.exports = router; \ No newline at end of file diff --git a/src/controllers/user.js b/src/controllers/user.js index 66ae0b5730..213e20d924 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -43,19 +43,16 @@ findTask = function(req, res) { Export it also so we can call it from deprecated.coffee */ api.score = function(req, res, next) { - var task = findTask(req,res); - if (!task) return res.json(404, {err: "No task found."}); - var id = req.params.id, direction = req.params.direction, user = res.locals.user, task; // Send error responses for improper API call - if (!id) return res.json(500, {err: ':id required'}); + if (!id) return res.json(400, {err: ':id required'}); if (direction !== 'up' && direction !== 'down') { if (direction == 'unlink') return next(); - return res.json(500, {err: ":direction must be 'up' or 'down'"}); + return res.json(400, {err: ":direction must be 'up' or 'down'"}); } // If exists already, score it if (task = user.tasks[id]) { @@ -181,7 +178,7 @@ api.update = function(req, res, next) { if (acceptablePUTPaths[k]) user.fns.dotSet(k, v); else - errors.push("path `" + k + "` was not saved, as it's a protected path. Make sure to send `PUT /api/v1/user` request bodies as `{'set.this.path':value}` instead of `{set:{this:{path:value}}}`"); + errors.push("path `" + k + "` was not saved, as it's a protected path. See https://github.com/HabitRPG/habitrpg/blob/develop/API.md for PUT /api/v2/user."); return true; }); user.save(function(err) { diff --git a/src/routes/apiv1.js b/src/routes/apiv1.js new file mode 100644 index 0000000000..53207631d2 --- /dev/null +++ b/src/routes/apiv1.js @@ -0,0 +1,171 @@ +var express = require('express'); +var router = new express.Router(); +var _ = require('lodash'); +var async = require('async'); +var icalendar = require('icalendar'); +var api = require('./../controllers/user'); +var auth = require('./../controllers/auth'); +var middleware = require('../middleware'); + +/* ---------- Deprecated API ------------*/ + +var initDeprecated = function(req, res, next) { + req.headers['x-api-user'] = req.params.uid; + req.headers['x-api-key'] = req.body.apiToken; + return next(); +}; + +router.post('/v1/users/:uid/tasks/:taskId/:direction', initDeprecated, auth.auth, api.score); + +// FIXME add this back in +router.get('/v1/users/:uid/calendar.ics', function(req, res, next) { + return next() //disable for now + + var apiToken, model, query, uid; + uid = req.params.uid; + apiToken = req.query.apiToken; + model = req.getModel(); + query = model.query('users').withIdAndToken(uid, apiToken); + return query.fetch(function(err, result) { + var formattedIcal, ical, tasks, tasksWithDates; + if (err) { + return res.send(500, err); + } + tasks = result.get('tasks'); + /* tasks = result[0].tasks*/ + + tasksWithDates = _.filter(tasks, function(task) { + return !!task.date; + }); + if (_.isEmpty(tasksWithDates)) { + return res.send(500, "No events found"); + } + ical = new icalendar.iCalendar(); + ical.addProperty('NAME', 'HabitRPG'); + _.each(tasksWithDates, function(task) { + var d, event; + event = new icalendar.VEvent(task.id); + event.setSummary(task.text); + d = new Date(task.date); + d.date_only = true; + event.setDate(d); + ical.addComponent(event); + return true; + }); + res.type('text/calendar'); + formattedIcal = ical.toString().replace(/DTSTART\:/g, 'DTSTART;VALUE=DATE:'); + return res.send(200, formattedIcal); + }); +}); + +/* + ------------------------------------------------------------------------ + Batch Update + This is super-deprecated, and will be removed once apiv2 is running against mobile for a while + ------------------------------------------------------------------------ + */ +var batchUpdate = function(req, res, next) { + var user = res.locals.user; + var oldSend = res.send; + var oldJson = res.json; + var performAction = function(action, cb) { + + // req.body=action.data; delete action.data; _.defaults(req.params, action) + // Would require changing action.dir on mobile app + req.params.id = action.data && action.data.id; + req.params.direction = action.dir; + req.params.type = action.type; + req.body = action.data; + res.send = res.json = function(code, data) { + if (_.isNumber(code) && code >= 400) { + console.error({ + code: code, + data: data + }); + } + //FIXME send error messages down + return cb(); + }; + switch (action.op) { + case "score": + api.score(req, res); + break; + case "addTask": + api.addTask(req, res); + break; + case "delTask": + api.deleteTask(req, res); + break; + case "revive": + api.revive(req, res); + break; + default: + cb(); + break; + } + }; + + // Setup the array of functions we're going to call in parallel with async + var actions = _.transform(req.body || [], function(result, action) { + if (!_.isEmpty(action)) { + result.push(function(cb) { + performAction(action, cb); + }); + } + }); + + // call all the operations, then return the user object to the requester + async.series(actions, function(err) { + res.json = oldJson; + res.send = oldSend; + if (err) return res.json(500, {err: err}); + var response = user.toJSON(); + response.wasModified = res.locals.wasModified; + if (response._tmp && response._tmp.drop){ + res.json(200, {_tmp: {drop: response._tmp.drop}, _v: response._v}); + }else if(response.wasModified){ + res.json(200, response); + }else{ + res.json(200, {_v: response._v}); + } + }); +}; + +/* + ------------------------------------------------------------------------ + API v1 Routes + ------------------------------------------------------------------------ + */ + + +var cron = api.cron; + +router.get('/status', function(req, res) { + return res.json({ + status: 'up' + }); +}); + +// Scoring +router.post('/user/task/:id/:direction', auth.auth, cron, api.score); +router.post('/user/tasks/:id/:direction', auth.auth, cron, api.score); + +// Tasks +router.get('/user/tasks', auth.auth, cron, api.getTasks); +router.get('/user/task/:id', auth.auth, cron, api.getTask); +router["delete"]('/user/task/:id', auth.auth, cron, api.deleteTask); +router.post('/user/task', auth.auth, cron, api.addTask); + +// User +router.get('/user', auth.auth, cron, api.getUser); +router.post('/user/revive', auth.auth, cron, api.revive); +router.post('/user/batch-update', middleware.forceRefresh, auth.auth, cron, batchUpdate); + +function deprecated(req, res) { + res.json(404, {err:'API v1 is no longer supported, please use API v2 instead (https://github.com/HabitRPG/habitrpg/blob/develop/API.md)'}); +} +router.get('*', deprecated); +router.post('*', deprecated); +router.put('*', deprecated); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/api.js b/src/routes/apiv2.js similarity index 100% rename from src/routes/api.js rename to src/routes/apiv2.js diff --git a/src/routes/auth.js b/src/routes/auth.js index b15f1b4a0f..c055f2888a 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -4,10 +4,10 @@ var router = new express.Router(); /* auth.auth*/ auth.setupPassport(router); //FIXME make this consistent with the others -router.post('/api/v1/register', auth.registerUser); -router.post('/api/v1/user/auth/local', auth.loginLocal); -router.post('/api/v1/user/auth/facebook', auth.loginFacebook); -router.post('/api/v1/user/reset-password', auth.resetPassword); -router.post('/api/v1/user/change-password', auth.auth, auth.changePassword); +router.post('/api/v2/register', auth.registerUser); +router.post('/api/v2/user/auth/local', auth.loginLocal); +router.post('/api/v2/user/auth/facebook', auth.loginFacebook); +router.post('/api/v2/user/reset-password', auth.resetPassword); +router.post('/api/v2/user/change-password', auth.auth, auth.changePassword); module.exports = router; \ No newline at end of file diff --git a/src/server.js b/src/server.js index d6b43abb95..68c2d97693 100644 --- a/src/server.js +++ b/src/server.js @@ -113,9 +113,9 @@ if ("development" === app.get("env")) { // Custom Directives app.use(require('./routes/pages').middleware); app.use(require('./routes/auth').middleware); -app.use('/api/v1', require('./routes/api').middleware); +app.use('/api/v2', require('./routes/apiv2').middleware); +app.use('/api/v1', require('./routes/apiv1').middleware); app.use('/export', require('./routes/dataexport').middleware); -app.use(require('./controllers/deprecated').middleware); server = http.createServer(app).listen(app.get("port"), function() { return console.log("Express server listening on port " + app.get("port")); }); diff --git a/views/shared/modals/buy-gems.jade b/views/shared/modals/buy-gems.jade index 61688ef33c..1b238a2e96 100644 --- a/views/shared/modals/buy-gems.jade +++ b/views/shared/modals/buy-gems.jade @@ -26,7 +26,7 @@ div(modal='modals.buyGems') .btn.btn-primary(ng-click='showStripe()') Pay with Card .span6.well h3 Pay with PayPal - script(src='/bower_components/JavaScriptButtons/dist/paypal-button.min.js?merchant=#{env.PAYPAL_MERCHANT}', data-button='buynow', data-name='20 Gems, Disable Ads, Donation to the Developers', data-quantity='1', data-amount='5', data-currency='USD', data-tax='0', data-callback='#{env.BASE_URL}/api/v1/user/buy-gems/paypal-ipn', data-env="#{env.NODE_ENV == 'production' ? '' : 'sandbox'}", data-custom='?uid={{user._id}}&apiToken={{user.apiToken}}', data-return='#{env.BASE_URL}', data-rm='1', data-no_shipping='1') + script(src='/bower_components/JavaScriptButtons/dist/paypal-button.min.js?merchant=#{env.PAYPAL_MERCHANT}', data-button='buynow', data-name='20 Gems, Disable Ads, Donation to the Developers', data-quantity='1', data-amount='5', data-currency='USD', data-tax='0', data-callback='#{env.BASE_URL}/api/v2/user/buy-gems/paypal-ipn', data-env="#{env.NODE_ENV == 'production' ? '' : 'sandbox'}", data-custom='?uid={{user._id}}&apiToken={{user.apiToken}}', data-return='#{env.BASE_URL}', data-rm='1', data-no_shipping='1') .modal-footer button.btn.btn-default.cancel(ng-click='modals.buyGems = false') Cancel \ No newline at end of file