mirror of
https://github.com/sudoxnym/habitica.git
synced 2026-08-05 03:52:14 +00:00
Merge branch 'develop'
This commit is contained in:
commit
aeee52a0e7
10 changed files with 358 additions and 307 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -5,4 +5,5 @@ node_modules
|
|||
*.swp
|
||||
.idea*
|
||||
config.json
|
||||
npm-debug.log
|
||||
npm-debug.log
|
||||
lib
|
||||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<link href="/vendor/bootstrap/docs/assets/css/docs.css" rel="stylesheet">
|
||||
<link href="/css/static-pages.css" rel="stylesheet">
|
||||
|
||||
<style type="text/css">
|
||||
<!-- <style type="text/css">
|
||||
.rotate-img {
|
||||
transform:rotate(90deg);
|
||||
-ms-transform:rotate(90deg); /* IE 9 */
|
||||
|
|
@ -19,14 +19,14 @@
|
|||
-webkit-transform:rotate(90deg); /* Safari and Chrome */
|
||||
-o-transform:rotate(90deg); /* Opera */
|
||||
}
|
||||
</style>
|
||||
</style> -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='container'>
|
||||
<div class='marketing'>
|
||||
<img class='rotate-img' src="/img/sprites/armor3_m.png" />
|
||||
<h2>The server is restarting.</h2>
|
||||
<img src="/img/sprites/dead.png" />
|
||||
<h2>The server is respawning.</h2>
|
||||
<p><a href="/">Try again</a> in a few. We restart often due to <a href="https://github.com/lefnire/habitrpg/issues/165">this issue</a>, and we're <a href=http://habitrpg.tumblr.com/post/55655159428/0-5-upgrade-aborted-angularjs-future>rewriting the site</a> to fix it. (AngularJS developers, come <a href=https://github.com/lefnire/habitrpg/tree/angular_rewrite>join us</a>!)</p>
|
||||
|
||||
<p>If this page persists, the server may be experiencing issues; the developers have been notified. Try switching to <a href="https://beta.habitrpg.com/">the beta site</a> or <a href="https://habitrpg.com/">the main site</a>.</p>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
52
src/server/routes.coffee
Normal file
52
src/server/routes.coffee
Normal file
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue