2014-04-12 13:51:02 -04:00
schema = require ' ../../app/schemas/models/user '
2014-02-17 18:19:19 -05:00
crypto = require ' crypto '
request = require ' request '
User = require ' ./User '
Handler = require ' ../commons/Handler '
2014-01-03 13:32:13 -05:00
mongoose = require ' mongoose '
2014-01-03 17:28:00 -05:00
config = require ' ../../server_config '
2014-02-17 18:19:19 -05:00
errors = require ' ../commons/errors '
async = require ' async '
2014-03-31 16:56:13 -04:00
log = require ' winston '
2014-07-21 08:36:10 -04:00
moment = require ' moment '
2014-06-30 22:16:26 -04:00
LevelSession = require ' ../levels/sessions/LevelSession '
2014-04-09 19:46:44 -04:00
LevelSessionHandler = require ' ../levels/sessions/level_session_handler '
2014-12-02 23:01:35 -05:00
SubscriptionHandler = require ' ../payments/subscription_handler '
2014-12-04 20:41:17 -05:00
DiscountHandler = require ' ../payments/discount_handler '
2014-05-19 19:25:41 -04:00
EarnedAchievement = require ' ../achievements/EarnedAchievement '
2014-06-17 16:03:08 -04:00
UserRemark = require ' ./remarks/UserRemark '
2014-08-30 20:09:57 -04:00
{ isID } = require ' ../lib/utils '
2014-01-03 13:32:13 -05:00
2014-09-23 20:13:41 -04:00
serverProperties = [ ' passwordHash ' , ' emailLower ' , ' nameLower ' , ' passwordReset ' , ' lastIP ' ]
2014-04-07 18:21:05 -04:00
candidateProperties = [
2014-04-07 20:58:02 -04:00
' jobProfile ' , ' jobProfileApproved ' , ' jobProfileNotes '
2014-03-23 12:30:01 -04:00
]
2014-01-03 13:32:13 -05:00
UserHandler = class UserHandler extends Handler
modelClass: User
2014-04-07 20:58:02 -04:00
getEditableProperties: (req, document) ->
props = super req , document
2014-06-17 18:17:19 -04:00
props . push ' permissions ' unless config . isProduction
2014-07-16 13:51:44 -04:00
props . push ' jobProfileApproved ' , ' jobProfileNotes ' , ' jobProfileApprovedDate ' if req . user . isAdmin ( ) # Admins naturally edit these
2014-07-22 14:07:00 -04:00
props . push @ privateProperties . . . if req . user . isAdmin ( ) # Admins are mad with power
2014-04-07 20:58:02 -04:00
props
2014-09-22 17:56:02 -04:00
formatEntity: (req, document, publicOnly=false) =>
2014-01-03 13:32:13 -05:00
return null unless document ?
obj = document . toObject ( )
delete obj [ prop ] for prop in serverProperties
2014-09-22 17:56:02 -04:00
includePrivates = not publicOnly and ( req . user and ( req . user . isAdmin ( ) or req . user . _id . equals ( document . _id ) ) )
2014-07-22 14:07:00 -04:00
delete obj [ prop ] for prop in @ privateProperties unless includePrivates
2014-09-22 17:56:02 -04:00
includeCandidate = not publicOnly and ( includePrivates or ( obj . jobProfile ? . active and req . user and ( ' employer ' in ( req . user . get ( ' permissions ' ) ? [ ] ) ) and @ employerCanViewCandidate req . user , obj ) )
2014-04-07 18:21:05 -04:00
delete obj [ prop ] for prop in candidateProperties unless includeCandidate
2014-01-03 13:32:13 -05: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 16:56:13 -04:00
request ( url , (err, response, body) ->
log . warn " Error grabbing FB token: #{ err } " if err
2014-01-03 13:32:13 -05:00
body = JSON . parse ( body )
emailsMatch = req . body . email is body . email
2014-06-30 22:16:26 -04:00
return callback ( res: ' Invalid Facebook Access Token. ' , code: 422 ) unless emailsMatch
2014-01-03 13:32:13 -05:00
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 16:56:13 -04:00
request ( url , (err, response, body) ->
log . warn " Error grabbing G+ token: #{ err } " if err
2014-01-03 13:32:13 -05:00
body = JSON . parse ( body )
emailsMatch = req . body . email is body . email
2014-06-30 22:16:26 -04:00
return callback ( res: ' Invalid G+ Access Token. ' , code: 422 ) unless emailsMatch
2014-01-03 13:32:13 -05:00
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 ' )
2014-06-30 22:16:26 -04:00
User . findOne ( { emailLower: emailLower } ) . exec (err, otherUser) ->
2014-03-31 16:56:13 -04:00
log . error " Database error setting user email: #{ err } " if err
2014-06-30 22:16:26 -04:00
return callback ( res: ' Database error. ' , code: 500 ) if err
2014-01-03 13:32:13 -05:00
if ( req . query . gplusID or req . query . facebookID ) and otherUser
# special case, log in as that user
return req . logIn ( otherUser , (err) ->
2014-06-30 22:16:26 -04:00
return callback ( res: ' Facebook user login error. ' , code: 500 ) if err
2014-01-03 13:32:13 -05:00
return callback ( null , req , otherUser )
)
2014-06-30 22:16:26 -04:00
r = { message: ' is already used by another account ' , property: ' email ' }
return callback ( { res: r , code: 409 } ) if otherUser
2014-01-03 13:32:13 -05:00
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 ( )
2014-05-07 15:25:05 -04:00
return callback ( null , req , user ) unless nameLower
2014-07-10 12:00:32 -04:00
return callback ( null , req , user ) if user . get ' anonymous ' # anonymous users can have any name
return callback ( null , req , user ) if nameLower is user . get ( ' nameLower ' )
2014-06-30 22:16:26 -04:00
User . findOne ( { nameLower: nameLower , anonymous: false } ) . exec (err, otherUser) ->
2014-03-31 16:56:13 -04:00
log . error " Database error setting user name: #{ err } " if err
2014-06-30 22:16:26 -04:00
return callback ( res: ' Database error. ' , code: 500 ) if err
r = { message: ' is already used by another account ' , property: ' name ' }
2014-04-13 08:25:49 -04:00
console . log ' Another user exists ' if otherUser
2014-06-30 22:16:26 -04:00
return callback ( { res: r , code: 409 } ) if otherUser
2014-01-03 13:32:13 -05:00
user . set ( ' name ' , req . body . name )
callback ( null , req , user )
2014-12-08 10:07:29 -05:00
2014-12-02 23:01:35 -05:00
# Subscription setting
(req, user, callback) ->
2014-12-05 17:11:38 -05:00
return callback ( null , req , user ) unless req . headers [ ' x-change-plan ' ] # ensure only saves that are targeted at changing the subscription actually affect the subscription
return callback ( null , req , user ) unless req . body . stripe
2014-12-02 23:01:35 -05:00
hasPlan = user . get ( ' stripe ' ) ? . planID ?
2014-12-05 17:11:38 -05:00
wantsPlan = req . body . stripe . planID ?
2014-12-08 10:07:29 -05:00
2014-12-02 23:01:35 -05:00
return callback ( null , req , user ) if hasPlan is wantsPlan
if wantsPlan and not hasPlan
SubscriptionHandler . subscribeUser ( req , user , (err) ->
return callback ( err ) if err
return callback ( null , req , user )
)
else if hasPlan and not wantsPlan
SubscriptionHandler . unsubscribeUser ( req , user , (err) ->
return callback ( err ) if err
return callback ( null , req , user )
)
2014-12-04 20:41:17 -05:00
# Discount setting
(req, user, callback) ->
2014-12-05 17:11:38 -05:00
return callback ( null , req , user ) unless req . body . stripe
2014-12-04 20:41:17 -05:00
return callback ( null , req , user ) unless req . user ? . isAdmin ( )
hasCoupon = user . get ( ' stripe ' ) ? . couponID
2014-12-05 17:11:38 -05:00
wantsCoupon = req . body . stripe . couponID
2014-12-04 20:41:17 -05:00
return callback ( null , req , user ) if hasCoupon is wantsCoupon
if wantsCoupon and ( hasCoupon isnt wantsCoupon )
DiscountHandler . discountUser ( req , user , (err) ->
return callback ( err ) if err
return callback ( null , req , user )
)
else if hasCoupon and not wantsCoupon
DiscountHandler . removeDiscountFromCustomer ( req , user , (err) ->
return callback ( err ) if err
return callback ( null , req , user )
)
2014-01-03 13:32:13 -05:00
]
getById: (req, res, id) ->
2014-07-09 14:23:05 -04:00
if Handler . isID ( id ) and req . user ? . _id . equals ( id )
2014-04-09 19:46:44 -04:00
return @ sendSuccess ( res , @ formatEntity ( req , req . user , 256 ) )
2014-01-03 13:32:13 -05:00
super ( req , res , id )
2014-03-20 18:40:02 -04:00
2014-04-18 14:17:13 -04:00
getNamesByIDs: (req, res) ->
2014-02-17 18:19:19 -05:00
ids = req . query . ids or req . body . ids
2014-03-03 13:21:51 -05:00
returnWizard = req . query . wizard or req . body . wizard
2014-06-30 22:16:26 -04:00
properties = if returnWizard then ' name wizard ' else ' name '
2014-04-18 14:17:13 -04:00
@ getPropertiesFromMultipleDocuments res , User , properties , ids
2014-01-03 13:32:13 -05:00
2014-02-27 17:07:11 -05:00
nameToID: (req, res, name) ->
2014-06-30 22:16:26 -04:00
User . findOne ( { nameLower: unescape ( name ) . toLowerCase ( ) , anonymous: false } ) . exec (err, otherUser) ->
2014-02-27 17:42:11 -05:00
res . send ( if otherUser then otherUser . _id else JSON . stringify ( ' ' ) )
2014-02-27 17:07:11 -05:00
res . end ( )
2014-04-12 17:13:26 -04:00
getSimulatorLeaderboard: (req, res) ->
2014-04-14 17:52:21 -04:00
queryParameters = @ getSimulatorLeaderboardQueryParameters ( req )
2014-06-30 22:16:26 -04:00
leaderboardQuery = User . find ( queryParameters . query ) . select ( ' name simulatedBy simulatedFor ' ) . sort ( { ' simulatedBy ' : queryParameters . sortOrder } ) . limit ( queryParameters . limit )
2014-04-14 17:52:21 -04:00
leaderboardQuery . exec (err, otherUsers) ->
otherUsers = _ . reject otherUsers , _id: req . user . _id if req . query . scoreOffset isnt - 1
otherUsers ? = [ ]
res . send ( otherUsers )
res . end ( )
getMySimulatorLeaderboardRank: (req, res) ->
req.query.order = 1
queryParameters = @ getSimulatorLeaderboardQueryParameters ( req )
User . count queryParameters . query , (err, count) =>
return @ sendDatabaseError ( res , err ) if err
res . send JSON . stringify ( count + 1 )
2014-06-08 12:35:35 -04:00
getSimulatorLeaderboardQueryParameters: (req) ->
2014-04-12 17:13:26 -04:00
@ validateSimulateLeaderboardRequestParameters ( req )
2014-04-15 13:45:54 -04:00
2014-04-12 17:13:26 -04:00
query = { }
2014-04-14 11:46:46 -04:00
sortOrder = - 1
limit = if req . query . limit > 30 then 30 else req . query . limit
2014-04-13 18:28:16 -04:00
if req . query . scoreOffset isnt - 1
2014-04-12 17:13:26 -04:00
simulatedByQuery = { }
2014-06-30 22:16:26 -04:00
simulatedByQuery [ if req . query . order is 1 then ' $gt ' else ' $lte ' ] = req . query . scoreOffset
2014-04-12 17:13:26 -04:00
query.simulatedBy = simulatedByQuery
2014-04-14 11:46:46 -04:00
sortOrder = 1 if req . query . order is 1
2014-04-14 14:39:30 -04:00
else
2014-06-30 22:16:26 -04:00
query.simulatedBy = { ' $exists ' : true }
2014-04-14 17:52:21 -04:00
{ query: query , sortOrder: sortOrder , limit: limit }
2014-04-12 17:13:26 -04:00
validateSimulateLeaderboardRequestParameters: (req) ->
req.query.order = parseInt ( req . query . order ) ? - 1
req.query.scoreOffset = parseFloat ( req . query . scoreOffset ) ? 100000
req.query.limit = parseInt ( req . query . limit ) ? 20
2014-01-03 13:32:13 -05:00
post: (req, res) ->
return @ sendBadInputError ( res , ' No input. ' ) if _ . isEmpty ( req . body )
2014-02-24 23:27:38 -05:00
return @ sendBadInputError ( res , ' Must have an anonymous user to post with. ' ) unless req . user
2014-08-23 14:07:52 -04:00
return @ sendBadInputError ( res , ' Existing users cannot create new ones. ' ) if req . user . get ( ' anonymous ' ) is false
2014-01-03 13:32:13 -05:00
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 23:27:38 -05:00
return true if req . user ? . isAdmin ( )
return req . user ? . _id . equals ( document . _id )
2014-01-03 13:32:13 -05:00
return true
getByRelationship: (req, res, args...) ->
return @ agreeToCLA ( req , res ) if args [ 1 ] is ' agreeToCLA '
2014-06-30 22:16:26 -04:00
return @ agreeToEmployerAgreement ( req , res ) if args [ 1 ] is ' agreeToEmployerAgreement '
2014-02-07 19:11:07 -05:00
return @ avatar ( req , res , args [ 0 ] ) if args [ 1 ] is ' avatar '
2014-04-18 14:17:13 -04:00
return @ getNamesByIDs ( req , res ) if args [ 1 ] is ' names '
2014-02-27 17:07:11 -05:00
return @ nameToID ( req , res , args [ 0 ] ) if args [ 1 ] is ' nameToID '
2014-06-11 22:38:41 -04:00
return @ getLevelSessionsForEmployer ( req , res , args [ 0 ] ) if args [ 1 ] is ' level.sessions ' and args [ 2 ] is ' employer '
2014-03-31 18:48:22 -04:00
return @ getLevelSessions ( req , res , args [ 0 ] ) if args [ 1 ] is ' level.sessions '
2014-04-06 20:01:56 -04:00
return @ getCandidates ( req , res ) if args [ 1 ] is ' candidates '
2014-06-10 19:30:07 -04:00
return @ getEmployers ( req , res ) if args [ 1 ] is ' employers '
2014-04-12 17:13:26 -04:00
return @ getSimulatorLeaderboard ( req , res , args [ 0 ] ) if args [ 1 ] is ' simulatorLeaderboard '
2014-04-14 17:52:21 -04:00
return @ getMySimulatorLeaderboardRank ( req , res , args [ 0 ] ) if args [ 1 ] is ' simulator_leaderboard_rank '
2014-05-19 18:24:16 -04:00
return @ getEarnedAchievements ( req , res , args [ 0 ] ) if args [ 1 ] is ' achievements '
2014-07-21 08:36:10 -04:00
return @ getRecentlyPlayed ( req , res , args [ 0 ] ) if args [ 1 ] is ' recently_played '
2014-06-10 19:30:07 -04:00
return @ trackActivity ( req , res , args [ 0 ] , args [ 2 ] , args [ 3 ] ) if args [ 1 ] is ' track ' and args [ 2 ]
2014-06-17 16:03:08 -04:00
return @ getRemark ( req , res , args [ 0 ] ) if args [ 1 ] is ' remark '
2014-08-30 20:09:57 -04:00
return @ searchForUser ( req , res ) if args [ 1 ] is ' admin_search '
2014-12-05 19:47:44 -05:00
return @ getStripeInfo ( req , res , args [ 0 ] ) if args [ 1 ] is ' stripe '
2014-01-03 13:32:13 -05:00
return @ sendNotFoundError ( res )
2014-04-11 13:33:22 -04:00
super ( arguments . . . )
2014-02-01 11:22:26 -05:00
2014-12-05 19:47:44 -05:00
getStripeInfo: (req, res, handle) ->
@ getDocumentForIdOrSlug handle , (err, user) =>
2014-12-05 20:19:44 -05:00
return @ sendNotFoundError ( res ) if not user
2014-12-05 19:47:44 -05:00
return @ sendForbiddenError ( res ) unless req . user and ( req . user . isAdmin ( ) or req . user . get ( ' _id ' ) . equals ( user . get ( ' _id ' ) ) )
2014-12-10 16:42:12 -05:00
return @ sendNotFoundError ( res ) if not customerID = user . get ( ' stripe ' ) ? . customerID
2014-12-05 19:47:44 -05:00
stripe . customers . retrieve customerID , (err, customer) =>
return @ sendDatabaseError ( res , err ) if err
@ sendSuccess ( res , JSON . stringify ( customer , null , ' \t ' ) )
2014-01-03 13:32:13 -05:00
agreeToCLA: (req, res) ->
2014-09-19 05:26:18 -04:00
return @ sendForbiddenError ( res ) unless req . user
2014-01-03 13:32:13 -05:00
doc =
user: req . user . _id + ' '
email: req . user . get ' email '
name: req . user . get ' name '
githubUsername: req . body . githubUsername
2014-01-05 19:02:50 -05:00
created: new Date ( ) + ' '
2014-01-18 13:16:55 -05:00
collection = mongoose . connection . db . collection ' cla.submissions ' , (err, collection) =>
2014-01-03 13:32:13 -05:00
return @ sendDatabaseError ( res , err ) if err
2014-01-18 13:16:55 -05:00
collection . insert doc , (err) =>
2014-01-03 13:32:13 -05:00
return @ sendDatabaseError ( res , err ) if err
req . user . set ( ' signedCLA ' , doc . created )
2014-01-18 13:16:55 -05:00
req . user . save (err) =>
2014-01-03 13:32:13 -05:00
return @ sendDatabaseError ( res , err ) if err
2014-06-30 22:16:26 -04:00
@ sendSuccess ( res , { result: ' success ' } )
2014-01-03 13:32:13 -05:00
2014-02-07 19:11:07 -05:00
avatar: (req, res, id) ->
2014-08-14 13:17:44 -04:00
@ modelClass . findById ( id ) . exec (err, document) =>
2014-02-07 19:11:07 -05:00
return @ sendDatabaseError ( res , err ) if err
2014-07-17 12:12:12 -04:00
return @ sendNotFoundError ( res ) unless document
2014-04-09 19:46:44 -04:00
photoURL = document ? . get ( ' photoURL ' )
2014-04-12 17:53:09 -04:00
if photoURL
photoURL = " /file/ #{ photoURL } "
2014-07-07 13:29:34 -04:00
else if req . query . employerPageAvatar is " true "
photoURL = @ buildGravatarURL document , req . query . s , " /images/pages/employer/anon_user.png "
2014-04-12 17:53:09 -04:00
else
photoURL = @ buildGravatarURL document , req . query . s , req . query . fallback
2014-04-09 19:46:44 -04:00
res . redirect photoURL
2014-02-07 19:11:07 -05:00
res . end ( )
2014-06-11 22:38:41 -04:00
getLevelSessionsForEmployer: (req, res, userID) ->
2014-11-22 21:40:28 -05:00
return @ sendForbiddenError ( res ) unless req . user
2014-09-19 05:26:18 -04:00
return @ sendForbiddenError ( res ) unless req . user . _id + ' ' is userID or req . user . isAdmin ( ) or ( ' employer ' in ( req . user . get ( ' permissions ' ) ? [ ] ) )
2014-09-19 14:18:33 -04:00
query = creator: userID , levelID: { $in: [ ' criss-cross ' , ' gridmancer ' , ' greed ' , ' dungeon-arena ' , ' brawlwood ' , ' gold-rush ' ] }
2014-07-17 19:30:56 -04:00
projection = ' levelName levelID team playtime codeLanguage submitted code totalScore teamSpells level '
2014-06-11 22:38:41 -04:00
LevelSession . find ( query ) . select ( projection ) . exec (err, documents) =>
return @ sendDatabaseError ( res , err ) if err
documents = ( LevelSessionHandler . formatEntity ( req , doc ) for doc in documents )
@ sendSuccess ( res , documents )
2014-07-28 15:25:11 -04:00
IDify: (idOrSlug, done) ->
2014-07-29 06:45:47 -04:00
return done null , idOrSlug if Handler . isID idOrSlug
2014-08-15 13:35:59 -04:00
User . findBySlug idOrSlug , (err, user) -> done err , user ? . get ' _id '
2014-06-19 11:07:30 -04:00
2014-07-28 15:25:11 -04:00
getLevelSessions: (req, res, userIDOrSlug) ->
2014-07-29 06:45:47 -04:00
@ IDify userIDOrSlug , (err, userID) =>
return @ sendDatabaseError res , err if err
return @ sendNotFoundError res unless userID ?
2014-07-28 15:25:11 -04:00
query = creator: userID + ' '
2014-11-22 21:40:28 -05:00
isAuthorized = req . user ? . _id + ' ' is userID or req . user ? . isAdmin ( )
2014-07-28 15:25:11 -04:00
projection = { }
if req . query . project
projection [ field ] = 1 for field in req . query . project . split ( ' , ' ) when isAuthorized or not ( field in LevelSessionHandler . privateProperties )
else unless isAuthorized
projection [ field ] = 0 for field in LevelSessionHandler . privateProperties
2014-07-29 11:28:13 -04:00
sort = { }
sort.changed = req . query . order if req . query . order
2014-07-28 15:25:11 -04:00
2014-07-29 11:28:13 -04:00
LevelSession . find ( query ) . select ( projection ) . sort ( sort ) . exec (err, documents) =>
2014-07-28 15:25:11 -04:00
return @ sendDatabaseError ( res , err ) if err
documents = ( LevelSessionHandler . formatEntity ( req , doc ) for doc in documents )
@ sendSuccess ( res , documents )
2014-04-25 13:46:43 -04:00
2014-07-28 15:25:11 -04:00
getEarnedAchievements: (req, res, userIDOrSlug) ->
2014-07-29 06:45:47 -04:00
@ IDify userIDOrSlug , (err, userID) =>
return @ sendDatabaseError res , err if err
return @ sendNotFoundError res unless userID ?
2014-07-28 15:25:11 -04:00
query = user: userID + ' '
query.notified = false if req . query . notified is ' false '
EarnedAchievement . find ( query ) . sort ( changed: - 1 ) . exec (err, documents) =>
return @ sendDatabaseError ( res , err ) if err ?
cleandocs = ( @ formatEntity ( req , doc ) for doc in documents )
for doc in documents # TODO Ruben Maybe move this logic elsewhere
doc . set ( ' notified ' , true )
doc . save ( )
@ sendSuccess ( res , cleandocs )
2014-05-19 18:24:16 -04:00
2014-07-21 08:36:10 -04:00
getRecentlyPlayed: (req, res, userID) ->
twoWeeksAgo = moment ( ) . subtract ( ' days ' , 14 ) . toDate ( )
LevelSession . find ( creator: userID , changed: $gt: twoWeeksAgo ) . sort ( changed: - 1 ) . exec (err, docs) =>
return @ sendDatabaseError res , err if err ?
cleandocs = ( @ formatEntity ( req , doc ) for doc in docs )
@ sendSuccess res , cleandocs
2014-06-10 19:30:07 -04:00
trackActivity: (req, res, userID, activityName, increment=1) ->
return @ sendMethodNotAllowed res unless req . method is ' POST '
2014-11-22 21:40:28 -05:00
isMe = userID is req . user ? . _id + ' '
isAuthorized = isMe or req . user ? . isAdmin ( )
isAuthorized || = ( ' employer ' in ( req . user ? . get ( ' permissions ' ) ? [ ] ) ) and ( activityName in [ ' viewed_by_employer ' , ' contacted_by_employer ' ] )
2014-09-19 05:26:18 -04:00
return @ sendForbiddenError res unless isAuthorized
2014-06-10 19:30:07 -04:00
updateUser = (user) =>
activity = user . trackActivity activityName , increment
user . update { activity: activity } , (err) =>
return @ sendDatabaseError res , err if err
@ sendSuccess res , result: ' success '
if isMe
updateUser ( req . user )
else
@ getDocumentForIdOrSlug userID , (err, user) =>
return @ sendDatabaseError res , err if err
return @ sendNotFoundError res unless user
updateUser user
2014-04-24 20:36:07 -04:00
agreeToEmployerAgreement: (req, res) ->
userIsAnonymous = req . user ? . get ( ' anonymous ' )
2014-06-30 22:16:26 -04:00
if userIsAnonymous then return errors . unauthorized ( res , ' You need to be logged in to agree to the employer agreeement. ' )
2014-04-24 20:36:07 -04:00
profileData = req . body
#TODO: refactor this bit to make it more elegant
if not profileData . id or not profileData . positions or not profileData . emailAddress or not profileData . firstName or not profileData . lastName
2014-06-30 22:16:26 -04:00
return errors . badInput ( res , ' You need to have a more complete profile to sign up for this service. ' )
2014-04-24 20:36:07 -04:00
@ modelClass . findById ( req . user . id ) . exec (err, user) =>
2014-09-01 12:11:10 -04:00
if user . get ( ' employerAt ' ) or user . get ( ' signedEmployerAgreement ' ) or ' employer ' in ( user . get ( ' permissions ' ) ? [ ] )
2014-06-30 22:16:26 -04:00
return errors . conflict ( res , ' You already have signed the agreement! ' )
2014-04-24 20:36:07 -04:00
#TODO: Search for the current position
2014-06-30 22:16:26 -04:00
employerAt = _ . filter ( profileData . positions . values , ' isCurrent ' ) [ 0 ] ? . company . name ? ' Not available '
2014-04-25 13:46:43 -04:00
signedEmployerAgreement =
2014-04-24 20:36:07 -04:00
linkedinID: profileData . id
date: new Date ( )
data: profileData
2014-04-25 13:46:43 -04:00
updateObject =
2014-06-30 22:16:26 -04:00
' employerAt ' : employerAt
' signedEmployerAgreement ' : signedEmployerAgreement
$push: ' permissions ' : ' employer '
2014-04-25 13:46:43 -04:00
2014-06-30 22:16:26 -04:00
User . update { ' _id ' : req . user . id } , updateObject , (err, result) =>
2014-04-24 20:36:07 -04:00
if err ? then return errors . serverError ( res , " There was an issue updating the user object to reflect employer status: #{ err } " )
2014-06-30 22:16:26 -04:00
res . send ( { ' message ' : ' The agreement was successful. ' } )
2014-04-24 20:36:07 -04:00
res . end ( )
2014-04-25 13:46:43 -04:00
2014-04-06 20:01:56 -04:00
getCandidates: (req, res) ->
2014-11-22 21:40:28 -05:00
return @ sendForbiddenError ( res ) unless req . user
2014-12-08 10:07:29 -05:00
return @ sendForbiddenError ( res ) # No one can view the candidates, since in a rush, we deleted their index!
2014-09-01 12:11:10 -04:00
authorized = req . user . isAdmin ( ) or ( ' employer ' in ( req . user . get ( ' permissions ' ) ? [ ] ) )
2014-06-24 13:15:38 -04:00
months = if req . user . isAdmin ( ) then 12 else 2
since = ( new Date ( ( new Date ( ) ) - months * 30.4 * 86400 * 1000 ) ) . toISOString ( )
2014-04-23 13:21:58 -04:00
query = { ' jobProfile.updated ' : { $gt: since } }
query [ ' jobProfile.active ' ] = true unless req . user . isAdmin ( )
2014-06-10 19:30:07 -04:00
selection = ' jobProfile jobProfileApproved photoURL '
2014-06-17 16:03:08 -04:00
selection += ' email name ' if authorized
2014-04-07 18:21:05 -04:00
User . find ( query ) . select ( selection ) . exec (err, documents) =>
2014-04-06 20:01:56 -04:00
return @ sendDatabaseError ( res , err ) if err
2014-04-18 15:48:13 -04:00
candidates = ( candidate for candidate in documents when @ employerCanViewCandidate req . user , candidate . toObject ( ) )
candidates = ( @ formatCandidate ( authorized , candidate ) for candidate in candidates )
2014-04-07 18:21:05 -04:00
@ sendSuccess ( res , candidates )
formatCandidate: (authorized, document) ->
2014-07-03 16:59:10 -04:00
fields = if authorized then [ ' name ' , ' jobProfile ' , ' jobProfileApproved ' , ' photoURL ' , ' _id ' ] else [ ' _id ' , ' jobProfile ' , ' jobProfileApproved ' ]
2014-04-07 18:21:05 -04:00
obj = _ . pick document . toObject ( ) , fields
2014-07-03 16:59:10 -04:00
obj . photoURL || = obj . jobProfile . photoURL #if authorized
2014-07-02 18:48:26 -04:00
subfields = [ ' country ' , ' city ' , ' lookingFor ' , ' jobTitle ' , ' skills ' , ' experience ' , ' updated ' , ' active ' , ' shortDescription ' , ' curated ' , ' visa ' ]
2014-04-07 18:21:05 -04:00
if authorized
2014-04-11 15:49:44 -04:00
subfields = subfields . concat [ ' name ' ]
2014-04-07 18:21:05 -04:00
obj.jobProfile = _ . pick obj . jobProfile , subfields
obj
2014-03-31 18:48:22 -04:00
2014-04-18 12:53:28 -04:00
employerCanViewCandidate: (employer, candidate) ->
return true if employer . isAdmin ( )
for job in candidate . jobProfile ? . work ? [ ]
# TODO: be smarter about different ways to write same company names to ensure privacy.
# We'll have to manually pay attention to how we set employer names for now.
2014-04-18 15:48:13 -04:00
if job . employer ? . toLowerCase ( ) is employer . get ( ' employerAt ' ) ? . toLowerCase ( )
log . info " #{ employer . get ( ' name ' ) } at #{ employer . get ( ' employerAt ' ) } can ' t see #{ candidate . jobProfile . name } because s/he worked there. "
2014-04-18 12:53:28 -04:00
return false if job . employer ? . toLowerCase ( ) is employer . get ( ' employerAt ' ) ? . toLowerCase ( )
true
2014-06-10 19:30:07 -04:00
getEmployers: (req, res) ->
2014-11-22 21:40:28 -05:00
return @ sendForbiddenError ( res ) unless req . user ? . isAdmin ( )
2014-12-08 10:07:29 -05:00
return @ sendForbiddenError ( res ) # No one can view the employers, since in a rush, we deleted their index!
2014-06-17 18:17:19 -04:00
query = { employerAt: { $exists: true , $ne: ' ' } }
2014-06-10 19:30:07 -04:00
selection = ' name firstName lastName email activity signedEmployerAgreement photoURL employerAt '
User . find ( query ) . select ( selection ) . lean ( ) . exec (err, documents) =>
return @ sendDatabaseError res , err if err
@ sendSuccess res , documents
2014-04-12 17:53:09 -04:00
buildGravatarURL: (user, size, fallback) ->
2014-04-09 19:46:44 -04:00
emailHash = @ buildEmailHash user
2014-06-30 22:16:26 -04:00
fallback ? = ' http://codecombat.com/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png '
2014-04-12 17:53:09 -04:00
fallback = " http://codecombat.com #{ fallback } " unless /^http/ . test fallback
" https://www.gravatar.com/avatar/ #{ emailHash } ?s= #{ size } &default= #{ fallback } "
2014-04-09 19:46:44 -04:00
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 18:48:22 -04:00
2014-06-17 16:03:08 -04:00
getRemark: (req, res, userID) ->
2014-11-22 21:40:28 -05:00
return @ sendForbiddenError ( res ) unless req . user ? . isAdmin ( )
2014-06-17 16:03:08 -04:00
query = user: userID
projection = null
if req . query . project
projection = { }
projection [ field ] = 1 for field in req . query . project . split ( ' , ' )
UserRemark . findOne ( query ) . select ( projection ) . exec (err, remark) =>
return @ sendDatabaseError res , err if err
return @ sendNotFoundError res unless remark ?
@ sendSuccess res , remark
2014-08-30 20:09:57 -04:00
searchForUser: (req, res) ->
# TODO: also somehow search the CLAs to find a match amongst those fields and to find GitHub ids
2014-11-22 21:40:28 -05:00
return @ sendForbiddenError ( res ) unless req . user ? . isAdmin ( )
2014-08-30 20:09:57 -04:00
search = req . body . search
query = email: { $exists: true } , $or: [
{ emailLower: search }
{ nameLower: search }
]
query . $or . push { _id: mongoose . Types . ObjectId ( search ) if isID search }
if search . length > 5
searchParts = search . split ( /[.+@]/ )
if searchParts . length > 1
query . $or . push { emailLower: { $regex: ' ^ ' + searchParts [ 0 ] } }
projection = name: 1 , email: 1 , dateCreated: 1
User . find ( query ) . select ( projection ) . lean ( ) . exec (err, users) =>
return @ sendDatabaseError res , err if err
@ sendSuccess res , users
2014-06-27 15:30:31 -04:00
countEdits = (model, done) ->
statKey = User . statsMapping . edits [ model . modelName ]
return done ( new Error ' Could not resolve statKey for model ' ) unless statKey ?
2014-08-14 13:17:44 -04:00
userStream = User . find ( ) . stream ( )
streamFinished = false
usersTotal = 0
usersFinished = 0
doneWithUser = (err) ->
log . error err if err ?
++ usersFinished
done ? ( ) if streamFinished and usersFinished is usersTotal
userStream . on ' error ' , (err) -> log . error err
userStream . on ' close ' , -> streamFinished = true
userStream . on ' data ' , (user) ->
2014-08-29 15:58:23 -04:00
usersTotal += 1
2014-08-14 19:40:35 -04:00
userObjectID = user . get ( ' _id ' )
userStringID = userObjectID . toHexString ( )
model . count { $or: [ creator: userObjectID , creator: userStringID ] } , (err, count) ->
if count
update = $set: { }
update . $set [ statKey ] = count
else
update = $unset: { }
update . $unset [ statKey ] = ' '
User . findByIdAndUpdate user . get ( ' _id ' ) , update , (err) ->
log . error err if err ?
2014-08-14 13:17:44 -04:00
doneWithUser ( )
2014-06-25 14:04:39 -04:00
2014-07-24 08:41:06 -04:00
# I don't like leaking big variables, could remove this for readability
# Meant for passing into MongoDB
{ isMiscPatch , isTranslationPatch } = do ->
2014-11-28 20:49:41 -05:00
deltas = require ' ../../app/core/deltas '
2014-07-24 08:41:06 -04:00
2014-07-24 13:42:43 -04:00
isMiscPatch: (obj) ->
expanded = deltas . flattenDelta obj . get ' delta '
_ . some expanded , (delta) -> ' i18n ' not in delta . dataPath
isTranslationPatch: (obj) ->
expanded = deltas . flattenDelta obj . get ' delta '
_ . some expanded , (delta) -> ' i18n ' in delta . dataPath
Patch = require ' ../patches/Patch '
# filter is passed a mongoose document and should return a boolean,
# determining whether the patch should be counted
countPatchesByUsersInMemory = (query, filter, statName, done) ->
updateUser = (user, count, doneUpdatingUser) ->
method = if count then ' $set ' else ' $unset '
update = { }
update [ method ] = { }
update [ method ] [ statName ] = count or ' '
User . findByIdAndUpdate user . get ( ' _id ' ) , update , doneUpdatingUser
2014-08-14 13:17:44 -04:00
userStream = User . find ( ) . stream ( )
streamFinished = false
usersTotal = 0
usersFinished = 0
doneWithUser = (err) ->
log . error err if err ?
++ usersFinished
done ? ( ) if streamFinished and usersFinished is usersTotal
userStream . on ' error ' , (err) -> log . error err
userStream . on ' close ' , -> streamFinished = true
userStream . on ' data ' , (user) ->
2014-08-29 15:58:23 -04:00
usersTotal += 1
2014-08-14 19:40:35 -04:00
userObjectID = user . get ' _id '
userStringID = userObjectID . toHexString ( )
# Extend query with a patch ownership test
2014-08-14 13:17:44 -04:00
_ . extend query , { $or: [ { creator: userObjectID } , { creator: userStringID } ] }
2014-08-14 19:40:35 -04:00
count = 0
2014-08-14 13:17:44 -04:00
stream = Patch . where ( query ) . stream ( )
stream . on ' data ' , (doc) -> ++ count if filter doc
stream . on ' error ' , (err) ->
updateUser user , count , doneWithUser
log . error " Recalculating #{ statName } for user #{ user } stopped prematurely because of error "
stream . on ' close ' , ->
updateUser user , count , doneWithUser
2014-07-24 08:41:06 -04:00
countPatchesByUsers = (query, statName, done) ->
Patch = require ' ../patches/Patch '
2014-08-14 13:17:44 -04:00
userStream = User . find ( ) . stream ( )
streamFinished = false
usersTotal = 0
usersFinished = 0
doneWithUser = (err) ->
log . error err if err ?
++ usersFinished
done ? ( ) if streamFinished and usersFinished is usersTotal
userStream . on ' error ' , (err) -> log . error err
userStream . on ' close ' , -> streamFinished = true
userStream . on ' data ' , (user) ->
2014-08-29 15:58:23 -04:00
usersTotal += 1
2014-08-14 19:40:35 -04:00
userObjectID = user . get ' _id '
userStringID = userObjectID . toHexString ( )
# Extend query with a patch ownership test
_ . extend query , { $or: [ { creator: userObjectID } , { creator: userStringID } ] }
Patch . count query , (err, count) ->
method = if count then ' $set ' else ' $unset '
update = { }
update [ method ] = { }
update [ method ] [ statName ] = count or ' '
2014-08-14 13:17:44 -04:00
User . findByIdAndUpdate user . get ( ' _id ' ) , update , doneWithUser
2014-07-24 08:41:06 -04:00
2014-07-23 09:22:53 -04:00
statRecalculators:
2014-06-24 12:14:26 -04:00
gamesCompleted: (done) ->
LevelSession = require ' ../levels/sessions/LevelSession '
2014-08-14 13:17:44 -04:00
userStream = User . find ( ) . stream ( )
streamFinished = false
usersTotal = 0
usersFinished = 0
doneWithUser = (err) ->
log . error err if err ?
++ usersFinished
done ? ( ) if streamFinished and usersFinished is usersTotal
userStream . on ' error ' , (err) -> log . error err
userStream . on ' close ' , -> streamFinished = true
userStream . on ' data ' , (user) ->
2014-08-29 15:58:23 -04:00
usersTotal += 1
2014-08-14 19:40:35 -04:00
userID = user . get ( ' _id ' ) . toHexString ( )
2014-12-04 15:57:57 -05:00
LevelSession . count { creator: userID , ' state.complete ' : true } , (err, count) ->
2014-08-14 19:40:35 -04:00
update = if count then { $set: ' stats.gamesCompleted ' : count } else { $unset: ' stats.gamesCompleted ' : ' ' }
2014-08-14 13:17:44 -04:00
User . findByIdAndUpdate user . get ( ' _id ' ) , update , doneWithUser
2014-06-24 14:27:22 -04:00
articleEdits: (done) ->
Article = require ' ../articles/Article '
2014-08-29 15:58:23 -04:00
countEdits Article , done
2014-06-24 14:27:22 -04:00
2014-06-25 14:04:39 -04:00
levelEdits: (done) ->
Level = require ' ../levels/Level '
2014-06-27 15:30:31 -04:00
countEdits Level , done
2014-06-25 14:04:39 -04:00
levelComponentEdits: (done) ->
LevelComponent = require ' ../levels/components/LevelComponent '
2014-06-27 15:30:31 -04:00
countEdits LevelComponent , done
2014-06-25 14:04:39 -04:00
levelSystemEdits: (done) ->
LevelSystem = require ' ../levels/systems/LevelSystem '
2014-06-27 15:30:31 -04:00
countEdits LevelSystem , done
2014-06-25 14:04:39 -04:00
thangTypeEdits: (done) ->
ThangType = require ' ../levels/thangs/ThangType '
2014-06-27 15:30:31 -04:00
countEdits ThangType , done
2014-06-24 14:27:22 -04:00
2014-07-23 14:00:28 -04:00
patchesContributed: (done) ->
2014-07-24 08:41:06 -04:00
countPatchesByUsers { ' status ' : ' accepted ' } , ' stats.patchesContributed ' , done
2014-07-23 14:00:28 -04:00
patchesSubmitted: (done) ->
2014-07-24 08:41:06 -04:00
countPatchesByUsers { } , ' stats.patchesSubmitted ' , done
2014-07-23 14:00:28 -04:00
2014-07-24 13:42:43 -04:00
# The below need functions for filtering and are thus checked in memory
2014-07-24 08:41:06 -04:00
totalTranslationPatches: (done) ->
2014-07-24 13:42:43 -04:00
countPatchesByUsersInMemory { } , isTranslationPatch , ' stats.totalTranslationPatches ' , done
2014-07-23 14:00:28 -04:00
2014-07-24 08:41:06 -04:00
totalMiscPatches: (done) ->
2014-07-24 13:42:43 -04:00
countPatchesByUsersInMemory { } , isMiscPatch , ' stats.totalMiscPatches ' , done
articleMiscPatches: (done) ->
countPatchesByUsersInMemory { ' target.collection ' : ' article ' } , isMiscPatch , User . statsMapping . misc . article , done
levelMiscPatches: (done) ->
countPatchesByUsersInMemory { ' target.collection ' : ' level ' } , isMiscPatch , User . statsMapping . misc . level , done
levelComponentMiscPatches: (done) ->
countPatchesByUsersInMemory { ' target.collection ' : ' level_component ' } , isMiscPatch , User . statsMapping . misc [ ' level.component ' ] , done
levelSystemMiscPatches: (done) ->
countPatchesByUsersInMemory { ' target.collection ' : ' level_system ' } , isMiscPatch , User . statsMapping . misc [ ' level.system ' ] , done
thangTypeMiscPatches: (done) ->
countPatchesByUsersInMemory { ' target.collection ' : ' thang_type ' } , isMiscPatch , User . statsMapping . misc [ ' thang.type ' ] , done
2014-07-24 08:41:06 -04:00
articleTranslationPatches: (done) ->
2014-07-24 13:42:43 -04:00
countPatchesByUsersInMemory { ' target.collection ' : ' article ' } , isTranslationPatch , User . statsMapping . translations . article , done
2014-07-23 14:00:28 -04:00
2014-07-24 13:42:43 -04:00
levelTranslationPatches: (done) ->
countPatchesByUsersInMemory { ' target.collection ' : ' level ' } , isTranslationPatch , User . statsMapping . translations . level , done
levelComponentTranslationPatches: (done) ->
countPatchesByUsersInMemory { ' target.collection ' : ' level_component ' } , isTranslationPatch , User . statsMapping . translations [ ' level.component ' ] , done
levelSystemTranslationPatches: (done) ->
countPatchesByUsersInMemory { ' target.collection ' : ' level_system ' } , isTranslationPatch , User . statsMapping . translations [ ' level.system ' ] , done
thangTypeTranslationPatches: (done) ->
countPatchesByUsersInMemory { ' target.collection ' : ' thang_type ' } , isTranslationPatch , User . statsMapping . translations [ ' thang.type ' ] , done
2014-07-23 14:00:28 -04:00
2014-08-30 20:09:57 -04:00
2014-07-23 14:00:28 -04:00
recalculateStats: (statName, done) =>
2014-07-24 08:41:06 -04:00
done new Error ' Recalculation handler not found ' unless statName of @ statRecalculators
2014-07-23 09:22:53 -04:00
@ statRecalculators [ statName ] done
2014-06-24 12:14:26 -04:00
recalculate: (req, res, statName) ->
2014-11-22 21:40:28 -05:00
return @ sendForbiddenError ( res ) unless req . user ? . isAdmin ( )
2014-07-23 09:22:53 -04:00
log . debug ' recalculate '
return @ sendNotFoundError ( res ) unless statName of @ statRecalculators
2014-07-23 14:00:28 -04:00
@ recalculateStats statName
2014-07-23 09:22:53 -04:00
@ sendAccepted res , { }
2014-06-17 16:03:08 -04:00
2014-03-16 11:11:55 -04:00
module.exports = new UserHandler ( )