mirror of
https://github.com/sudoxnym/habitica-self-host.git
synced 2026-04-14 19:47:03 +00:00
api & rewrite: buy gems, reroll, fully converted server to JS, bug fixes
This commit is contained in:
parent
afb510fe84
commit
e568a5ba31
22 changed files with 1460 additions and 882 deletions
|
|
@ -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;
|
||||
}
|
||||
}])
|
||||
|
|
@ -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);
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
}]);
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ angular.module('userServices', []).
|
|||
},
|
||||
|
||||
authenticated: function(){
|
||||
this.settings.auth.apiId !== "";
|
||||
return this.settings.auth.apiId !== "";
|
||||
},
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -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')));
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
891
src/controllers/api.js
Normal file
891
src/controllers/api.js
Normal file
|
|
@ -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");
|
||||
});
|
||||
};
|
||||
|
|
@ -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
|
||||
81
src/controllers/deprecated.js
Normal file
81
src/controllers/deprecated.js
Normal file
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
51
src/controllers/static.js
Normal file
51
src/controllers/static.js
Normal file
|
|
@ -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;
|
||||
|
|
@ -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*/
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
67
src/models/group.js
Normal file
67
src/models/group.js
Normal file
|
|
@ -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);
|
||||
|
|
@ -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)
|
||||
311
src/models/user.js
Normal file
311
src/models/user.js
Normal file
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
21
src/routes/pages.js
Normal file
21
src/routes/pages.js
Normal file
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
| {{user.balance * 4 | number:0}} Gems
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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?')
|
||||
|
|
|
|||
Loading…
Reference in a new issue