diff --git a/.gitignore b/.gitignore index 01f730f678..c91cccb781 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ node_modules *.swp .idea* config.json -npm-debug.log \ No newline at end of file +npm-debug.log +lib \ No newline at end of file diff --git a/package.json b/package.json index 726c66b963..d2539cad14 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "derby": "git://github.com/lefnire/derby#habitrpg", "racer": "git://github.com/lefnire/racer#habitrpg", "racer-db-mongo": "git://github.com/lefnire/racer-db-mongo#habitrpg", - "derby-ui-boot": "git://github.com/codeparty/derby-ui-boot#f04ba2c7e7b3a8f1462a6e70fe4f81055d74c4b2", + "derby-ui-boot": "git://github.com/HabitRPG/derby-ui-boot#habit0.3", "derby-auth": "git://github.com/lefnire/derby-auth#master", "connect-mongo": "*", "passport-facebook": "*", @@ -26,7 +26,7 @@ "expect.js": "~0.2.0", "derby-i18n": "git://github.com/switz/derby-i18n#master", "relative-date": "~1.1.1", - "lodash": "~1.2.1", + "lodash": "~1.3.1", "async": "~0.2.9", "optimist": "~0.5.2" }, diff --git a/public/500.html b/public/500.html index fb28f601f6..b03f0740bb 100644 --- a/public/500.html +++ b/public/500.html @@ -11,7 +11,7 @@ - + -->
- -

The server is restarting.

+ +

The server is respawning.

Try again in a few. We restart often due to this issue, and we're rewriting the site to fix it. (AngularJS developers, come join us!)

If this page persists, the server may be experiencing issues; the developers have been notified. Try switching to the beta site or the main site.

