Skip to content

Commit

Permalink
API utility sendResult function, new POST /api/v1/register route to…
Browse files Browse the repository at this point in the history
… register new uname/pass users (i'm thinking about nixing facebook altogether in the future)
  • Loading branch information
lefnire committed Aug 14, 2013
1 parent 4b08353 commit 66bdd36
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 58 deletions.
92 changes: 60 additions & 32 deletions src/server/api.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ check = validator.check
sanitize = validator.sanitize
utils = require 'derby-auth/utils'
misc = require '../app/misc'
derbyAuthUtil = require('derby-auth/utils')

api = module.exports

Expand All @@ -19,6 +20,11 @@ api = module.exports
------------------------------------------------------------------------
####

sendResult = (req, next, code, data) ->
req.habit ?= {}
req.habit.result = if data then {code, data} else {code}
next()

NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request"
NO_USER_FOUND = err: "No user found."

Expand Down Expand Up @@ -84,8 +90,7 @@ api.scoreTask = (req, res, next) ->
# 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()
sendResult req, next, 200, _.extend(result, delta: delta)

# Set completed if type is daily or todo and task exists
if (existing = user.at "tasks.#{id}").get()
Expand Down Expand Up @@ -115,17 +120,15 @@ api.getTasks = (req, res, next) ->
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()
sendResult req, next, 200, tasks

###
Get Task
###
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)
req.habit.result = data: task
next()
sendResult req, next, 200, task

###
Validate task
Expand Down Expand Up @@ -166,17 +169,14 @@ api.validateTask = (req, res, next) ->
###
api.deleteTask = (req, res, next) ->
deleteTask req.habit.user, req.habit.task, ->
req.habit.result = code: 204
next()

sendResult req, next, 204

###
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()
req.habit.user.set "tasks.#{req.habit.task.id}", req.habit.task, ->
sendResult req, next, 200, req.habit.task

###
Update tasks (plural). This will update, add new, delete, etc all at once.
Expand Down Expand Up @@ -206,14 +206,12 @@ api.updateTasks = (req, res, next) ->
true

async.series series, ->
req.habit.result = code: 201, data: tasks
next()
sendResult req, next, 201, tasks

api.createTask = (req, res, next) ->
task = req.habit.task
addTask req.habit.user, task, ->
req.habit.result = code: 201, data: task
next()
sendResult req, next, 201, task

api.sortTask = (req, res, next) ->
{id} = req.params
Expand All @@ -235,10 +233,10 @@ api.buy = (req, res, next) ->
return res.json 400, err: ":type must be in one of: 'weapon', 'armor', 'head', 'shield'"
hasEnough = true
done = ->
req.habit.result = if hasEnough
data: req.habit.user.get("items")
else {data: {err: "Not enough GP"}}
next()
if hasEnough
sendResult req, res, 200, req.habit.user.get("items")
else
sendResult req, next, 200, {err: "Not enough GP"}
misc.batchTxn req.getModel(), (uObj, paths) ->
hasEnough = items.buyItem(uObj, type, {paths})
,{user:req.habit.user, done}
Expand All @@ -249,6 +247,43 @@ api.buy = (req, res, next) ->
------------------------------------------------------------------------
###


###
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 sendResult req, next, 401, err: ":username, :email, :password, :confirmPassword required"
if password isnt confirmPassword
return sendResult req, next, 401, err: ":password and :confirmPassword don't match"
try
validator.check(email).isEmail()
catch e
return sendResult req, next, 401, err: e.message

model = req.getModel()
async.waterfall [
(cb) -> model.query('users').withEmail(email).fetch cb

, (user, cb) ->
return cb("Email already taken") if user.get()
model.query('users').withUsername(username).fetch cb

, (user, cb) ->
return cb("Username already taken") if user.get()
newUser = helpers.newUser(true)
salt = utils.makeSalt()
newUser.auth = local: {username, email, salt}
newUser.auth.local.hashed_password = derbyAuthUtil.encryptPassword(password, salt)
newUser.auth.timestamps = {created: +new Date}
req._isServer = true
id = model.add "users", newUser, (err) -> cb(err, id)
], (err, id) ->
return sendResult req, next, 401, {err} if err
sendResult req, next, 200, model.get("users.#{id}")

###
Get User
###
Expand All @@ -263,8 +298,7 @@ api.getUser = (req, res, next) ->
delete uObj.auth.hashed_password
delete uObj.auth.salt

req.habit.result = data: uObj
next()
sendResult req, next, 200, uObj

