diff --git a/assets/js/app.js b/assets/js/app.js index 8d03690a1b..b1649c4514 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -18,7 +18,9 @@ window.habitrpg = angular.module('habitrpg', .otherwise({redirectTo: '/tasks'}); var settings = JSON.parse(localStorage.getItem(STORAGE_SETTINGS_ID)); - $httpProvider.defaults.headers.common['Content-Type'] = 'application/json;charset=utf-8'; - $httpProvider.defaults.headers.common['x-api-user'] = settings.auth.apiId; - $httpProvider.defaults.headers.common['x-api-key'] = settings.auth.apiId; + if (settings && settings.auth) { + $httpProvider.defaults.headers.common['Content-Type'] = 'application/json;charset=utf-8'; + $httpProvider.defaults.headers.common['x-api-user'] = settings.auth.apiId; + $httpProvider.defaults.headers.common['x-api-key'] = settings.auth.apiId; + } }]) \ No newline at end of file diff --git a/assets/js/controllers/rootCtrl.js b/assets/js/controllers/rootCtrl.js index abef58afd4..e261c606b3 100644 --- a/assets/js/controllers/rootCtrl.js +++ b/assets/js/controllers/rootCtrl.js @@ -3,8 +3,8 @@ /* Make user and settings available for everyone through root scope. */ -habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', - function($scope, $rootScope, $location, User) { +habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$http', + function($scope, $rootScope, $location, User, $http) { $rootScope.modals = {}; $rootScope.User = User; $rootScope.user = User.user; @@ -29,4 +29,25 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', alert("This feature is not yet ported from the original site."); } + $rootScope.showStripe = function() { + var disableAds = User.user.flags.ads == 'hide' ? '' : 'Disable Ads, '; + StripeCheckout.open({ + key: window.env.STRIPE_PUB_KEY, + address: false, + amount: 500, + name: "Checkout", + description: "Buy 20 Gems, " + disableAds + "Support the Developers", + panelLabel: "Checkout", + token: function(data) { + $scope.$apply(function(){ + $http.post("/api/v1/user/buy-gems", data) + .success(function() { + window.location.href = "/"; + }).error(function(err) { + alert(err); + }); + }) + } + }); + } }]); diff --git a/assets/js/services/userServices.js b/assets/js/services/userServices.js index 1a83b8307d..187b534e2a 100644 --- a/assets/js/services/userServices.js +++ b/assets/js/services/userServices.js @@ -129,7 +129,7 @@ angular.module('userServices', []). }, authenticated: function(){ - this.settings.auth.apiId !== ""; + return this.settings.auth.apiId !== ""; }, /* diff --git a/src/config.js b/src/config.js index 308179c43a..6bf407baba 100644 --- a/src/config.js +++ b/src/config.js @@ -6,7 +6,7 @@ var path = require("path"); conf.argv() .env() - .file('defaults', path.join(path.resolve(__dirname, '../config.json.example'))) + //.file('defaults', path.join(path.resolve(__dirname, '../config.json.example'))) .file('user', path.join(path.resolve(__dirname, '../config.json'))); /* diff --git a/src/controllers/api.coffee b/src/controllers/api.coffee deleted file mode 100644 index 64eab99203..0000000000 --- a/src/controllers/api.coffee +++ /dev/null @@ -1,493 +0,0 @@ -# @see ./routes.coffee for routing - -_ = require 'lodash' -async = require 'async' -algos = require 'habitrpg-shared/script/algos' -helpers = require 'habitrpg-shared/script/helpers' -items = require 'habitrpg-shared/script/items' -validator = require 'derby-auth/node_modules/validator' -check = validator.check -sanitize = validator.sanitize -utils = require 'derby-auth/utils' -derbyAuthUtil = require('derby-auth/utils') -User = require('./../models/user').model -Group = require('./../models/group').model - -api = module.exports - -### - ------------------------------------------------------------------------ - Misc - ------------------------------------------------------------------------ -#### - -NO_TOKEN_OR_UID = {err: "You must include a token and uid (user id) in your request"} -NO_USER_FOUND = {err: "No user found."} - -### - beforeEach auth interceptor -### -api.auth = (req, res, next) -> - uid = req.headers['x-api-user'] - token = req.headers['x-api-key'] - return res.json(401, NO_TOKEN_OR_UID) unless uid and token - - User.findOne {_id: uid, apiToken: token}, (err, user) -> - return res.json(500, {err}) if err - return res.json(401, NO_USER_FOUND) if _.isEmpty(user) - res.locals.wasModified = +user._v isnt +req.query._v - res.locals.user = user - next() - -### - ------------------------------------------------------------------------ - Tasks - ------------------------------------------------------------------------ -### - -### - Local Methods - --------------- -### - -# FIXME put this in helpers, so mobile & web can us it too -# FIXME actually, move to mongoose -taskSanitizeAndDefaults = (task) -> - task.id ?= helpers.uuid() - task.value = ~~task.value - task.type ?= 'habit' - task.text = sanitize(task.text).xss() if _.isString(task.text) - task.notes = sanitize(task.notes).xss() if _.isString(task.text) - if task.type is 'habit' - task.up = true unless _.isBoolean(task.up) - task.down = true unless _.isBoolean(task.down) - if task.type in ['daily', 'todo'] - task.completed = false unless _.isBoolean(task.completed) - if task.type is 'daily' - task.repeat ?= {m:true,t:true,w:true,th:true,f:true,s:true,su:true} - task - -### -Validate task -### -api.verifyTaskExists = (req, res, next) -> - # If we're updating, get the task from the user - task = res.locals.user.tasks[req.params.id] - return res.json(400, err: "No task found.") if _.isEmpty(task) - res.locals.task = task - next() - -addTask = (user, task) -> - taskSanitizeAndDefaults(task) - user.tasks[task.id] = task - user["#{task.type}Ids"].unshift task.id - task - -# Override current user.task with incoming values, then sanitize all values -updateTask = (user, id, incomingTask) -> - user.tasks[id] = taskSanitizeAndDefaults _.defaults(incomingTask, user.tasks[id]) - -deleteTask = (user, task) -> - delete user.tasks[task.id] - if (ids = user["#{task.type}Ids"]) and ~(i = ids.indexOf task.id) - ids.splice(i,1) - -### - API Routes - --------------- -### - -### - This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login - Export it also so we can call it from deprecated.coffee -### -api.scoreTask = (req, res, next) -> - {id, direction} = req.params - - # Send error responses for improper API call - return res.json(500, {err: ':id required'}) unless id - return res.json(500, {err: ":direction must be 'up' or 'down'"}) unless direction in ['up','down'] - - {user} = res.locals - - # If exists already, score it - if (existing = user.tasks[id]) - # Set completed if type is daily or todo and task exists - if existing.type in ['daily', 'todo'] - existing.completed = (direction is 'up') - - # If it doesn't exist, this is likely a 3rd party up/down - create a new one, then score it - else - task = - id: id - value: 0 - type: req.body?.type or 'habit' - text: req.body?.title or id - notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." - if task.type is 'habit' - task.up = task.down = true - if task.type in ['daily', 'todo'] - task.completed = direction is 'up' - addTask user, task - - task = user.tasks[id] - delta = algos.score(user, task, direction) - user.save (err, saved) -> - return res.json(500, {err}) if err - res.json 200, _.extend({delta: delta}, saved.toJSON().stats) - -### - Get all tasks -### -api.getTasks = (req, res, next) -> - types = - if req.query.type in ['habit','todo','daily','reward'] then [req.query.type] - else ['habit','todo','daily','reward'] - tasks = _.toArray (_.filter res.locals.user.tasks, (t)-> t.type in types) - res.json 200, tasks - -### - Get Task -### -api.getTask = (req, res, next) -> - task = res.locals.user.tasks[req.params.id] - return res.json(400, err: "No task found.") if _.isEmpty(task) - res.json 200, task - -### - Delete Task -### -api.deleteTask = (req, res, next) -> - deleteTask res.locals.user, res.locals.task - res.locals.user.save (err) -> - return res.json(500, {err}) if err - res.send 204 - -### - Update Task -### -api.updateTask = (req, res, next) -> - {user} = res.locals - {id} = req.params - updateTask user, id, req.body - user.save (err, saved) -> - return res.json(500, {err}) if err - res.json 200, _.findWhere(saved.toJSON().tasks, {id}) - -### - Update tasks (plural). This will update, add new, delete, etc all at once. - Should we keep this? -### -api.updateTasks = (req, res, next) -> - {user} = res.locals - tasks = req.body - _.each tasks, (task, idx) -> - if task.id - if task.del # Delete - deleteTask user, task - task = deleted: true - else # Update - updateTask user, task.id, task - else # Create - task = addTask user, task - tasks[idx] = task - - user.save (err, saved) -> - return res.json 500, {err:err} if err - res.json 201, tasks - -api.createTask = (req, res, next) -> - {user} = res.locals - task = addTask user, req.body - user.save (err) -> - return res.json(500, {err}) if err - res.json 201, task - -api.sortTask = (req, res, next) -> - {id} = req.params - {to, from, type} = req.body - {user} = res.locals - path = "#{type}Ids" - user[path].splice(to, 0, user[path].splice(from, 1)[0]) - user.save (err, saved) -> - return res.json(500,{err}) if err - res.json 200, saved.toJSON()[path] - -api.clearCompleted = (req, res, next) -> - {user} = res.locals - completedIds = _.pluck( _.where(user.tasks, {type:'todo', completed:true}), 'id') - todoIds = user.todoIds - _.each completedIds, (id) -> delete user.tasks[id]; true - user.todoIds = _.difference(todoIds, completedIds) - user.save (err, saved) -> - return res.json(500, {err}) if err - res.json saved - -### - ------------------------------------------------------------------------ - Items - ------------------------------------------------------------------------ -### -api.buy = (req, res, next) -> - {user} = res.locals - type = req.params.type - unless type in ['weapon', 'armor', 'head', 'shield', 'potion'] - return res.json(400, err: ":type must be in one of: 'weapon', 'armor', 'head', 'shield', 'potion'") - hasEnough = items.buyItem(user, type) - if hasEnough - user.save (err, saved) -> - return res.json(500,{err}) if err - res.json 200, saved.toJSON().items - else - res.json 200, {err: "Not enough GP"} - -### - ------------------------------------------------------------------------ - User - ------------------------------------------------------------------------ -### - - -### - Registers a new user. Only accepting username/password registrations, no Facebook -### -api.registerUser = (req, res, next) -> - {email, username, password, confirmPassword} = req.body - - unless username and password and email - return res.json 401, err: ":username, :email, :password, :confirmPassword required" - if password isnt confirmPassword - return res.json 401, err: ":password and :confirmPassword don't match" - try - validator.check(email).isEmail() - catch e - return res.json 401, err: e.message - - async.waterfall [ - (cb) -> - User.findOne {'auth.local.email':email}, cb - - , (found, cb) -> - return cb("Email already taken") if found - User.findOne {'auth.local.username':username}, cb - - , (found, cb) -> - return cb("Username already taken") if found - newUser = helpers.newUser(true) - salt = utils.makeSalt() - newUser.auth = local: {username, email, salt} - newUser.auth.local.hashed_password = derbyAuthUtil.encryptPassword(password, salt) - user = new User(newUser) - user.save cb - - ], (err, saved) -> - return res.json(401, {err}) if err - res.json 200, saved - -### - Get User -### -api.getUser = (req, res, next) -> - {user} = res.locals - - user.stats.toNextLevel = algos.tnl user.stats.lvl - user.stats.maxHealth = 50 - - delete user.apiToken - if user.auth - delete user.auth.hashed_password - delete user.auth.salt - - res.json(200, user) - -### - Register new user with uname / password -### -api.loginLocal = (req, res, next) -> - {username, password} = req.body - async.waterfall [ - (cb) -> - return cb('No username or password') unless username and password - User.findOne {'auth.local.username':username}, cb - , (user, cb) -> - return cb('Username not found') unless user - # We needed the whole user object first so we can get his salt to encrypt password comparison - User.findOne({ - 'auth.local.username': username - 'auth.local.hashed_password': utils.encryptPassword(password, user.auth.local.salt) - }, cb) - ], (err, user) -> - err = 'Incorrect password' unless user - return res.json(401, {err}) if err - res.json 200, {id: user._id, token: user.apiToken} - -### - POST /user/auth/facebook -### -api.loginFacebook = (req, res, next) -> - {facebook_id, email, name} = req.body - return res.json(401, err: 'No facebook id provided') unless facebook_id - User.findOne {'auth.local.facebook.id':facebook_id}, (err, user) -> - return res.json(401, {err}) if err - if user - 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.") - -### - Update user - FIXME add documentation here -### -api.updateUser = (req, res, next) -> - {user} = res.locals - errors = [] - - return res.json(200, user) if _.isEmpty(req.body) - - # FIXME we need to do some crazy sanitiazation if they're using the old `PUT /user {data}` method. - # The new `PUT /user {'stats.hp':50} - - # FIXME - one-by-one we want to widdle down this list, instead replacing each needed set path with API operations - # There's a trick here. In order to prevent prevent clobering top-level paths, we add `.` to make sure they're - # sending bodies as {"set.this.path":value} instead of {set:{this:{path:value}}}. Permit lastCron since it's top-level - # Note: custom is for 3rd party apps - acceptableAttrs = 'tasks. achievements. filters. flags. invitations. items. lastCron party. preferences. profile. stats. tags. custom.'.split(' ') - _.each req.body, (v, k) -> - if (_.find acceptableAttrs, (attr)-> k.indexOf(attr) is 0)? - if _.isObject(v) - errors.push "Value for #{k} was an object. Be careful here, you could clobber stuff." - helpers.dotSet(k,v,user) - 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}}}`" - true - user.save (err) -> - return res.json(500, {err: errors}) unless _.isEmpty errors - return res.json(500, {err}) if err - res.json 200, user - -api.cron = (req, res, next) -> - {user} = res.locals - algos.cron user - #FIXME make sure the variable references got handled properly - user.save next - -api.revive = (req, res, next) -> - {user} = res.locals - algos.revive user - user.save (err, saved) -> - return res.json(500,{err}) if err - res.json 200, saved - -api.reroll = (req, res, next) -> - {user} = res.locals - if user.balance < 1 - return res.json 401, {err: "Not enough tokens."} - user.balance -= 1 - _.each user.tasks, (task) -> - user.tasks[task.id].value = 0 unless task.type is 'reward' - true - user.stats.hp = 50 - user.save (err, saved) -> - return res.json(500, {err}) if err - res.json 200, saved - -### - ------------------------------------------------------------------------ - Party - ------------------------------------------------------------------------ -### -api.getGroups = (req, res, next) -> - {user} = res.locals - #TODO should we support non-authenticated users? just for viewing public groups? - async.parallel - party: (cb) -> - async.waterfall [ - (cb2) -> - Group.findOne {type:'party', members: {'$in': [user._id]}}, cb2 - (party, cb2) -> - party = party.toJSON() - query = _id: {'$in': party.members, '$nin': [user._id]} - fields = 'profile preferences items stats achievements party backer auth.local.username auth.facebook.first_name auth.facebook.last_name auth.facebook.name auth.facebook.username'.split(' ') - fields = _.reduce fields, ((m,k,v) -> m[k]=1;m), {} - User.find query, fields, (err, members) -> - party.members = members - cb2(err, party) - ], (err, members) -> cb(err, members) - - guilds: (cb) -> - return cb(null, {}) - Group.findOne {type:'guild', members: {'$in': [user._id]}}, cb - - public: (cb) -> - return cb(null, {}) - Group.find {privacy: 'public'}, {name:1, description:1, members:1}, cb - - , (err, results) -> -# console.log {arguments} - return res.json 500,{err} if err - res.json results - -### - ------------------------------------------------------------------------ - Batch Update - Run a bunch of updates all at once - ------------------------------------------------------------------------ -### -api.batchUpdate = (req, res, next) -> - {user} = res.locals - - oldSend = res.send - oldJson = res.json - - performAction = (action, cb) -> - # TODO come up with a more consistent approach here. like: - # req.body=action.data; delete action.data; _.defaults(req.params, action) - # Would require changing action.dir on mobile app - req.params.id = action.data?.id - req.params.direction = action.dir - req.params.type = action.type - req.body = action.data - - res.send = res.json = (code, data) -> - console.error({code, data}) if _.isNumber(code) and code >= 400 - #FIXME send error messages down - cb() - - switch action.op - when "score" - api.scoreTask(req, res) - when "buy" - api.buy(req, res) - when "sortTask" - api.verifyTaskExists req, res, -> - api.sortTask(req, res) - when "addTask" - api.createTask(req, res) - when "delTask" - api.verifyTaskExists req, res, -> - api.deleteTask(req, res) - when "set" - api.updateUser(req, res) - when "revive" - api.revive(req, res) - when "clear-completed" - api.clearCompleted(req, res) - when "reroll" - api.reroll(req, res) - else cb() - - # Setup the array of functions we're going to call in parallel with async - actions = _.transform (req.body ? []), (result, action) -> - unless _.isEmpty(action) - result.push (cb) -> performAction(action, cb) - - # call all the operations, then return the user object to the requester - async.series actions, (err) -> - res.json = oldJson; res.send = oldSend - return res.json(500, {err}) if err - response = user.toJSON() - response.wasModified = res.locals.wasModified - res.json 200, response - console.log "Reply sent" - diff --git a/src/controllers/api.js b/src/controllers/api.js new file mode 100644 index 0000000000..cc4c9e6a7a --- /dev/null +++ b/src/controllers/api.js @@ -0,0 +1,891 @@ +/* @see ./routes.coffee for routing*/ + +// fixme remove this junk, was coffeescript compiled (probably for IE8 compat) +var __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + +var _ = require('lodash'); +var nconf = require('nconf'); +var async = require('async'); +var algos = require('habitrpg-shared/script/algos'); +var helpers = require('habitrpg-shared/script/helpers'); +var items = require('habitrpg-shared/script/items'); +var validator = require('derby-auth/node_modules/validator'); +var check = validator.check; +var sanitize = validator.sanitize; +var utils = require('derby-auth/utils'); +var derbyAuthUtil = require('derby-auth/utils'); +var User = require('./../models/user').model; +var Group = require('./../models/group').model; +var api = module.exports; + +/* + ------------------------------------------------------------------------ + Misc + ------------------------------------------------------------------------ +*/ + +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."}; + +/* + beforeEach auth interceptor +*/ + + +api.auth = function(req, res, next) { + var token, uid; + uid = req.headers['x-api-user']; + token = req.headers['x-api-key']; + if (!(uid && token)) { + return res.json(401, NO_TOKEN_OR_UID); + } + return User.findOne({ + _id: uid, + apiToken: token + }, function(err, user) { + if (err) { + return res.json(500, { + err: err + }); + } + if (_.isEmpty(user)) { + return res.json(401, NO_USER_FOUND); + } + res.locals.wasModified = +user._v !== +req.query._v; + res.locals.user = user; + return next(); + }); +}; + +/* + ------------------------------------------------------------------------ + Tasks + ------------------------------------------------------------------------ +*/ + + +/* + Local Methods + --------------- +*/ + + +/* +// FIXME put this in helpers, so mobile & web can us it too +// FIXME actually, move to mongoose +*/ + + +function taskSanitizeAndDefaults(task) { + var _ref; + if (task.id == null) { + task.id = helpers.uuid(); + } + task.value = ~~task.value; + if (task.type == null) { + task.type = 'habit'; + } + if (_.isString(task.text)) { + task.text = sanitize(task.text).xss(); + } + if (_.isString(task.text)) { + task.notes = sanitize(task.notes).xss(); + } + if (task.type === 'habit') { + if (!_.isBoolean(task.up)) { + task.up = true; + } + if (!_.isBoolean(task.down)) { + task.down = true; + } + } + if ((_ref = task.type) === 'daily' || _ref === 'todo') { + if (!_.isBoolean(task.completed)) { + task.completed = false; + } + } + if (task.type === 'daily') { + if (task.repeat == null) { + task.repeat = { + m: true, + t: true, + w: true, + th: true, + f: true, + s: true, + su: true + }; + } + } + return task; +}; + +/* +Validate task +*/ + + +api.verifyTaskExists = function(req, res, next) { + /* If we're updating, get the task from the user*/ + + var task; + task = res.locals.user.tasks[req.params.id]; + if (_.isEmpty(task)) { + return res.json(400, { + err: "No task found." + }); + } + res.locals.task = task; + return next(); +}; + +function addTask(user, task) { + taskSanitizeAndDefaults(task); + user.tasks[task.id] = task; + user["" + task.type + "Ids"].unshift(task.id); + return task; +}; + +/* Override current user.task with incoming values, then sanitize all values*/ + + +function updateTask(user, id, incomingTask) { + return user.tasks[id] = taskSanitizeAndDefaults(_.defaults(incomingTask, user.tasks[id])); +}; + +function deleteTask(user, task) { + var i, ids; + delete user.tasks[task.id]; + if ((ids = user["" + task.type + "Ids"]) && ~(i = ids.indexOf(task.id))) { + return ids.splice(i, 1); + } +}; + +/* + API Routes + --------------- +*/ + + +/* + This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login + Export it also so we can call it from deprecated.coffee +*/ + + +api.scoreTask = function(req, res, next) { + var delta, direction, existing, id, task, user, _ref, _ref1, _ref2, _ref3, _ref4; + _ref = req.params, id = _ref.id, direction = _ref.direction; + /* Send error responses for improper API call*/ + + if (!id) { + return res.json(500, { + err: ':id required' + }); + } + if (direction !== 'up' && direction !== 'down') { + return res.json(500, { + err: ":direction must be 'up' or 'down'" + }); + } + user = res.locals.user; + /* If exists already, score it*/ + + if ((existing = user.tasks[id])) { + /* Set completed if type is daily or todo and task exists*/ + + if ((_ref1 = existing.type) === 'daily' || _ref1 === 'todo') { + existing.completed = direction === 'up'; + } + } else { + /* If it doesn't exist, this is likely a 3rd party up/down - create a new one, then score it*/ + + task = { + id: id, + value: 0, + type: ((_ref2 = req.body) != null ? _ref2.type : void 0) || 'habit', + text: ((_ref3 = req.body) != null ? _ref3.title : void 0) || id, + notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." + }; + if (task.type === 'habit') { + task.up = task.down = true; + } + if ((_ref4 = task.type) === 'daily' || _ref4 === 'todo') { + task.completed = direction === 'up'; + } + addTask(user, task); + } + task = user.tasks[id]; + delta = algos.score(user, task, direction); + return user.save(function(err, saved) { + if (err) { + return res.json(500, { + err: err + }); + } + return res.json(200, _.extend({ + delta: delta + }, saved.toJSON().stats)); + }); +}; + +/* + Get all tasks +*/ + + +api.getTasks = function(req, res, next) { + var tasks, types, _ref; + types = (_ref = req.query.type) === 'habit' || _ref === 'todo' || _ref === 'daily' || _ref === 'reward' ? [req.query.type] : ['habit', 'todo', 'daily', 'reward']; + tasks = _.toArray(_.filter(res.locals.user.tasks, function(t) { + var _ref1; + return _ref1 = t.type, __indexOf.call(types, _ref1) >= 0; + })); + return res.json(200, tasks); +}; + +/* + Get Task +*/ + + +api.getTask = function(req, res, next) { + var task; + task = res.locals.user.tasks[req.params.id]; + if (_.isEmpty(task)) { + return res.json(400, { + err: "No task found." + }); + } + return res.json(200, task); +}; + +/* + Delete Task +*/ + + +api.deleteTask = function(req, res, next) { + deleteTask(res.locals.user, res.locals.task); + return res.locals.user.save(function(err) { + if (err) { + return res.json(500, { + err: err + }); + } + return res.send(204); + }); +}; + +/* + Update Task +*/ + + +api.updateTask = function(req, res, next) { + var id, user; + user = res.locals.user; + id = req.params.id; + updateTask(user, id, req.body); + return user.save(function(err, saved) { + if (err) { + return res.json(500, { + err: err + }); + } + return res.json(200, _.findWhere(saved.toJSON().tasks, { + id: id + })); + }); +}; + +/* + Update tasks (plural). This will update, add new, delete, etc all at once. + Should we keep this? +*/ + + +api.updateTasks = function(req, res, next) { + var tasks, user; + user = res.locals.user; + tasks = req.body; + _.each(tasks, function(task, idx) { + if (task.id) { + /*delete*/ + + if (task.del) { + deleteTask(user, task); + task = { + deleted: true + }; + } else { + /* Update*/ + + updateTask(user, task.id, task); + } + } else { + /* Create*/ + + task = addTask(user, task); + } + return tasks[idx] = task; + }); + return user.save(function(err, saved) { + if (err) { + return res.json(500, { + err: err + }); + } + return res.json(201, tasks); + }); +}; + +api.createTask = function(req, res, next) { + var task, user; + user = res.locals.user; + task = addTask(user, req.body); + return user.save(function(err) { + if (err) { + return res.json(500, { + err: err + }); + } + return res.json(201, task); + }); +}; + +api.sortTask = function(req, res, next) { + var from, id, path, to, type, user, _ref; + id = req.params.id; + _ref = req.body, to = _ref.to, from = _ref.from, type = _ref.type; + user = res.locals.user; + path = "" + type + "Ids"; + user[path].splice(to, 0, user[path].splice(from, 1)[0]); + return user.save(function(err, saved) { + if (err) { + return res.json(500, { + err: err + }); + } + return res.json(200, saved.toJSON()[path]); + }); +}; + +api.clearCompleted = function(req, res, next) { + var completedIds, todoIds, user; + user = res.locals.user; + completedIds = _.pluck(_.where(user.tasks, { + type: 'todo', + completed: true + }), 'id'); + todoIds = user.todoIds; + _.each(completedIds, function(id) { + delete user.tasks[id]; + return true; + }); + user.todoIds = _.difference(todoIds, completedIds); + return user.save(function(err, saved) { + if (err) { + return res.json(500, { + err: err + }); + } + return res.json(saved); + }); +}; + +/* + ------------------------------------------------------------------------ + Items + ------------------------------------------------------------------------ +*/ + + +api.buy = function(req, res, next) { + var hasEnough, type, user; + user = res.locals.user; + type = req.params.type; + if (type !== 'weapon' && type !== 'armor' && type !== 'head' && type !== 'shield' && type !== 'potion') { + return res.json(400, { + err: ":type must be in one of: 'weapon', 'armor', 'head', 'shield', 'potion'" + }); + } + hasEnough = items.buyItem(user, type); + if (hasEnough) { + return user.save(function(err, saved) { + if (err) { + return res.json(500, { + err: err + }); + } + return res.json(200, saved.toJSON().items); + }); + } else { + return res.json(200, { + err: "Not enough GP" + }); + } +}; + +/* + ------------------------------------------------------------------------ + User + ------------------------------------------------------------------------ +*/ + + +/* + Registers a new user. Only accepting username/password registrations, no Facebook +*/ + + +api.registerUser = function(req, res, next) { + var confirmPassword, e, email, password, username, _ref; + _ref = req.body, email = _ref.email, username = _ref.username, password = _ref.password, confirmPassword = _ref.confirmPassword; + if (!(username && password && email)) { + return res.json(401, { + err: ":username, :email, :password, :confirmPassword required" + }); + } + if (password !== confirmPassword) { + return res.json(401, { + err: ":password and :confirmPassword don't match" + }); + } + try { + validator.check(email).isEmail(); + } catch (_error) { + e = _error; + return res.json(401, { + err: e.message + }); + } + return async.waterfall([ + function(cb) { + return User.findOne({ + 'auth.local.email': email + }, cb); + }, function(found, cb) { + if (found) { + return cb("Email already taken"); + } + return User.findOne({ + 'auth.local.username': username + }, cb); + }, function(found, cb) { + var newUser, salt, user; + if (found) { + return cb("Username already taken"); + } + newUser = helpers.newUser(true); + salt = utils.makeSalt(); + newUser.auth = { + local: { + username: username, + email: email, + salt: salt + } + }; + newUser.auth.local.hashed_password = derbyAuthUtil.encryptPassword(password, salt); + user = new User(newUser); + return user.save(cb); + } + ], function(err, saved) { + if (err) { + return res.json(401, { + err: err + }); + } + return res.json(200, saved); + }); +}; + +/* + Get User +*/ + + +api.getUser = function(req, res, next) { + var user; + user = res.locals.user; + user.stats.toNextLevel = algos.tnl(user.stats.lvl); + user.stats.maxHealth = 50; + delete user.apiToken; + if (user.auth) { + delete user.auth.hashed_password; + delete user.auth.salt; + } + return res.json(200, user); +}; + +/* + Register new user with uname / password +*/ + + +api.loginLocal = function(req, res, next) { + var password, username, _ref; + _ref = req.body, username = _ref.username, password = _ref.password; + return async.waterfall([ + function(cb) { + if (!(username && password)) { + return cb('No username or password'); + } + return User.findOne({ + 'auth.local.username': username + }, cb); + }, function(user, cb) { + if (!user) { + return cb('Username not found'); + } + /* We needed the whole user object first so we can get his salt to encrypt password comparison*/ + + return User.findOne({ + 'auth.local.username': username, + 'auth.local.hashed_password': utils.encryptPassword(password, user.auth.local.salt) + }, cb); + } + ], function(err, user) { + if (!user) { + err = 'Incorrect password'; + } + if (err) { + return res.json(401, { + err: err + }); + } + return res.json(200, { + id: user._id, + token: user.apiToken + }); + }); +}; + +/* + POST /user/auth/facebook +*/ + + +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.local.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 + }); + } 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." + }); + } + }); +}; + +/* + Update user + FIXME add documentation here +*/ + + +api.updateUser = function(req, res, next) { + var acceptableAttrs, errors, user; + user = res.locals.user; + errors = []; + if (_.isEmpty(req.body)) { + return res.json(200, user); + } + /* + # FIXME we need to do some crazy sanitiazation if they're using the old `PUT /user {data}` method. + # The new `PUT /user {'stats.hp':50} + + # FIXME - one-by-one we want to widdle down this list, instead replacing each needed set path with API operations + # There's a trick here. In order to prevent prevent clobering top-level paths, we add `.` to make sure they're + # sending bodies as {"set.this.path":value} instead of {set:{this:{path:value}}}. Permit lastCron since it's top-level + # Note: custom is for 3rd party apps + */ + + acceptableAttrs = 'tasks. achievements. filters. flags. invitations. items. lastCron party. preferences. profile. stats. tags. custom.'.split(' '); + _.each(req.body, function(v, k) { + if ((_.find(acceptableAttrs, function(attr) { + return k.indexOf(attr) === 0; + })) != null) { + if (_.isObject(v)) { + errors.push("Value for " + k + " was an object. Be careful here, you could clobber stuff."); + } + helpers.dotSet(k, v, user); + } 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}}}`"); + } + return true; + }); + return user.save(function(err) { + if (!_.isEmpty(errors)) { + return res.json(500, { + err: errors + }); + } + if (err) { + return res.json(500, { + err: err + }); + } + return res.json(200, user); + }); +}; + +api.cron = function(req, res, next) { + var user; + user = res.locals.user; + algos.cron(user); + /*FIXME make sure the variable references got handled properly*/ + + return user.save(next); +}; + +api.revive = function(req, res, next) { + var user; + user = res.locals.user; + algos.revive(user); + return user.save(function(err, saved) { + if (err) { + return res.json(500, { + err: err + }); + } + return res.json(200, saved); + }); +}; + +api.reroll = function(req, res, next) { + var user; + user = res.locals.user; + if (user.balance < 1) { + return res.json(401, { + err: "Not enough tokens." + }); + } + user.balance -= 1; + _.each(user.tasks, function(task) { + if (task.type !== 'reward') { + user.tasks[task.id].value = 0; + } + return true; + }); + user.stats.hp = 50; + return user.save(function(err, saved) { + if (err) { + return res.json(500, { + err: err + }); + } + return res.json(200, saved); + }); +}; + + +/* + Setup Stripe response when posting payment + */ +api.buyGems = function(req, res) { + var api_key = nconf.get('STRIPE_API_KEY'); + var stripe = require("stripe")(api_key); + var token = req.body.id; + // console.dir {token:token, req:req}, 'stripe' + + async.waterfall([ + function(cb){ + stripe.charges.create({ + amount: "500", // $5 + currency: "usd", + card: token + }, cb); + }, + function(response, cb) { + res.locals.user.balance += 5; + res.locals.user.flags.ads = 'hide'; + res.locals.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, saved); + }); +}; + +/* + ------------------------------------------------------------------------ + Party + ------------------------------------------------------------------------ +*/ + + +api.getGroups = function(req, res, next) { + var user = res.locals.user; + /*TODO should we support non-authenticated users? just for viewing public groups?*/ + + return async.parallel({ + party: function(cb) { + return async.waterfall([ + function(cb2) { + return Group.findOne({ + type: 'party', + members: { + '$in': [user._id] + } + }, cb2); + }, function(party, cb2) { + var fields, query; + party = party.toJSON(); + query = { + _id: { + '$in': party.members, + '$nin': [user._id] + } + }; + fields = 'profile preferences items stats achievements party backer auth.local.username auth.facebook.first_name auth.facebook.last_name auth.facebook.name auth.facebook.username'.split(' '); + fields = _.reduce(fields, (function(m, k, v) { + m[k] = 1; + return m; + }), {}); + return User.find(query, fields, function(err, members) { + party.members = members; + return cb2(err, party); + }); + } + ], function(err, members) { + return cb(err, members); + }); + }, + guilds: function(cb) { + return cb(null, {}); + return Group.findOne({ + type: 'guild', + members: { + '$in': [user._id] + } + }, cb); + }, + "public": function(cb) { + return cb(null, {}); + return Group.find({ + privacy: 'public' + }, { + name: 1, + description: 1, + members: 1 + }, cb); + } + }, function(err, results) { + if (err) { + return res.json(500, { + err: err + }); + } + return res.json(results); + }); +}; + +/* + ------------------------------------------------------------------------ + Batch Update + Run a bunch of updates all at once + ------------------------------------------------------------------------ +*/ + + +api.batchUpdate = function(req, res, next) { + var actions, oldJson, oldSend, performAction, user, _ref; + user = res.locals.user; + oldSend = res.send; + oldJson = res.json; + performAction = function(action, cb) { + /* + # TODO come up with a more consistent approach here. like: + # req.body=action.data; delete action.data; _.defaults(req.params, action) + # Would require changing action.dir on mobile app + */ + + var _ref; + req.params.id = (_ref = action.data) != null ? _ref.id : void 0; + 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": + return api.scoreTask(req, res); + case "buy": + return api.buy(req, res); + case "sortTask": + return api.verifyTaskExists(req, res, function() { + return api.sortTask(req, res); + }); + case "addTask": + return api.createTask(req, res); + case "delTask": + return api.verifyTaskExists(req, res, function() { + return api.deleteTask(req, res); + }); + case "set": + return api.updateUser(req, res); + case "revive": + return api.revive(req, res); + case "clear-completed": + return api.clearCompleted(req, res); + case "reroll": + return api.reroll(req, res); + default: + return cb(); + } + }; + /* Setup the array of functions we're going to call in parallel with async*/ + + actions = _.transform((_ref = req.body) != null ? _ref : [], function(result, action) { + if (!_.isEmpty(action)) { + return result.push(function(cb) { + return performAction(action, cb); + }); + } + }); + /* call all the operations, then return the user object to the requester*/ + + return async.series(actions, function(err) { + var response; + res.json = oldJson; + res.send = oldSend; + if (err) { + return res.json(500, { + err: err + }); + } + response = user.toJSON(); + response.wasModified = res.locals.wasModified; + res.json(200, response); + return console.log("Reply sent"); + }); +}; \ No newline at end of file diff --git a/src/controllers/deprecated.coffee b/src/controllers/deprecated.coffee deleted file mode 100644 index 4a8f898c92..0000000000 --- a/src/controllers/deprecated.coffee +++ /dev/null @@ -1,52 +0,0 @@ -express = require 'express' -router = new express.Router() - -_ = require 'lodash' -icalendar = require('icalendar') -api = require './api' - -# ---------- Deprecated Paths ------------ - -deprecatedMessage = 'This API is no longer supported, see https://github.com/lefnire/habitrpg/wiki/API for new protocol' - -router.get '/:uid/up/:score?', (req, res) -> res.send(500, deprecatedMessage) -router.get '/:uid/down/:score?', (req, res) -> res.send(500, deprecatedMessage) -router.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> res.send(500, deprecatedMessage) - -# Redirect to new API -initDeprecated = (req, res, next) -> - req.headers['x-api-user'] = req.params.uid - req.headers['x-api-key'] = req.body.apiToken - next() - -router.post '/v1/users/:uid/tasks/:taskId/:direction', initDeprecated, api.auth, api.scoreTask - -router.get '/v1/users/:uid/calendar.ics', (req, res) -> - #return next() #disable for now - {uid} = req.params - {apiToken} = req.query - - model = req.getModel() - query = model.query('users').withIdAndToken(uid, apiToken) - query.fetch (err, result) -> - return res.send(500, err) if err - tasks = result.get('tasks') - # tasks = result[0].tasks - tasksWithDates = _.filter tasks, (task) -> !!task.date - return res.send(500, "No events found") if _.isEmpty(tasksWithDates) - - ical = new icalendar.iCalendar() - ical.addProperty('NAME', 'HabitRPG') - _.each tasksWithDates, (task) -> - 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 - true - res.type('text/calendar') - formattedIcal = ical.toString().replace(/DTSTART\:/g, 'DTSTART;VALUE=DATE:') - res.send(200, formattedIcal) - -module.exports = router diff --git a/src/controllers/deprecated.js b/src/controllers/deprecated.js new file mode 100644 index 0000000000..5852e0a297 --- /dev/null +++ b/src/controllers/deprecated.js @@ -0,0 +1,81 @@ +var api, deprecatedMessage, express, icalendar, initDeprecated, router, _; + +express = require('express'); + +router = new express.Router(); + +_ = require('lodash'); + +icalendar = require('icalendar'); + +api = require('./api'); + +/* ---------- Deprecated Paths ------------*/ + + +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*/ + + +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, api.auth, api.scoreTask); + +router.get('/v1/users/:uid/calendar.ics', function(req, res) { + /*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/private.coffee b/src/controllers/private.coffee deleted file mode 100644 index 09ca326d0c..0000000000 --- a/src/controllers/private.coffee +++ /dev/null @@ -1,60 +0,0 @@ -_ = require 'lodash' -misc = require "../app/misc" - -module.exports.middleware = (req, res, next) -> - model = req.getModel() - model.set '_stripePubKey', process.env.STRIPE_PUB_KEY - return next() - -module.exports.app = (appExports, model) -> - - appExports.showStripe = (e, el) -> - token = (res) -> - $.ajax({ - type: "POST", - url: "/charge", - data: res - }).success -> - window.location.href = "/" - .error (err) -> - alert err.responseText - - disableAds = if (model.get('_user.flags.ads') is 'hide') then '' else 'Disable Ads, ' - - StripeCheckout.open - key: model.get('_stripePubKey') - address: false - amount: 500 - name: "Checkout" - description: "Buy 20 Gems, #{disableAds}Support the Developers" - panelLabel: "Checkout" - token: token - -module.exports.routes = (expressApp) -> - ### - Setup Stripe response when posting payment - ### - expressApp.post '/charge', (req, res) -> - stripeCallback = (err, response) -> - if err - console.error(err, 'Stripe Error') - return res.send(500, err.response.error.message) - else - model = req.getModel() - userId = model.get('_userId') #or model.session.userId # see http://goo.gl/TPYIt - req._isServer = true - model.fetch "users.#{userId}", (err, user) -> - model.ref '_user', "users.#{userId}" - model.set('_user.balance', model.get('_user.balance')+5) - model.set('_user.flags.ads','hide') - return res.send(200) - - api_key = process.env.STRIPE_API_KEY # secret stripe API key - stripe = require("stripe")(api_key) - token = req.body.id - # console.dir {token:token, req:req}, 'stripe' - stripe.charges.create - amount: "500" # $5 - currency: "usd" - card: token - , stripeCallback \ No newline at end of file diff --git a/src/controllers/static.coffee b/src/controllers/static.coffee deleted file mode 100644 index 4b5cbe6b3e..0000000000 --- a/src/controllers/static.coffee +++ /dev/null @@ -1,24 +0,0 @@ -express = require 'express' -router = new express.Router() - -path = require 'path' -derby = require 'derby' - -# ---------- Static Pages ------------ -staticPages = derby.createStatic path.dirname(path.dirname(__dirname)) - -beforeEach = (req, res, next) -> - req.getModel().set '_nodeEnv', 'production' # we don't want cheat buttons on static pages - next() - -router.get '/splash.html', (req, res) -> res.redirect('/static/front') -router.get '/static/front', beforeEach, (req, res) -> staticPages.render 'static/front', res -router.get '/static/about', (req, res) -> res.redirect 'http://community.habitrpg.com/node/97' -router.get '/static/team', (req, res) -> res.redirect 'http://community.habitrpg.com/node/96' -router.get '/static/extensions', (req, res) -> res.redirect 'http://community.habitrpg.com/extensions' -router.get '/static/faq', (req, res) -> res.redirect 'http://community.habitrpg.com/faq-page' - -router.get '/static/privacy', beforeEach, (req, res) -> staticPages.render 'static/privacy', res -router.get '/static/terms', beforeEach, (req, res) -> staticPages.render 'static/terms', res - -module.exports = router diff --git a/src/controllers/static.js b/src/controllers/static.js new file mode 100644 index 0000000000..556d667592 --- /dev/null +++ b/src/controllers/static.js @@ -0,0 +1,51 @@ +var beforeEach, derby, express, path, router, staticPages; +express = require('express'); +router = new express.Router(); +path = require('path'); +derby = require('derby'); + +/* ---------- Static Pages ------------*/ + + +staticPages = derby.createStatic(path.dirname(path.dirname(__dirname))); + +beforeEach = function(req, res, next) { + /* we don't want cheat buttons on static pages*/ + + req.getModel().set('_nodeEnv', 'production'); + return next(); +}; + +router.get('/splash.html', function(req, res) { + return res.redirect('/static/front'); +}); + +router.get('/static/front', beforeEach, function(req, res) { + return staticPages.render('static/front', res); +}); + +router.get('/static/about', function(req, res) { + return res.redirect('http://community.habitrpg.com/node/97'); +}); + +router.get('/static/team', function(req, res) { + return res.redirect('http://community.habitrpg.com/node/96'); +}); + +router.get('/static/extensions', function(req, res) { + return res.redirect('http://community.habitrpg.com/extensions'); +}); + +router.get('/static/faq', function(req, res) { + return res.redirect('http://community.habitrpg.com/faq-page'); +}); + +router.get('/static/privacy', beforeEach, function(req, res) { + return staticPages.render('static/privacy', res); +}); + +router.get('/static/terms', beforeEach, function(req, res) { + return staticPages.render('static/terms', res); +}); + +module.exports = router; \ No newline at end of file diff --git a/src/middleware.js b/src/middleware.js index afc1381ac9..43221af124 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -16,7 +16,8 @@ module.exports = function(req, res, next) { var _base; _.defaults(((_base = res.locals).habitrpg != null ? (_base = res.locals).habitrpg : _base.habitrpg = {}), { NODE_ENV: nconf.get('NODE_ENV'), - IS_MOBILE: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header('User-Agent')) + IS_MOBILE: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header('User-Agent')), + STRIPE_PUB_KEY: nconf.get('STRIPE_PUB_KEY') }); /*CORS middleware*/ diff --git a/src/models/group.coffee b/src/models/group.coffee deleted file mode 100644 index acd7e61726..0000000000 --- a/src/models/group.coffee +++ /dev/null @@ -1,34 +0,0 @@ -mongoose = require("mongoose") -Schema = mongoose.Schema -helpers = require('habitrpg-shared/script/helpers') -_ = require('lodash') - -GroupSchema = new Schema( - - _id: {type: String, 'default': helpers.uuid} - name: String - description: String - leader: {type: String, ref: 'User'} - members: [{type: String, ref: 'User'}] - invites: [{type: String, ref: 'User'}] - type: {type: String, enum: ['guild', 'party']} - privacy: {type: String, enum: ['private', 'public']} - _v: {Number, 'default': 0} - chat: Array -# [{ -# timestamp: Date -# user: String -# text: String -# contributor: String -# uuid: String -# id: String -# }] - balance: Number - logo: String - leaderMessage: String - - -, {strict: 'throw'}) - -module.exports.schema = GroupSchema -module.exports.model = mongoose.model("Group", GroupSchema) \ No newline at end of file diff --git a/src/models/group.js b/src/models/group.js new file mode 100644 index 0000000000..046c267ffe --- /dev/null +++ b/src/models/group.js @@ -0,0 +1,67 @@ +var GroupSchema, Schema, helpers, mongoose, _; + +mongoose = require("mongoose"); + +Schema = mongoose.Schema; + +helpers = require('habitrpg-shared/script/helpers'); + +_ = require('lodash'); + +GroupSchema = new Schema({ + _id: { + type: String, + 'default': helpers.uuid + }, + name: String, + description: String, + leader: { + type: String, + ref: 'User' + }, + members: [ + { + type: String, + ref: 'User' + } + ], + invites: [ + { + type: String, + ref: 'User' + } + ], + type: { + type: String, + "enum": ['guild', 'party'] + }, + privacy: { + type: String, + "enum": ['private', 'public'] + }, + _v: { + Number: Number, + 'default': 0 + }, + chat: Array, + /* + # [{ + # timestamp: Date + # user: String + # text: String + # contributor: String + # uuid: String + # id: String + # }] + */ + + balance: Number, + logo: String, + leaderMessage: String +}, { + strict: 'throw' +}); + +module.exports.schema = GroupSchema; + +module.exports.model = mongoose.model("Group", GroupSchema); \ No newline at end of file diff --git a/src/models/user.coffee b/src/models/user.coffee deleted file mode 100644 index 25c668499d..0000000000 --- a/src/models/user.coffee +++ /dev/null @@ -1,192 +0,0 @@ -mongoose = require("mongoose") -Schema = mongoose.Schema -helpers = require('habitrpg-shared/script/helpers') -_ = require('lodash') - -UserSchema = new Schema( - - _id: {type: String, 'default': helpers.uuid} - apiToken: {type: String, 'default': helpers.uuid} - - # We want to know *every* time an object updates. Mongoose uses __v to designate when an object contains arrays which - # have been updated (http://goo.gl/gQLz41), but we want *every* update - _v: {type: Number, 'default': 0} - - achievements: - originalUser: Boolean - helpedHabit: Boolean - ultimateGear: Boolean - beastMaster: Boolean - streak: Number - - auth: - facebook: Schema.Types.Mixed - local: - email: String - hashed_password: String - salt: String - username: String - - timestamps: - created: {type: Date, 'default': Date.now} - loggedin: Date - - backer: Schema.Types.Mixed # TODO -# tier: Number -# admin: Boolean -# contributor: Boolean -# tokensApplieds: Boolean - - balance: Number - - habitIds: Array - dailyIds: Array - todoIds: Array - rewardIds: Array - - # Removed `filters`, no longer persisting to the database - - flags: - ads: String #FIXME to boolean (currently show/hide) - dropsEnabled: Boolean - itemsEnabled: Boolean - newStuff: String #FIXME to boolean (currently show/hide) - partyEnabled: Boolean - petsEnabled: Boolean - rest: Boolean # FIXME remove? - - history: - exp: [ - date: Date - value: Number - ] - todos: [ - data: Date - value: Number - ] - - invitations: # FIXME remove? - guilds: Array - party: Schema.Types.Mixed - - items: - armor: Number - weapon: Number - head: Number - shield: Number - currentPet: #FIXME - tidy this up, not the best way to store current pet - text: String #Cactus - name: String #Cactus - value: Number #3 - notes: String #"Find a hatching potion to pour on this egg, and one day it will hatch into a loyal pet.", - modifier: String #Skeleton - str: String #Cactus-Skeleton - - eggs: [ - text: String #"Wolf", - name: String #"Wolf", - value: Number #3 - notes: String #"Find a hatching potion to pour on this egg, and one day it will hatch into a loyal pet.", - type: String #"Egg", - dialog: String #"You've found a Wolf Egg! Find a hatching potion to pour on this egg, and one day it will hatch into a loyal pet." }, - ] - hatchingPotions: Array # ["Base", "Skeleton",...] - lastDrop: - date: Date - count: Number - - pets: Array # ["BearCub-Base", "Cactus-Base", ...] - - #FIXME store as Date? - lastCron: {type: Number, 'default': +new Date} - party: # FIXME remove? - current: String #party._id FIXME make these populate docs? - invitation: String #party._id - lastMessageSeen: String #party._id - leader: Boolean - - preferences: - armorSet: String #"v2", - dayStart: Number #"0", FIXME do we need a migration for this? - gender: String # "m", - hair: String #"blond", - hideHeader: Boolean #false, - showHelm: Boolean #true, - skin: String #"white", - timezoneOffset: Number #240 - - profile: - blurb: String #"I made Habit. Don't judge me! It'll get better, I promise", - imageUrl: String #"https://sphotos-a-lga.xx.fbcdn.net/hphotos-ash4/1004403_10152886610690144_825305769_n.jpg", - name: String #"Tyler", - websites: Array #["http://ocdevel.com" ] - - stats: - hp: Number - exp: Number - gp: Number - lvl: Number - - tags: [ - id: String # FIXME use refs? - name: String # "pomodoro" - ] - - # We can't define `tasks` until we move off Derby, since Derby requires dictionary of objects. When we're off, migrate - # to array of subdocs - tasks: Schema.Types.Mixed - # history: {date, value} - # id - # notes - # tags { "4ddf03d9-54bd-41a3-b011-ca1f1d2e9371" : true }, - # text - # type - # up - # down - # value - # completed - # priority: '!!' - # repeat {m: true, t: true} - # streak -, {strict: true}) # 'throw' - -### - Derby requires a strange storage format for somethign called "refLists". Here we hook into loading the data, so we - can provide a more "expected" storage format for our various helper methods. Since the attributes are passed by reference, - the underlying data will be modified too - so when we save back to the database, it saves it in the way Derby likes. - This will go away after the rewrite is complete -### -UserSchema.post 'init', (doc) -> - - # Fix corrupt values, FIXME we can remove this after off Derby - _.each doc.tasks, (task, k) -> - return delete doc.tasks[k] unless task?.id? - task.value = 0 if isNaN(+task.value) - _.each doc.stats, (v,k) -> - doc.stats[k] = 0 if isNaN(+v) - - _.each ['habit','daily','todo','reward'], (type) -> - # we use _.transform instead of a simple _.where in order to maintain sort-order - doc["#{type}s"] = _.transform doc["#{type}Ids"], (result, tid) -> result.push(doc.tasks[tid]) - -#UserSchema.virtual('id').get () -> @_id -UserSchema.methods.toJSON = () -> - doc = @toObject() - doc.id = doc._id - _.each ['habit','daily','todo','reward'], (type) -> - # we use _.transform instead of a simple _.where in order to maintain sort-order - doc["#{type}s"] = _.transform doc["#{type}Ids"], (result, tid) -> result.push(doc.tasks[tid]) - #delete doc["#{type}Ids"] - #delete doc.tasks - doc.filters = {} - doc - -# FIXME - since we're using special @post('init') above, we need to flag when the original path was modified. -# Custom setter/getter virtuals? -UserSchema.pre 'save', (next) -> - @markModified('tasks') - @._v++ #our own version incrementer - next() - -module.exports.schema = UserSchema -module.exports.model = mongoose.model("User", UserSchema) \ No newline at end of file diff --git a/src/models/user.js b/src/models/user.js new file mode 100644 index 0000000000..328591f15b --- /dev/null +++ b/src/models/user.js @@ -0,0 +1,311 @@ +var Schema, UserSchema, helpers, mongoose, _; + +mongoose = require("mongoose"); + +Schema = mongoose.Schema; + +helpers = require('habitrpg-shared/script/helpers'); + +_ = require('lodash'); + +UserSchema = new Schema({ + _id: { + type: String, + 'default': helpers.uuid + }, + apiToken: { + type: String, + 'default': helpers.uuid + }, + /* + # We want to know *every* time an object updates. Mongoose uses __v to designate when an object contains arrays which + # have been updated (http://goo.gl/gQLz41), but we want *every* update + */ + + _v: { + type: Number, + 'default': 0 + }, + achievements: { + originalUser: Boolean, + helpedHabit: Boolean, + ultimateGear: Boolean, + beastMaster: Boolean, + streak: Number + }, + auth: { + facebook: Schema.Types.Mixed, + local: { + email: String, + hashed_password: String, + salt: String, + username: String + }, + timestamps: { + created: { + type: Date, + 'default': Date.now + }, + loggedin: Date + } + }, + /* TODO*/ + + backer: Schema.Types.Mixed, + /* + # tier: Number + # admin: Boolean + # contributor: Boolean + # tokensApplieds: Boolean + */ + + balance: Number, + habitIds: Array, + dailyIds: Array, + todoIds: Array, + rewardIds: Array, + /* Removed `filters`, no longer persisting to the database*/ + + flags: { + ads: String, + dropsEnabled: Boolean, + itemsEnabled: Boolean, + /*FIXME to boolean (currently show/hide)*/ + + newStuff: String, + partyEnabled: Boolean, + petsEnabled: Boolean, + /* FIXME remove?*/ + + rest: Boolean + }, + history: { + exp: [ + { + date: Date, + value: Number + } + ], + todos: [ + { + data: Date, + value: Number + } + ] + }, + /* FIXME remove?*/ + + invitations: { + guilds: Array, + party: Schema.Types.Mixed + }, + items: { + armor: Number, + weapon: Number, + head: Number, + shield: Number, + /*FIXME - tidy this up, not the best way to store current pet*/ + + currentPet: { + /*Cactus*/ + + text: String, + /*Cactus*/ + + name: String, + /*3*/ + + value: Number, + /*"Find a hatching potion to pour on this egg, and one day it will hatch into a loyal pet.",*/ + + notes: String, + /*Skeleton*/ + + modifier: String, + /*Cactus-Skeleton*/ + + str: String + }, + eggs: [ + { + /*"Wolf",*/ + + text: String, + /*"Wolf",*/ + + name: String, + /*3*/ + + value: Number, + /*"Find a hatching potion to pour on this egg, and one day it will hatch into a loyal pet.",*/ + + notes: String, + /*"Egg",*/ + + type: String, + /*"You've found a Wolf Egg! Find a hatching potion to pour on this egg, and one day it will hatch into a loyal pet." },*/ + + dialog: String + } + ], + /* ["Base", "Skeleton",...]*/ + + hatchingPotions: Array, + lastDrop: { + date: Date, + count: Number + }, + /* ["BearCub-Base", "Cactus-Base", ...]*/ + + pets: Array + }, + /*FIXME store as Date?*/ + + lastCron: { + type: Number, + 'default': +(new Date) + }, + /* FIXME remove?*/ + + party: { + /*party._id FIXME make these populate docs?*/ + + current: String, + /*party._id*/ + + invitation: String, + /*party._id*/ + + lastMessageSeen: String, + leader: Boolean + }, + preferences: { + armorSet: String, + dayStart: Number, + gender: String, + hair: String, + hideHeader: Boolean, + showHelm: Boolean, + skin: String, + timezoneOffset: Number + }, + profile: { + blurb: String, + imageUrl: String, + name: String, + /*["http://ocdevel.com" ]*/ + + websites: Array + }, + stats: { + hp: Number, + exp: Number, + gp: Number, + lvl: Number + }, + tags: [ + { + /* FIXME use refs?*/ + + id: String, + name: String + } + ], + /* + # We can't define `tasks` until we move off Derby, since Derby requires dictionary of objects. When we're off, migrate + # to array of subdocs + */ + + tasks: Schema.Types.Mixed + /* + # history: {date, value} + # id + # notes + # tags { "4ddf03d9-54bd-41a3-b011-ca1f1d2e9371" : true }, + # text + # type + # up + # down + # value + # completed + # priority: '!!' + # repeat {m: true, t: true} + # streak + */ + +}, { + strict: true +}); + +/* + Derby requires a strange storage format for somethign called "refLists". Here we hook into loading the data, so we + can provide a more "expected" storage format for our various helper methods. Since the attributes are passed by reference, + the underlying data will be modified too - so when we save back to the database, it saves it in the way Derby likes. + This will go away after the rewrite is complete +*/ + + +UserSchema.post('init', function(doc) { + /* Fix corrupt values, FIXME we can remove this after off Derby*/ + + _.each(doc.tasks, function(task, k) { + if ((task != null ? task.id : void 0) == null) { + return delete doc.tasks[k]; + } + if (isNaN(+task.value)) { + return task.value = 0; + } + }); + _.each(doc.stats, function(v, k) { + if (isNaN(+v)) { + return doc.stats[k] = 0; + } + }); + return _.each(['habit', 'daily', 'todo', 'reward'], function(type) { + /* we use _.transform instead of a simple _.where in order to maintain sort-order*/ + + return doc["" + type + "s"] = _.transform(doc["" + type + "Ids"], function(result, tid) { + return result.push(doc.tasks[tid]); + }); + }); +}); + +/*UserSchema.virtual('id').get () -> @_id*/ + + +UserSchema.methods.toJSON = function() { + var doc; + doc = this.toObject(); + doc.id = doc._id; + _.each(['habit', 'daily', 'todo', 'reward'], function(type) { + /* we use _.transform instead of a simple _.where in order to maintain sort-order*/ + + return doc["" + type + "s"] = _.transform(doc["" + type + "Ids"], function(result, tid) { + return result.push(doc.tasks[tid]); + }); + /*delete doc["#{type}Ids"]*/ + + }); + /*delete doc.tasks*/ + + doc.filters = {}; + return doc; +}; + +/* +# FIXME - since we're using special @post('init') above, we need to flag when the original path was modified. +# Custom setter/getter virtuals? +*/ + + +UserSchema.pre('save', function(next) { + this.markModified('tasks'); + /*our own version incrementer*/ + + this._v++; + return next(); +}); + +module.exports.schema = UserSchema; + +module.exports.model = mongoose.model("User", UserSchema); \ No newline at end of file diff --git a/src/routes/api.js b/src/routes/api.js index 6c9f170c4f..78f8de84c5 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -49,6 +49,7 @@ router.put('/user', auth, cron, api.updateUser); router.post('/user/revive', auth, cron, api.revive); router.post('/user/batch-update', auth, cron, api.batchUpdate); router.post('/user/reroll', auth, cron, api.reroll); +router.post('/user/buy-gems', auth, api.buyGems); /* Groups*/ router.get('/groups', auth, api.getGroups); diff --git a/src/routes/pages.coffee b/src/routes/pages.coffee deleted file mode 100644 index bbdcff4fe8..0000000000 --- a/src/routes/pages.coffee +++ /dev/null @@ -1,14 +0,0 @@ -nconf = require('nconf') -express = require 'express' -router = new express.Router() -_ = require('lodash') - -router.get '/', (req, res) -> - res.render 'index', - title: 'HabitRPG | Your Life, The Role Playing Game' - env: res.locals.habitrpg - -router.get '/partials/tasks', (req, res) -> res.render 'tasks/index' -router.get '/partials/options', (req, res) -> res.render 'options' - -module.exports = router \ No newline at end of file diff --git a/src/routes/pages.js b/src/routes/pages.js new file mode 100644 index 0000000000..486dc92dcb --- /dev/null +++ b/src/routes/pages.js @@ -0,0 +1,21 @@ +var nconf = require('nconf'); +var express = require('express'); +var router = new express.Router(); +var _ = require('lodash'); + +router.get('/', function(req, res) { + return res.render('index', { + title: 'HabitRPG | Your Life, The Role Playing Game', + env: res.locals.habitrpg + }); +}); + +router.get('/partials/tasks', function(req, res) { + return res.render('tasks/index'); +}); + +router.get('/partials/options', function(req, res) { + return res.render('options'); +}); + +module.exports = router; \ No newline at end of file diff --git a/views/shared/gems.jade b/views/shared/gems.jade index 4a107a2ca3..f276a47528 100644 --- a/views/shared/gems.jade +++ b/views/shared/gems.jade @@ -1,5 +1,5 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Gems', popover="Used for buying special items (reroll, eggs, hatching potions, etc). You'll need to unlock those features before being able to use Gems.", popover-placement='bottom') - span.task-action-btn.tile.flush.bright.add-gems-btn(x-bind='click:showStripe') + + span.task-action-btn.tile.flush.bright.add-gems-btn(ng-click='showStripe()') + span.task-action-btn.tile.flush.neutral .Gems - | {{gems(_user.balance)}} Gems \ No newline at end of file + | {{user.balance * 4 | number:0}} Gems \ No newline at end of file diff --git a/views/shared/modals/reroll.jade b/views/shared/modals/reroll.jade index 8b619996dd..4f48f8f4a6 100644 --- a/views/shared/modals/reroll.jade +++ b/views/shared/modals/reroll.jade @@ -11,9 +11,9 @@ div(modal='modals.reroll') .modal-footer span(ng-if='user.balance < 1') - a.btn.btn-success.btn-large(data-dismiss="modal", x-bind="click:showStripe") Buy More Gems + a.btn.btn-success.btn-large(ng-click="showStripe()") Buy More Gems span.gem-cost Not enough Gems span(ng-if='user.balance >= 1', ng-controller='SettingsCtrl') - a.btn.btn-danger.btn-large(data-dismiss="modal", ng-click='reroll()') Re-Roll + a.btn.btn-danger.btn-large(ng-click='reroll()') Re-Roll span.gem-cost 4 Gems diff --git a/views/tasks/ads.jade b/views/tasks/ads.jade index 6a5281588b..4337c96f8f 100644 --- a/views/tasks/ads.jade +++ b/views/tasks/ads.jade @@ -1,6 +1,6 @@ div(ng-if='authenticated() && user.flags.ads!="hide"') span.pull-right(ng-if='list.type!="reward"') - a(x-bind='click:showStripe', tooltip='Remove Ads', ng-click='notPorted()') + a(ng-click='showStripe()', tooltip='Remove Ads') i.icon-remove br a(ng-click='modals.whyAds=true', tooltip='Why Ads?')