diff --git a/src/app/misc.coffee b/src/app/misc.coffee index ce14ca9f2a..ada432134f 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -3,22 +3,26 @@ algos = require 'habitrpg-shared/script/algos' items = require('habitrpg-shared/script/items').items helpers = require('habitrpg-shared/script/helpers') -module.exports.batchTxn = batchTxn = (model, cb, options) -> - user = model.at("_user") - uObj = hydrate(user.get()) # see https://github.com/codeparty/racer/issues/116 +module.exports.batchTxn = batchTxn = (model, cb, options={}) -> + _.defaults options, {user: model.at("_user"), cron: false, done: ->} + {user} = options + # see https://github.com/codeparty/racer/issues/116 + uObj = helpers.hydrate user.get() batch = set: (k,v) -> helpers.dotSet(k,v,uObj); paths[k] = true get: (k) -> helpers.dotGet(k,uObj) paths = {} model._dontPersist = true ret = cb uObj, paths, batch - _.each paths, (v,k) -> user.pass({cron:options?.cron}).set(k,helpers.dotGet(k, uObj));true + console.log {cron: options.cron} + _.each paths, (v,k) -> user.pass({cron:options.cron}).set(k,batch.get(k));true model._dontPersist = false # some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js # pass true if we have levelled to supress xp notification unless _.isEmpty paths - setOps = _.reduce paths, ((m,v,k)-> m[k] = helpers.dotGet(k,uObj);m), {} - user.set "update__", setOps, options?.done + setOps = _.reduce paths, ((m,v,k)-> m[k] = batch.get(k);m), {} + user.set "update__", setOps, options.done + else options.done() ret #TODO put this in habitrpg-shared @@ -86,18 +90,6 @@ module.exports.score = (model, taskId, direction, allowUndo=false) -> delta -### - Make sure model.get() returns all properties, see https://github.com/codeparty/racer/issues/116 -### -module.exports.hydrate = hydrate = (spec) -> - if _.isObject(spec) and !_.isArray(spec) - hydrated = {} - keys = _.keys(spec).concat(_.keys(spec.__proto__)) - keys.forEach (k) -> hydrated[k] = hydrate(spec[k]) - hydrated - else spec - - ### Cleanup task-corruption (null tasks, rogue/invisible tasks, etc) Obviously none of this should be happening, but we'll stop-gap until we can find & fix diff --git a/src/app/profile.coffee b/src/app/profile.coffee index 4577ba1da7..97714561f3 100644 --- a/src/app/profile.coffee +++ b/src/app/profile.coffee @@ -9,24 +9,9 @@ module.exports.app = (appExports, model) -> user = model.at('_user') appExports.revive = -> - # Reset stats - user.set 'stats.hp', 50 - user.set 'stats.exp', 0 - user.set 'stats.gp', 0 - user.incr 'stats.lvl', -1 if user.get('stats.lvl') > 1 - - ## Lose a random item - loseThisItem = false - owned = user.get('items') - # unless they're already at 0-everything - if parseInt(owned.armor)>0 or parseInt(owned.head)>0 or parseInt(owned.shield)>0 or parseInt(owned.weapon)>0 - # find a random item to lose - until loseThisItem - #candidate = {0:'items.armor', 1:'items.head', 2:'items.shield', 3:'items.weapon', 4:'stats.gp'}[Math.random()*5|0] - candidate = {0:'armor', 1:'head', 2:'shield', 3:'weapon'}[Math.random()*4|0] - loseThisItem = candidate if owned[candidate] > 0 - user.set "items.#{loseThisItem}", 0 - + [uObj, paths] = [user.get(), {}] + algos.revive(uObj, {paths}) + _.each paths, ((v,k) -> user.set k, helpers.dotGet(k, uObj)) items.updateStore(model) appExports.reset = (e, el) -> @@ -96,4 +81,3 @@ module.exports.app = (appExports, model) -> appExports.toggleResting = -> model.set '_user.flags.rest', !model.get('_user.flags.rest') - diff --git a/src/server/api.coffee b/src/server/api.coffee index 48c812dbb2..a234e63a23 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -1,171 +1,141 @@ -express = require 'express' -router = new express.Router() +# @see ./routes.coffee for routing _ = require 'lodash' +async = require 'async' algos = require 'habitrpg-shared/script/algos' helpers = require 'habitrpg-shared/script/helpers' validator = require 'derby-auth/node_modules/validator' check = validator.check sanitize = validator.sanitize utils = require 'derby-auth/utils' +misc = require '../app/misc' + +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." -addTask = (user, task) -> - task.type ?= 'habit' - tid = user.add "tasks", task - user.push "#{task.type}Ids", tid - -# ---------- /api/v1 API ------------ -# Every url added beneath router is prefaced by /api/v1 - -### - v1 API. Requires api-v1-user (user id) and api-v1-key (api key) headers, Test with: - $ cd node_modules/racer && npm install && cd ../.. - $ mocha test/api.mocha.coffee -### - -### - API Status -### -router.get '/status', (req, res) -> - res.json status: 'up' - ### beforeEach auth interceptor ### -auth = (req, res, next) -> +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 || token + return res.json 401, NO_TOKEN_OR_UID unless uid and token model = req.getModel() query = model.query('users').withIdAndToken(uid, token) query.fetch (err, user) -> return res.json err: err if err - req.user = user - req.userObj = user.get() - return res.json 401, NO_USER_FOUND if _.isEmpty(req.userObj) + (req.habit ?= {}).user = user + return res.json 401, NO_USER_FOUND if _.isEmpty(user.get()) req._isServer = true next() ### - GET /user -### -router.get '/user', auth, (req, res) -> - user = req.userObj - - 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 user - -### - TODO POST /user - when a put attempt didn't work, create a new one with POST + ------------------------------------------------------------------------ + Tasks + ------------------------------------------------------------------------ ### -### - PUT /user -### -router.put '/user', auth, (req, res) -> - user = req.user - partialUser = req.body.user +addTask = (user, task, cb) -> + task.type ?= 'habit' + tid = user.add "tasks", task, -> + ids = user.get "#{task.type}Ids" + ids.unshift tid + user.set "#{task.type}Ids", ids, cb - # REVISIT is this the best way of handling protected v acceptable attr mass-setting? Possible pitfalls: (1) we have to remember - # to update here when we add new schema attrs in the future, (2) developers can't assign random variables (which - # is currently beneficial for Kevin & Paul). Pros: protects accidental or malicious user data corruption +deleteTask = (user, task, cb) -> + user.del "tasks.#{task.id}", -> + taskIds = user.get "#{task.type}Ids" + user.remove "#{task.type}Ids", taskIds.indexOf(task.id), 1, cb - # TODO - this accounts for single-nested items (stats.hp, stats.exp) but will clobber any other depth. - # See http://stackoverflow.com/a/6394168/362790 for when we need to cross that road - - acceptableAttrs = ['flags', 'history', 'items', 'preferences', 'profile', 'stats'] - user.set 'lastCron', partialUser.lastCron if partialUser.lastCron? - _.each acceptableAttrs, (attr) -> - _.each partialUser[attr], (val, key) -> user.set("#{attr}.#{key}", val);true - - updateTasks partialUser.tasks, req.user, req.getModel() if partialUser.tasks? - - userObj = user.get() - userObj.tasks = _.toArray(userObj.tasks) # FIXME figure out how we're going to consistently handle this. should always be array - res.json 201, userObj +score = (model, user, taskId, direction, done) -> + delta = 0 + misc.batchTxn model, (uObj, paths) -> + tObj = uObj.tasks[taskId] + delta = algos.score(uObj, tObj, direction, {paths}) + #, {user, done} + , {user, done} + delta ### - POST /user/auth/local + 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 ### -router.post '/user/auth/local', (req, res) -> - username = req.body.username - password = req.body.password - return res.json 401, err: 'No username or password' unless username and password +api.scoreTask = (req, res, next) -> + {id, direction} = req.params + {title, service, type} = req.body + type ||= 'habit' - model = req.getModel() + # 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'] - q = model.query("users").withUsername(username) - q.fetch (err, result1) -> - return res.json 401, { err } if err - u1 = result1.get() - return res.json 401, err: 'Username not found' unless u1 # user not found + {user} = req.habit - # We needed the whole user object first so we can get his salt to encrypt password comparison - q = model.query("users").withLogin(username, utils.encryptPassword(password, u1.auth.local.salt)) - q.fetch (err, result2) -> - return res.json 401, { err } if err + done = -> + # TODO - could modify batchTxn to conform to this better + delta = score req.getModel(), user, id, direction, -> + result = user.get('stats') + req.habit.result = data: _.extend(result, delta: delta) + next() - # joshua tree? - u2 = result2.get() - return res.json 401, err: 'Incorrect password' unless u2 + # Set completed if type is daily or todo and task exists + if (existing = user.at "tasks.#{id}").get() + if existing.get('type') in ['daily', 'todo'] + existing.set 'completed', (direction is 'up'), done + else done() - res.json - id: u2.id - token: u2.apiToken + # If it doesn't exist, this is likely a 3rd party up/down - create a new one + else + task = {id, type, value: 0} + task.text = title or id + task.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 type is 'habit' + task.up = true + task.down = true + if type in ['daily', 'todo'] + task.completed = direction is 'up' + addTask user, task, done ### - POST /user/auth/facebook + Get all tasks ### -router.post '/user/auth/facebook', (req, res) -> - {facebook_id, email, name} = req.body - return res.json 401, err: 'No facebook id provided' unless facebook_id - model = req.getModel() - q = model.query("users").withProvider('facebook', facebook_id) - q.fetch (err, result) -> - return res.json 401, { err } if err - u = result.get() - console.log {facebook_id, u} - if u - return res.json - id: u.id - token: u.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." - +api.getTasks = (req, res, next) -> + types = + if /^(habit|todo|daily|reward)$/.test(req.query.type) then [req.query.type] + else ['habit','todo','daily','reward'] + tasks = _.toArray (_.filter req.habit.user.get('tasks'), (t)-> t.type in types) + req.habit.result = data: tasks + next() ### - GET /user/task/:id + Get Task ### -router.get '/user/task/:id', auth, (req, res) -> - task = req.userObj.tasks[req.params.id] +api.getTask = (req, res, next) -> + task = req.habit.user.get "tasks.#{req.params.id}" return res.json 400, err: "No task found." if !task || _.isEmpty(task) - - res.json 200, task + req.habit.result = data: task + next() ### - validate task + Validate task ### -validateTask = (req, res, next) -> +api.validateTask = (req, res, next) -> task = {} newTask = { type, text, notes, value, up, down, completed } = req.body # If we're updating, get the task from the user if req.method is 'PUT' or req.method is 'DELETE' - task = req.userObj?.tasks[req.params.id] + task = req.habit.user.get "tasks.#{req.params.id}" return res.json 400, err: "No task found." if !task || _.isEmpty(task) # Strip for now type = undefined @@ -187,31 +157,19 @@ validateTask = (req, res, next) -> newTask.completed = false unless typeof completed is 'boolean' _.extend task, newTask - req.task = task + req.habit.task = task next() ### - PUT /user/task/:id + Delete Task ### -router.put '/user/task/:id', auth, validateTask, (req, res) -> - req.user.set "tasks.#{req.task.id}", req.task - - res.json 200, req.task +api.deleteTask = (req, res, next) -> + deleteTask req.habit.user, req.habit.task, -> + req.habit.result = code: 204 + next() ### - DELETE /user/task/:id -### -router.delete '/user/task/:id', auth, validateTask, (req, res) -> - taskIds = req.user.get "#{req.task.type}Ids" - - req.user.del "tasks.#{req.task.id}" - # Remove one id from array of typeIds - req.user.remove "#{req.task.type}Ids", taskIds.indexOf(req.task.id), 1 - - res.send 204 - -### - POST /user/tasks + Helper function for updating multiple tasks ### updateTasks = (tasks, user, model) -> for idx, task of tasks @@ -232,87 +190,191 @@ updateTasks = (tasks, user, model) -> tasks[idx] = task return tasks -router.post '/user/tasks', auth, (req, res) -> - tasks = updateTasks req.body, req.user, req.getModel() - res.json 201, tasks - +### + Update Task +### +api.updateTask = (req, res, next) -> + req.habit.user.set "tasks.#{req.habit.task.id}", req.habit.task + req.habit.result = data: req.habit.task + next() ### - POST /user/task/ + Update tasks (plural). This will update, add new, delete, etc all at once. + Should we keep this? ### -router.post '/user/task', auth, validateTask, (req, res) -> - task = req.task - addTask req.user, task - res.json 201, task +api.updateTasks = (req, res, next) -> + tasks = updateTasks req.body, req.habit.user, req.getModel() + req.habit.result = code: 201, data: tasks + next() + +api.createTask = (req, res, next) -> + task = req.habit.task + addTask req.habit.user, task, -> + req.habit.result = code: 201, data: task + next() + +api.sortTask = (req, res, next) -> + {id} = req.params + {to, from, type} = req.habit.task + {user} = req.habit + path = "#{type}Ids" + a = user.get(path) + a.splice(to, 0, a.splice(from, 1)[0]) + user.set path, a, next ### - GET /user/tasks + ------------------------------------------------------------------------ + User + ------------------------------------------------------------------------ ### -router.get '/user/tasks', auth, (req, res) -> - return res.json 400, NO_USER_FOUND if _.isEmpty(req.userObj) - - types = - if /^(habit|todo|daily|reward)$/.test(req.query.type) then [req.query.type] - else ['habit','todo','daily','reward'] - tasks = _.toArray (_.filter req.user.get('tasks'), (t)-> t.type in types) - - res.json 200, tasks ### - This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login + Get User ### -scoreTask = (req, res, next) -> - {taskId, direction} = req.params - {title, service, icon, type} = req.body - type ||= 'habit' +api.getUser = (req, res, next) -> + uObj = req.habit.user.get() - # Send error responses for improper API call - return res.send(500, ':taskId required') unless taskId - return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] + uObj.stats.toNextLevel = algos.tnl uObj.stats.lvl + uObj.stats.maxHealth = 50 + + delete uObj.apiToken + if uObj.auth + delete uObj.auth.hashed_password + delete uObj.auth.salt + + req.habit.result = data: uObj + next() + +### + Register new user with uname / password +### +api.loginLocal = (req, res, next) -> + username = req.body.username + password = req.body.password + return res.json 401, err: 'No username or password' unless username and password model = req.getModel() - {user, userObj} = req - existingTask = user.at "tasks.#{taskId}" - # TODO add service & icon to task - # If task exists, set it's compltion - if existingTask.get() - # Set completed if type is daily or todo - existingTask.set 'completed', (direction is 'up') if /^(daily|todo)$/.test existingTask.get('type') - else - task = - id: taskId - type: type - text: (title || taskId) - value: 0 - 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." + q = model.query("users").withUsername(username) + q.fetch (err, result1) -> + return res.json 401, { err } if err + u1 = result1.get() + return res.json 401, err: 'Username not found' unless u1 # user not found - switch type - when 'habit' - task.up = true - task.down = true - when 'daily', 'todo' - task.completed = direction is 'up' + # We needed the whole user object first so we can get his salt to encrypt password comparison + q = model.query("users").withLogin(username, utils.encryptPassword(password, u1.auth.local.salt)) + q.fetch (err, result2) -> + return res.json 401, { err } if err - addTask user, task + # joshua tree? + u2 = result2.get() + return res.json 401, err: 'Incorrect password' unless u2 - # TODO - could modify batchTxn to conform to this better - uObj = req.user.get() - tObj = uObj.tasks[taskId] - paths = {} - delta = algos.score(uObj, tObj, direction, {paths}) - _.each paths, (v,k) -> user.set(k,helpers.dotGet(k, uObj));true - - result = uObj.stats - result.delta = delta - res.json result + req.habit ?= {} + req.habit.result = data: + id: u2.id + token: u2.apiToken + next() ### - POST /user/tasks/:taskId/:direction + POST /user/auth/facebook ### -router.post '/user/task/:taskId/:direction', auth, scoreTask -router.post '/user/tasks/:taskId/:direction', auth, scoreTask +api.loginFacebook = (req, res, next) -> + {facebook_id, email, name} = req.body + return res.json 401, err: 'No facebook id provided' unless facebook_id + model = req.getModel() + q = model.query("users").withProvider('facebook', facebook_id) + q.fetch (err, result) -> + return res.json 401, { err } if err + u = result.get() + if u + req.habit ?= {} + req.habit.result = data: + id: u.id + token: u.apiToken + next() + 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} = req.habit + + # 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 + # Note: custom is for 3rd party apps + acceptableAttrs = 'tasks achievements filters flags invitations items lastCron party preferences profile stats tags custom'.split(' ') + series = [] + _.each req.body, (v, k) -> + if (_.find acceptableAttrs, (attr)-> k.indexOf(attr) is 0)? + series.push (cb) -> req.habit.user.set(k, v, cb) + async.series series, (err) -> + return next(err) if err + req.habit.result = data: helpers.derbyUserToAPI(user) + next() + +api.cron = (req, res, next) -> + {user} = req.habit + misc.batchTxn req.getModel(), (uObj, paths) -> + uObj = helpers.derbyUserToAPI(uObj, {asScope:false}) + algos.cron uObj, {paths} + , {user, done:next, cron:true} + +api.revive = (req, res, next) -> + {user} = req.habit + done = -> + req.habit.result = data: helpers.derbyUserToAPI(user) + next() + misc.batchTxn req.getModel(), (uObj, paths) -> + algos.revive uObj, {paths} + , {user, done} + + +### + ------------------------------------------------------------------------ + Batch Update + Run a bunch of updates all at once + ------------------------------------------------------------------------ +### +api.batchUpdate = (req, res, next) -> + {user} = req.habit + + performAction = (action, cb) -> + req.params.id = action.data?.id + req.params.direction = action.dir + req.body = action.data + + switch action.op + when "score" + api.scoreTask(req, res, cb) + when "sortTask" + api.sortTask(req, res, cb) + when "addTask" + api.validateTask req, res, -> + api.createTask(req, res, cb) + when "delTask" + api.validateTask req, res, -> + api.deleteTask(req, res, cb) + when "set" + api.updateUser(req, res, cb) + when "revive" + api.revive(req, res, cb) + 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) -> + return res.json 500, {err} if err + res.json helpers.derbyUserToAPI(user) + console.log "Reply sent" -module.exports = router -module.exports.auth = auth -module.exports.scoreTask = scoreTask # export so deprecated can call it diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee deleted file mode 100644 index db4e2d1a02..0000000000 --- a/src/server/apiv2.coffee +++ /dev/null @@ -1,61 +0,0 @@ -express = require 'express' -router = new express.Router() -util = require('util') - -_ = require 'lodash' -algos = require 'habitrpg-shared/script/algos' -helpers = require 'habitrpg-shared/script/helpers' -validator = require 'derby-auth/node_modules/validator' -check = validator.check -sanitize = validator.sanitize -misc = require '../app/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." - -# ---------- /api/v1 API ------------ -# Every url added beneath router is prefaced by /api/v2 - -### - API Status -### -router.get '/status', (req, res) -> - res.json status: 'up' - -### - beforeEach auth interceptor -### -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 || token - - model = req.getModel() - query = model.query('users').withIdAndToken(uid, token) - - query.fetch (err, user) -> - return res.json err: err if err - req.user = user - req.userObj = user.get() - return res.json 401, NO_USER_FOUND if !req.userObj || _.isEmpty(req.userObj) - req._isServer = true - next() - -### -POST new actions -### -router.post '/', auth, (req, res) -> - actions = req.body - if _.isArray actions - actions.forEach (action)-> - switch action.op - when score - {} - when newTask - req.user.set "tasks.#{req.task.id}", action.task - - console.log util.inspect req.body - - res.json 200, req.userObj - -module.exports = router \ No newline at end of file diff --git a/src/server/index.coffee b/src/server/index.coffee index 4c67ea04ba..b235c949eb 100644 --- a/src/server/index.coffee +++ b/src/server/index.coffee @@ -76,8 +76,7 @@ mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, -> .use(store.modelMiddleware()) .use(middleware.translate) # API should be hit before all other routes - .use('/api/v1', require('./api').middleware) - .use('/api/v2', require('./apiv2').middleware) + .use('/api/v1', require('./routes').middleware) .use(require('./deprecated').middleware) # Show splash page for newcomers .use(middleware.splash) diff --git a/src/server/routes.coffee b/src/server/routes.coffee new file mode 100644 index 0000000000..db82857d0f --- /dev/null +++ b/src/server/routes.coffee @@ -0,0 +1,52 @@ +express = require 'express' +router = new express.Router() +api = require './api' + +### + ---------- /api/v1 API ------------ + Every url added to router is prefaced by /api/v1 + See ./routes/coffee for routes + + v1 API. Requires x-api-user (user id) and x-api-key (api key) headers, Test with: + $ cd node_modules/racer && npm install && cd ../.. + $ mocha test/api.mocha.coffee +### + +{auth, validateTask, cron} = api + +### + We don't want the api functions to actually res.send results (unless there was an error) + because we'll be re-using the same functions when apiv2 rolls around, but returning different results. + So handle sending results for apiv1 here +### +v1Send = (req, res, next) -> + {result} = req.habit + if (result.code and result.data) then res.json result.code, result.data + else if result.code then res.send result.code + else if result.data then res.json result.data + else res.send 200 + +router.get '/status', (req, res) -> res.json status: 'up' + +# Scoring +router.post '/user/task/:id/:direction', auth, cron, api.scoreTask, v1Send +router.post '/user/tasks/:id/:direction', auth, cron, api.scoreTask, v1Send + +# Tasks +router.get '/user/tasks', auth, cron, api.getTasks, v1Send # plural +router.get '/user/task/:id', auth, cron, api.getTask, v1Send +router.put '/user/task/:id', auth, cron, validateTask, api.updateTask, v1Send +router.post '/user/tasks', auth, cron, api.updateTasks, v1Send # plural +router.delete '/user/task/:id', auth, cron, validateTask, api.deleteTask, v1Send +router.post '/user/task', auth, cron, validateTask, api.createTask, v1Send +router.put '/user/task/:id/sort', auth, cron, validateTask, api.sortTask, v1Send + +# User +router.get '/user', auth, cron, api.getUser, v1Send +router.post '/user/auth/local', api.loginLocal, v1Send +router.post '/user/auth/facebook', api.loginFacebook, v1Send +router.put '/user', auth, cron, api.updateUser, v1Send +router.post '/user/revive', auth, cron, api.revive, v1Send +router.post '/user/batch-update', auth, cron, api.batchUpdate # this one we're handling specially + +module.exports = router \ No newline at end of file diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 0b10eb7a93..8b0656f6b6 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -379,7 +379,7 @@ describe 'API', -> .send(user: userUpdates) .end (res) -> expect(res.body.err).to.be undefined - expect(res.statusCode).to.be 201 + expect(res.statusCode).to.be 200 tasks = res.body.tasks expect(_.find(tasks,{id:habitId})).to.eql {id: habitId,text: 'hello2',notes: 'note2'} @@ -398,7 +398,6 @@ describe 'API', -> expect(user.get("tasks.#{foundNewTask.id}")).to.eql id: foundNewTask.id, text: 'new task2', notes: 'notes2' done() - it 'POST /api/v1/user/auth/local', (done) -> userAuth = username: username @@ -422,7 +421,7 @@ describe 'API', -> id: userAuth.facebook_id name: userAuth.name email: userAuth.email - console.log {newUser} + #console.log {newUser} model.set "users.#{id}", newUser, -> request.post("#{baseURL}/user/auth/facebook") @@ -434,3 +433,26 @@ describe 'API', -> expect(res.body.id).to.be newUser.id #expect(res.body.token).to.be newUser.apiToken done() + + it 'PUT /api/v1/batch-update', (done) -> + userBefore = {} +# user.set "lastCron", +new Date #FIXME this shouldn't be handled here + query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken) + query.fetch (err, user) -> userBefore = user.get() + #console.log {userBefore} + + jsonRaw = + [{"op":"score","task":{"completed":true,"date":null,"down":null,"id":"049ee706-7992-408f-8bdd-a0f87b6cddee","notes":null,"price":null,"priority":null,"streak":1,"text":"asdasd","type":"daily","up":null,"value":-15.159032750819472},"dir":"down"},{"op":"score","task":{"completed":true,"date":null,"down":null,"id":"049ee706-7992-408f-8bdd-a0f87b6cddee","notes":null,"price":null,"priority":null,"streak":1,"text":"asdasd","type":"daily","up":null,"value":-15.159032750819472},"dir":"up"},{},{},{},{},{"op":"score","task":{"completed":true,"date":null,"down":null,"id":"049ee706-7992-408f-8bdd-a0f87b6cddee","notes":null,"price":null,"priority":null,"streak":1,"text":"asdasd","type":"daily","up":null,"value":-16.63136866553572},"dir":"down"},{"op":"score","task":{"completed":true,"date":null,"down":null,"id":"049ee706-7992-408f-8bdd-a0f87b6cddee","notes":null,"price":null,"priority":null,"streak":1,"text":"asdasd","type":"daily","up":null,"value":-16.63136866553572},"dir":"up"},{},{},{"op":"score","task":{"completed":true,"date":null,"down":null,"history":[{"date":1370796966979,"value":-1.9263318037820194},{"date":1371394179245,"value":-9.632667221983818},{"date":1371987764419,"value":-8.899142684639843},{"date":1371901709065,"value":-8.697714139260505},{"date":1371987764419,"value":-9.947389352351902},{"date":1372072605105,"value":-8.65704735207246},{"date":1372185464158,"value":-9.905420946093672},{"date":1372348155721,"value":-8.616465915449927},{"date":1372404365115,"value":-7.369389857231249},{"date":1372619315210,"value":-6.16153655829917},{"date":1372797766170,"value":-4.990495966065229},{"date":1373266931264,"value":-12.356300657493207},{"date":1373322727209,"value":-10.983796434663102},{"date":1373407801484,"value":-9.658725758132753},{"date":1373639117325,"value":-8.377893411957398},{"date":1373719671601,"value":-7.138418159258412},{"date":1373826521297,"value":-5.902428899644443},{"date":1373839445467,"value":-8.264210410818091},{"date":1373929050162,"value":-8.224444248693572},{"date":1374058202835,"value":-9.459055183497052},{"date":1374102966396,"value":-8.184759693251706},{"date":1374187619046,"value":-6.951403643619735},{"date":1374342649005,"value":-5.72148344218024},{"date":1374434356841,"value":-8.072174632205511},{"date":1374562742400,"value":-10.571154014907808},{"date":1374886027789,"value":-16.097395454285916},{"date":1375011848715,"value":-17.607991906162557},{"date":1375436884647,"value":-16.037773949824242},{"date":1375563074478,"value":-17.546064223707074},{"date":1375568230260,"value":-19.11379232897715},{"date":1375734631073,"value":-20.745784396408645},{"date":1375785010434,"value":-18.767794224069412},{"date":1375826853480,"value":-18.636651213045212}],"id":"fe4b9061-eb58-468c-9b25-10c72be772e6","notes":"","price":null,"priority":null,"repeat":{"su":true,"m":true,"t":true,"w":true,"th":true,"f":true,"s":true},"streak":1,"tags":{"40492758-1202-4d85-8cb3-d40e45f4dd1d":false},"text":"Read 50 pages","type":"daily","up":null,"value":-17.024492021142215},"dir":"up"},{},{},{}] + + request.put("http://localhost:1337/api/v1/batch-update") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .send(jsonRaw) + .end (res) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + tasks = res.body.tasks + + done() +