###
Register new user with uname / password
Expand All @@ -291,11 +325,9 @@ api.loginLocal = (req, res, next) ->
u2 = result2.get()
return res.json 401, err: 'Incorrect password' unless u2

req.habit ?= {}
req.habit.result = data:
sendResult req, next, 200,
id: u2.id
token: u2.apiToken
next()

###
POST /user/auth/facebook
Expand All @@ -309,11 +341,9 @@ api.loginFacebook = (req, res, next) ->
return res.json 401, { err } if err
u = result.get()
if u
req.habit ?= {}
req.habit.result = data:
sendResult req, next, 200,
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."
Expand All @@ -337,8 +367,7 @@ api.updateUser = (req, res, next) ->
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()
sendResult req, next, 200, helpers.derbyUserToAPI(user)

api.cron = (req, res, next) ->
{user} = req.habit
Expand All @@ -350,8 +379,7 @@ api.cron = (req, res, next) ->
api.revive = (req, res, next) ->
{user} = req.habit
done = ->
req.habit.result = data: helpers.derbyUserToAPI(user)
next()
sendResult req, res, 200, helpers.derbyUserToAPI(user)
misc.batchTxn req.getModel(), (uObj, paths) ->
algos.revive uObj, {paths}
, {user, done}
Expand Down
5 changes: 4 additions & 1 deletion src/server/routes.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ api = require './api'
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) ->
v1Send = (req, res) ->
{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
Expand All @@ -28,6 +28,9 @@ v1Send = (req, res, next) ->

router.get '/status', (req, res) -> res.json status: 'up'

# Auth
router.post '/register', api.registerUser, v1Send

# Scoring
router.post '/user/task/:id/:direction', auth, cron, api.scoreTask, v1Send
router.post '/user/tasks/:id/:direction', auth, cron, api.scoreTask, v1Send
Expand Down
66 changes: 41 additions & 25 deletions test/api.mocha.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ request = require 'superagent'
expect = require 'expect.js'
require 'coffee-script'
utils = require 'derby-auth/utils'
async = require 'async'

conf = require("nconf")
conf.argv().env().file({file: __dirname + '../config.json'}).defaults
Expand Down Expand Up @@ -32,6 +33,11 @@ uuid = null
taskPath = null
baseURL = 'http://localhost:1337/api/v1'

expectUserEqual = (u1, u2) ->
'lastCron update__'.split(' ').forEach (path) ->
delete u1[path]; delete u2[path]
expect(u1).to.eql(u2)

###### Specs ######

describe 'API', ->
Expand All @@ -44,26 +50,35 @@ describe 'API', ->

before (done) ->
server = require '../src/server'
server.listen '1337', '0.0.0.0'
server.on 'listening', (data) ->
server.listen '1337', '0.0.0.0', ->
store = server.habitStore
#store.flush()
model = store.createModel()
model.set '_userId', uid = model.id()
user = helpers.newUser(true)
user.apiToken = model.id()
model.session = {userId:uid}
salt = utils.makeSalt()
username = 'jonfishman' + Math.random().toString().split('.')[1]
user.auth =
local:
username: username
hashed_password: utils.encryptPassword('icculus', salt)
salt: salt
model.set "users.#{uid}", user
delete model.session
# Crappy hack to let server start before tests run
setTimeout done, 2000

randomID = model.id()
params =
username: randomID
password: randomID
confirmPassword: randomID
email: "#{randomID}@gmail.com"

register = ->
request.post("#{baseURL}/register")
.set('Accept', 'application/json')
.send(params)
.end (res) ->
user = res.body
uid = user.id
username = user.auth.local.username

#TODO fix me , we shouldn't need session & _userID in API function testing
model.set '_userId', uid
model.session = {userId:uid}

done()

# nasty hack, why isn't server.listen callback fired when server is completely up?
setTimeout register, 2000

describe 'Without token or user id', ->

Expand All @@ -87,13 +102,14 @@ describe 'API', ->
params = null
currentUser = null

before ->
user = model.at("users.#{uid}")
currentUser = user.get()
params =
title: 'Title'
text: 'Text'
type: 'habit'
before (done) ->
model.fetch "users.#{uid}", (err, _user) ->
user = _user
params =
title: 'Title'
text: 'Text'
type: 'habit'
done()

beforeEach ->
currentUser = user.get()
Expand All @@ -112,7 +128,7 @@ describe 'API', ->
self.stats.toNextLevel = 150
self.stats.maxHealth = 50

expect(res.body).to.eql self
expectUserEqual(res.body, self)
done()

it 'GET /api/v1/user/task/:id', (done) ->
Expand Down

0 comments on commit 66bdd36

Please sign in to comment.