2014-02-17 15:19:19 -08:00
schema = require ' ./user_schema '
crypto = require ' crypto '
request = require ' request '
User = require ' ./User '
Handler = require ' ../commons/Handler '
2014-01-03 10:32:13 -08:00
mongoose = require ' mongoose '
2014-01-03 14:28:00 -08:00
config = require ' ../../server_config '
2014-02-17 15:19:19 -08:00
errors = require ' ../commons/errors '
async = require ' async '
2014-03-31 13:56:13 -07:00
log = require ' winston '
2014-03-31 15:48:22 -07:00
LevelSession = require ( ' ../levels/sessions/LevelSession ' )
2014-04-09 16:46:44 -07:00
LevelSessionHandler = require ' ../levels/sessions/level_session_handler '
2014-01-03 10:32:13 -08:00
serverProperties = [ ' passwordHash ' , ' emailLower ' , ' nameLower ' , ' passwordReset ' ]
2014-03-23 09:30:01 -07:00
privateProperties = [
' permissions ' , ' email ' , ' firstName ' , ' lastName ' , ' gender ' , ' facebookID ' ,
' gplusID ' , ' music ' , ' volume ' , ' aceConfig '
]
2014-04-07 15:21:05 -07:00
candidateProperties = [
2014-04-07 17:58:02 -07:00
' jobProfile ' , ' jobProfileApproved ' , ' jobProfileNotes '
2014-03-23 09:30:01 -07:00
]
2014-01-03 10:32:13 -08:00
UserHandler = class UserHandler extends Handler
modelClass: User
editableProperties: [
' name ' , ' photoURL ' , ' password ' , ' anonymous ' , ' wizardColor1 ' , ' volume ' ,
2014-03-23 09:30:01 -07:00
' firstName ' , ' lastName ' , ' gender ' , ' facebookID ' , ' gplusID ' , ' emailSubscriptions ' ,
2014-01-12 11:54:50 -08:00
' testGroupNumber ' , ' music ' , ' hourOfCode ' , ' hourOfCodeComplete ' , ' preferredLanguage ' ,
2014-04-05 17:05:03 -07:00
' wizard ' , ' aceConfig ' , ' autocastDelay ' , ' lastLevel ' , ' jobProfile '
2014-01-03 10:32:13 -08:00
]
jsonSchema: schema
2014-02-01 08:22:26 -08:00
2014-01-03 14:28:00 -08:00
constructor: ->
super ( arguments . . . )
@ editableProperties . push ( ' permissions ' ) unless config . isProduction
2014-01-03 10:32:13 -08:00
2014-04-07 17:58:02 -07:00
getEditableProperties: (req, document) ->
props = super req , document
props . push ' jobProfileApproved ' , ' jobProfileNotes ' if req . user . isAdmin ( )
props
2014-01-03 10:32:13 -08:00
formatEntity: (req, document) ->
return null unless document ?
obj = document . toObject ( )
delete obj [ prop ] for prop in serverProperties
2014-04-07 15:21:05 -07:00
includePrivates = req . user and ( req . user . isAdmin ( ) or req . user . _id . equals ( document . _id ) )
2014-01-03 10:32:13 -08:00
delete obj [ prop ] for prop in privateProperties unless includePrivates
2014-04-07 17:58:02 -07:00
includeCandidate = includePrivates or ( obj . jobProfileApproved and req . user and ( ' employer ' in ( req . user . permissions ? [ ] ) ) )
2014-04-07 15:21:05 -07:00
delete obj [ prop ] for prop in candidateProperties unless includeCandidate
2014-01-03 10:32:13 -08:00
return obj
waterfallFunctions: [
# FB access token checking
# Check the email is the same as FB reports
(req, user, callback) ->
fbID = req . query . facebookID
fbAT = req . query . facebookAccessToken
return callback ( null , req , user ) unless fbID and fbAT
url = " https://graph.facebook.com/me?access_token= #{ fbAT } "
2014-03-31 13:56:13 -07:00
request ( url , (err, response, body) ->
log . warn " Error grabbing FB token: #{ err } " if err
2014-01-03 10:32:13 -08:00
body = JSON . parse ( body )
emailsMatch = req . body . email is body . email
return callback ( res : ' Invalid Facebook Access Token. ' , code : 422 ) unless emailsMatch
callback ( null , req , user )
)
# GPlus access token checking
(req, user, callback) ->
gpID = req . query . gplusID
gpAT = req . query . gplusAccessToken
return callback ( null , req , user ) unless gpID and gpAT
url = " https://www.googleapis.com/oauth2/v2/userinfo?access_token= #{ gpAT } "
2014-03-31 13:56:13 -07:00
request ( url , (err, response, body) ->
log . warn " Error grabbing G+ token: #{ err } " if err
2014-01-03 10:32:13 -08:00
body = JSON . parse ( body )
emailsMatch = req . body . email is body . email
return callback ( res : ' Invalid G+ Access Token. ' , code : 422 ) unless emailsMatch
callback ( null , req , user )
)
# Email setting
(req, user, callback) ->
return callback ( null , req , user ) unless req . body . email ?
emailLower = req . body . email . toLowerCase ( )
return callback ( null , req , user ) if emailLower is user . get ( ' emailLower ' )
User . findOne ( { emailLower : emailLower } ) . exec (err, otherUser) ->
2014-03-31 13:56:13 -07:00
log . error " Database error setting user email: #{ err } " if err
2014-01-03 10:32:13 -08:00
return callback ( res : ' Database error. ' , code : 500 ) if err
if ( req . query . gplusID or req . query . facebookID ) and otherUser
# special case, log in as that user
return req . logIn ( otherUser , (err) ->
return callback ( res : ' Facebook user login error. ' , code : 500 ) if err
return callback ( null , req , otherUser )
)
r = { message : ' is already used by another account ' , property : ' email ' }
return callback ( { res : r , code : 409 } ) if otherUser
user . set ( ' email ' , req . body . email )
callback ( null , req , user )
# Name setting
(req, user, callback) ->
return callback ( null , req , user ) unless req . body . name
nameLower = req . body . name ? . toLowerCase ( )
return callback ( null , req , user ) if nameLower is user . get ( ' nameLower ' )
User . findOne ( { nameLower : nameLower } ) . exec (err, otherUser) ->
2014-03-31 13:56:13 -07:00
log . error " Database error setting user name: #{ err } " if err
2014-01-03 10:32:13 -08:00
return callback ( res : ' Database error. ' , code : 500 ) if err
r = { message : ' is already used by another account ' , property : ' name ' }
return callback ( { res : r , code : 409 } ) if otherUser
user . set ( ' name ' , req . body . name )
callback ( null , req , user )
]
getById: (req, res, id) ->
2014-02-24 20:27:38 -08:00
if req . user ? . _id . equals ( id )
2014-04-09 16:46:44 -07:00
return @ sendSuccess ( res , @ formatEntity ( req , req . user , 256 ) )
2014-01-03 10:32:13 -08:00
super ( req , res , id )
2014-03-20 15:40:02 -07:00
2014-02-17 15:19:19 -08:00
getNamesByIds: (req, res) ->
ids = req . query . ids or req . body . ids
ids = ids . split ( ' , ' ) if _ . isString ids
ids = _ . uniq ids
2014-03-20 15:40:02 -07:00
2014-03-03 10:21:51 -08:00
# TODO: Extend and repurpose this handler to return other public info about a user more flexibly,
# say by a query parameter that lists public properties to return.
returnWizard = req . query . wizard or req . body . wizard
query = if returnWizard then { name : 1 , wizard : 1 } else { name : 1 }
2014-03-20 15:40:02 -07:00
2014-02-17 15:19:19 -08:00
makeFunc = (id) ->
(callback) ->
2014-03-03 10:21:51 -08:00
User . findById ( id , query ) . exec (err, document) ->
2014-02-17 15:19:19 -08:00
return done ( err ) if err
2014-03-03 10:21:51 -08:00
if document and returnWizard
callback ( null , { name : document . get ( ' name ' ) , wizard : document . get ( ' wizard ' ) or { } } )
else
callback ( null , document ? . get ( ' name ' ) or ' ' )
2014-03-20 15:40:02 -07:00
2014-02-17 15:19:19 -08:00
funcs = { }
for id in ids
return errors . badInput ( res , " Given an invalid id: #{ id } " ) unless Handler . isID ( id )
funcs [ id ] = makeFunc ( id )
2014-03-20 15:40:02 -07:00
2014-02-17 15:19:19 -08:00
async . parallel funcs , (err, results) ->
return errors . serverError err if err
res . send results
res . end ( )
2014-01-03 10:32:13 -08:00
2014-02-27 14:07:11 -08:00
nameToID: (req, res, name) ->
2014-02-27 14:42:11 -08:00
User . findOne ( { nameLower : name . toLowerCase ( ) } ) . exec (err, otherUser) ->
res . send ( if otherUser then otherUser . _id else JSON . stringify ( ' ' ) )
2014-02-27 14:07:11 -08:00
res . end ( )
2014-01-03 10:32:13 -08:00
post: (req, res) ->
return @ sendBadInputError ( res , ' No input. ' ) if _ . isEmpty ( req . body )
2014-02-24 20:27:38 -08:00
return @ sendBadInputError ( res , ' Must have an anonymous user to post with. ' ) unless req . user
2014-01-03 10:32:13 -08:00
return @ sendBadInputError ( res , ' Existing users cannot create new ones. ' ) unless req . user . get ( ' anonymous ' )
req.body._id = req . user . _id if req . user . get ( ' anonymous ' )
@ put ( req , res )
hasAccessToDocument: (req, document) ->
if req . route . method in [ ' put ' , ' post ' , ' patch ' ]
2014-02-24 20:27:38 -08:00
return true if req . user ? . isAdmin ( )
return req . user ? . _id . equals ( document . _id )
2014-01-03 10:32:13 -08:00
return true
getByRelationship: (req, res, args...) ->
return @ agreeToCLA ( req , res ) if args [ 1 ] is ' agreeToCLA '
2014-02-07 16:11:07 -08:00
return @ avatar ( req , res , args [ 0 ] ) if args [ 1 ] is ' avatar '
2014-02-17 15:19:19 -08:00
return @ getNamesByIds ( req , res ) if args [ 1 ] is ' names '
2014-02-27 14:07:11 -08:00
return @ nameToID ( req , res , args [ 0 ] ) if args [ 1 ] is ' nameToID '
2014-03-31 15:48:22 -07:00
return @ getLevelSessions ( req , res , args [ 0 ] ) if args [ 1 ] is ' level.sessions '
2014-04-06 17:01:56 -07:00
return @ getCandidates ( req , res ) if args [ 1 ] is ' candidates '
2014-01-03 10:32:13 -08:00
return @ sendNotFoundError ( res )
2014-04-11 10:33:22 -07:00
super ( arguments . . . )
2014-02-01 08:22:26 -08:00
2014-01-03 10:32:13 -08:00
agreeToCLA: (req, res) ->
2014-02-24 20:27:38 -08:00
return @ sendUnauthorizedError ( res ) unless req . user
2014-01-03 10:32:13 -08:00
doc =
user: req . user . _id + ' '
email: req . user . get ' email '
name: req . user . get ' name '
githubUsername: req . body . githubUsername
2014-01-05 16:02:50 -08:00
created: new Date ( ) + ' '
2014-01-18 10:16:55 -08:00
collection = mongoose . connection . db . collection ' cla.submissions ' , (err, collection) =>
2014-01-03 10:32:13 -08:00
return @ sendDatabaseError ( res , err ) if err
2014-01-18 10:16:55 -08:00
collection . insert doc , (err) =>
2014-01-03 10:32:13 -08:00
return @ sendDatabaseError ( res , err ) if err
req . user . set ( ' signedCLA ' , doc . created )
2014-01-18 10:16:55 -08:00
req . user . save (err) =>
2014-01-03 10:32:13 -08:00
return @ sendDatabaseError ( res , err ) if err
2014-01-14 23:13:47 +01:00
@ sendSuccess ( res , { result : ' success ' } )
2014-01-03 10:32:13 -08:00
2014-02-07 16:11:07 -08:00
avatar: (req, res, id) ->
2014-04-09 16:46:44 -07:00
@ modelClass . findById ( id ) . exec (err, document) =>
2014-02-07 16:11:07 -08:00
return @ sendDatabaseError ( res , err ) if err
2014-04-09 16:46:44 -07:00
photoURL = document ? . get ( ' photoURL ' )
photoURL || = @ buildGravatarURL document
res . redirect photoURL
2014-02-07 16:11:07 -08:00
res . end ( )
2014-03-31 15:48:22 -07:00
getLevelSessions: (req, res, userID) ->
return @ sendUnauthorizedError ( res ) unless req . user . _id + ' ' is userID or req . user . isAdmin ( )
query = { ' creator ' : userID }
projection = null
if req . query . project
projection = { }
projection [ field ] = 1 for field in req . query . project . split ( ' , ' )
LevelSession . find ( query ) . select ( projection ) . exec (err, documents) =>
return @ sendDatabaseError ( res , err ) if err
2014-04-09 16:46:44 -07:00
documents = ( LevelSessionHandler . formatEntity ( req , doc ) for doc in documents )
2014-03-31 15:48:22 -07:00
@ sendSuccess ( res , documents )
2014-04-06 17:01:56 -07:00
getCandidates: (req, res) ->
2014-04-07 15:21:05 -07:00
authorized = req . user . isAdmin ( ) or ( ' employer ' in req . user . get ( ' permissions ' ) )
since = ( new Date ( ( new Date ( ) ) - 2 * 30.4 * 86400 * 1000 ) ) . toISOString ( )
#query = {'jobProfileApproved': true, 'jobProfile.active': true, 'jobProfile.updated': {$gt: since}}
query = { ' jobProfile.active ' : true , ' jobProfile.updated ' : { $gt: since } } # testing
2014-04-07 17:58:02 -07:00
query.jobProfileApproved = true unless req . user . isAdmin ( )
2014-04-07 15:21:05 -07:00
selection = ' jobProfile '
2014-04-07 17:58:02 -07:00
selection += ' email ' if authorized
selection += ' jobProfileApproved ' if req . user . isAdmin ( )
2014-04-07 15:21:05 -07:00
User . find ( query ) . select ( selection ) . exec (err, documents) =>
2014-04-06 17:01:56 -07:00
return @ sendDatabaseError ( res , err ) if err
2014-04-07 15:21:05 -07:00
candidates = ( @ formatCandidate ( authorized , doc ) for doc in documents )
@ sendSuccess ( res , candidates )
formatCandidate: (authorized, document) ->
2014-04-09 16:46:44 -07:00
fields = if authorized then [ ' jobProfile ' , ' jobProfileApproved ' , ' photoURL ' , ' _id ' ] else [ ' jobProfile ' ]
2014-04-07 15:21:05 -07:00
obj = _ . pick document . toObject ( ) , fields
2014-04-10 17:54:28 -07:00
obj . photoURL || = obj . jobProfile . photoURL if authorized
2014-04-09 16:46:44 -07:00
obj . photoURL || = @ buildGravatarURL document if authorized
2014-04-11 12:49:44 -07:00
subfields = [ ' country ' , ' city ' , ' lookingFor ' , ' jobTitle ' , ' skills ' , ' experience ' , ' updated ' ]
2014-04-07 15:21:05 -07:00
if authorized
2014-04-11 12:49:44 -07:00
subfields = subfields . concat [ ' name ' ]
2014-04-07 15:21:05 -07:00
obj.jobProfile = _ . pick obj . jobProfile , subfields
obj
2014-03-31 15:48:22 -07:00
2014-04-09 16:46:44 -07:00
buildGravatarURL: (user) ->
emailHash = @ buildEmailHash user
defaultAvatar = " http://codecombat.com/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png "
" https://www.gravatar.com/avatar/ #{ emailHash } ?default= #{ defaultAvatar } "
buildEmailHash: (user) ->
# emailHash is used by gravatar
hash = crypto . createHash ( ' md5 ' )
if user . get ( ' email ' )
hash . update ( _ . trim ( user . get ( ' email ' ) ) . toLowerCase ( ) )
else
hash . update ( user . get ( ' _id ' ) + ' ' )
hash . digest ( ' hex ' )
2014-03-31 15:48:22 -07:00
2014-03-16 19:11:55 +04:00
module.exports = new UserHandler ( )