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 '
2016-04-06 13:56:06 -04:00
User = require ' ./../models/User '
2014-02-17 18:19:19 -05:00
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 '
2016-04-06 13:56:06 -04:00
AnalyticsLogEvent = require ' ../models/AnalyticsLogEvent '
Clan = require ' ../models/Clan '
CourseInstance = require ' ../models/CourseInstance '
LevelSession = require ' ../models/LevelSession '
2016-04-07 12:40:53 -04:00
LevelSessionHandler = require ' ./level_session_handler '
2016-04-06 13:56:06 -04:00
Payment = require ' ../models/Payment '
2016-04-07 12:40:53 -04:00
SubscriptionHandler = require ' ./subscription_handler '
DiscountHandler = require ' ./discount_handler '
2016-04-06 13:56:06 -04:00
EarnedAchievement = require ' ../models/EarnedAchievement '
2016-04-07 12:40:53 -04:00
UserRemark = require ' ./../models/UserRemark '
2015-03-13 18:19:20 -04:00
{ findStripeSubscription } = require ' ../lib/utils '
2014-08-30 20:09:57 -04:00
{ isID } = require ' ../lib/utils '
2016-03-18 20:05:21 -04:00
slack = require ' ../slack '
2015-02-04 16:54:35 -05:00
sendwithus = require ' ../sendwithus '
2016-04-06 13:56:06 -04:00
Prepaid = require ' ../models/Prepaid '
UserPollsRecord = require ' ../models/UserPollsRecord '
EarnedAchievement = require ' ../models/EarnedAchievement '
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
2015-02-19 16:14:34 -05:00
allowedMethods: [ ' GET ' , ' POST ' , ' PUT ' , ' PATCH ' , ' DELETE ' ]
2014-04-07 20:58:02 -04:00
getEditableProperties: (req, document) ->
props = super req , document
2016-03-11 18:00:55 -05:00
props . push ' permissions ' unless config . isProduction or global . testing
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
2016-03-09 17:40:52 -05:00
if not req . user . isAdmin ( )
2016-05-11 17:39:26 -04:00
if document . isTeacher ( ) and req . body . role not in User . teacherRoles
props = _ . without props , ' role '
2014-04-07 20:58:02 -04:00
props
2014-09-22 17:56:02 -04:00
formatEntity: (req, document, publicOnly=false) =>
2016-03-03 17:22:50 -05:00
# TODO: Delete. This function is duplicated in server User model toObject transform.
2014-01-03 13:32:13 -05:00
return null unless document ?
obj = document . toObject ( )
2016-03-03 17:22:50 -05:00
delete obj [ prop ] for prop in User . serverProperties
includePrivates = not publicOnly and ( req . user and ( req . user . isAdmin ( ) or req . user . _id . equals ( document . _id ) or req . session . amActually is document . id ) )
delete obj [ prop ] for prop in User . 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 ) )
2016-03-03 17:22:50 -05:00
delete obj [ prop ] for prop in User . 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 ' }
2016-06-17 13:35:22 -04:00
log . info ' 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) ->
2015-03-13 18:19:20 -04:00
# TODO: Make subscribe vs. unsubscribe explicit. This property dance is confusing.
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
2015-03-13 18:19:20 -04:00
finishSubscription = (hasPlan, wantsPlan) ->
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 )
)
if req . body . stripe . subscribeEmails ?
2014-12-02 23:01:35 -05:00
SubscriptionHandler . subscribeUser ( req , user , (err) ->
return callback ( err ) if err
return callback ( null , req , user )
)
2015-03-13 18:19:20 -04:00
else if req . body . stripe . unsubscribeEmail ?
2014-12-02 23:01:35 -05:00
SubscriptionHandler . unsubscribeUser ( req , user , (err) ->
return callback ( err ) if err
return callback ( null , req , user )
)
2015-03-13 18:19:20 -04:00
else
wantsPlan = req . body . stripe . planID ?
2015-03-19 18:02:45 -04:00
hasPlan = user . get ( ' stripe ' ) ? . planID ? and not req . body . stripe . prepaidCode ?
2015-03-13 18:19:20 -04:00
finishSubscription hasPlan , wantsPlan
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 )
2015-10-14 09:02:00 -04:00
return @ sendSuccess ( res , @ formatEntity ( req , req . user ) )
2014-01-03 13:32:13 -05:00
super ( req , res , id )
2014-03-20 18:40:02 -04:00
2015-07-08 20:34:31 -04:00
getByIDs: (req, res) ->
return @ sendForbiddenError ( res ) unless req . user ? . isAdmin ( )
User . find { _id: { $in: req . body . ids } } , (err, users) =>
return @ sendDatabaseError ( res , err ) if err
cleandocs = ( @ formatEntity ( req , doc ) for doc in users )
@ sendSuccess ( res , cleandocs )
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
2015-02-12 11:50:45 -05:00
properties = if returnWizard then ' name wizard firstName lastName ' else ' name firstName lastName '
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 )
2015-12-05 11:18:36 -05:00
leaderboardQuery . cache ( 10 * 60 * 1000 ) if req . query . scoreOffset is - 1
2014-04-14 17:52:21 -04:00
leaderboardQuery . exec (err, otherUsers) ->
2015-04-19 17:51:57 -04:00
otherUsers = _ . reject otherUsers , _id: req . user . _id if req . query . scoreOffset isnt - 1 and req . user
2015-02-26 20:20:27 -05:00
otherUsers ? = [ ]
res . send ( otherUsers )
res . end ( )
2014-04-14 17:52:21 -04:00
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) ->
2015-02-19 16:14:34 -05:00
if req . route . method in [ ' put ' , ' post ' , ' patch ' , ' delete ' ]
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
2015-02-19 16:14:34 -05:00
delete : (req, res, userID) ->
2015-02-24 08:36:12 -05:00
# Instead of just deleting the User object, we should remove all the properties except for _id
2015-02-24 11:54:30 -05:00
# And add a `deleted: true` property
2015-02-19 16:14:34 -05:00
@ getDocumentForIdOrSlug userID , (err, user) => # Check first
return @ sendDatabaseError res , err if err
return @ sendNotFoundError res unless user
return @ sendForbiddenError res unless @ hasAccessToDocument ( req , user )
2015-02-26 20:20:27 -05:00
2015-06-18 18:02:15 -04:00
# Delete subscriptions attached to this user first
# TODO: check sponsored subscriptions (stripe.recipients)
checkPersonalSubscription = (user, done) =>
try
return done ( ) unless user . get ( ' stripe ' ) ? . subscriptionID ?
SubscriptionHandler . unsubscribeUser { body: user . toObject ( ) } , user , (err) =>
log . error ( " User delete check personal sub " + err ) if err
done ( )
catch error
log . error ( " User delete check personal sub " + error )
done ( )
checkRecipientSubscription = (user, done) =>
try
return done ( ) unless sponsorID = user . get ( ' stripe ' ) ? . sponsorID
User . findById sponsorID , (err, sponsor) =>
if err
log . error ( " User delete check recipient sub " + err )
return done ( )
unless sponsor
log . error ( " User delete check recipient sub no sponsor #{ user . get ( ' stripe ' ) . sponsorID } " )
return done ( )
sponsorObject = sponsor . toObject ( )
sponsorObject.stripe.unsubscribeEmail = user . get ( ' email ' )
SubscriptionHandler . unsubscribeRecipient { body: sponsorObject } , sponsor , (err) =>
log . error ( " User delete check recipient sub " + err ) if err
done ( )
catch error
log . error ( " User delete check recipient sub " + error )
done ( )
deleteSubscriptions = (user, done) =>
checkPersonalSubscription user , (err) =>
checkRecipientSubscription user , done
2015-02-24 11:54:30 -05:00
2015-06-18 18:02:15 -04:00
deleteSubscriptions user , =>
obj = user . toObject ( )
for prop , val of obj
user . set ( prop , undefined ) unless prop is ' _id '
2015-06-18 18:17:56 -04:00
user . set ( ' dateDeleted ' , new Date ( ) )
2015-06-18 18:02:15 -04:00
user . set ( ' deleted ' , true )
# Hack to get saving of Users to work. Probably should replace these props with strings
# so that validation doesn't get hung up on Date objects in the documents.
delete obj . dateCreated
user . save (err) =>
return @ sendDatabaseError ( res , err ) if err
@ sendNoContent res
2015-02-19 16:14:34 -05:00
2014-01-03 13:32:13 -05:00
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 '
2015-07-08 20:34:31 -04:00
return @ getByIDs ( req , res ) if args [ 1 ] is ' users '
2014-04-18 14:17:13 -04:00
return @ getNamesByIDs ( req , res ) if args [ 1 ] is ' names '
2015-09-25 13:03:44 -04:00
return @ getPrepaidCodes ( req , res ) if args [ 1 ] is ' prepaid_codes '
2016-02-04 20:31:25 -05:00
return @ getSchoolCounts ( req , res ) if args [ 1 ] is ' school_counts '
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 '
2015-04-06 14:17:13 -04:00
return @ getClans ( req , res , args [ 0 ] ) if args [ 1 ] is ' clans '
2015-09-03 14:04:40 -04:00
return @ getCourseInstances ( req , res , args [ 0 ] ) if args [ 1 ] is ' course_instances '
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 '
2015-03-13 18:19:20 -04:00
return @ getSubRecipients ( req , res ) if args [ 1 ] is ' sub_recipients '
return @ getSubSponsor ( req , res ) if args [ 1 ] is ' sub_sponsor '
2015-07-08 20:34:31 -04:00
return @ getSubSponsors ( req , res ) if args [ 1 ] is ' sub_sponsors '
2015-02-04 16:54:35 -05:00
return @ sendOneTimeEmail ( req , res , args [ 0 ] ) if args [ 1 ] is ' send_one_time_email '
2015-11-12 18:27:28 -05:00
return @ resetProgress ( req , res , args [ 0 ] ) if args [ 1 ] is ' reset_progress '
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
2015-03-13 18:19:20 -04:00
info = card: customer . sources ? . data ? [ 0 ]
findStripeSubscription customerID , subscriptionID: user . get ( ' stripe ' ) . subscriptionID , (subscription) =>
info.subscription = subscription
findStripeSubscription customerID , subscriptionID: user . get ( ' stripe ' ) . sponsorSubscriptionID , (subscription) =>
info.sponsorSubscription = subscription
@ sendSuccess ( res , JSON . stringify ( info , null , ' \t ' ) )
getSubRecipients: (req, res) ->
# Return map of userIDs to name/email/cancel date
# TODO: Add test for this API
return @ sendSuccess ( res , { } ) if _ . isEmpty ( req . user ? . get ( ' stripe ' ) ? . recipients ? [ ] )
return @ sendSuccess ( res , { } ) unless req . user . get ( ' stripe ' ) ? . customerID ?
# Get recipients User info
ids = ( recipient . userID for recipient in req . user . get ( ' stripe ' ) . recipients )
User . find ( { ' _id ' : { $in: ids } } , ' name emailLower ' ) . exec (err, users) =>
info = { }
_ . each users , (user) -> info [ user . id ] = user . toObject ( )
customerID = req . user . get ( ' stripe ' ) . customerID
nextBatch = (starting_after, done) ->
options = limit: 100
options.starting_after = starting_after if starting_after
stripe . customers . listSubscriptions customerID , options , (err, subscriptions) ->
return done ( err ) if err
return done ( ) unless subscriptions ? . data ? . length > 0
for sub in subscriptions . data
userID = sub . metadata ? . id
continue unless userID of info
if sub . cancel_at_period_end and info [ userID ] [ ' cancel_at_period_end ' ] isnt false
info [ userID ] [ ' cancel_at_period_end ' ] = new Date ( sub . current_period_end * 1000 )
else
info [ userID ] [ ' cancel_at_period_end ' ] = false
if subscriptions . has_more
return nextBatch ( subscriptions . data [ subscriptions . data . length - 1 ] . id , done )
else
return done ( )
nextBatch null , (err) =>
return @ sendDatabaseError ( res , err ) if err
@ sendSuccess ( res , info )
getSubSponsor: (req, res) ->
# TODO: Add test for this API
return @ sendSuccess ( res , { } ) unless req . user ? . get ( ' stripe ' ) ? . sponsorID ?
# Get sponsor User info
User . findById req . user . get ( ' stripe ' ) . sponsorID , (err, sponsor) =>
return @ sendDatabaseError ( res , err ) if err
return @ sendDatabaseError ( res , ' No sponsor customerID ' ) unless sponsor ? . get ( ' stripe ' ) ? . customerID ?
info =
email: sponsor . get ( ' emailLower ' )
name: sponsor . get ( ' name ' )
# Get recipient subscription info
2016-05-12 13:58:45 -04:00
findStripeSubscription sponsor . get ( ' stripe ' ) ? . customerID , userID: req . user . id , (subscription) =>
2015-03-13 18:19:20 -04:00
info.subscription = subscription
@ sendDatabaseError ( res , ' No sponsored subscription found ' ) unless info . subscription ?
@ sendSuccess ( res , info )
2014-12-05 19:47:44 -05:00
2015-07-08 20:34:31 -04:00
getSubSponsors: (req, res) ->
return @ sendForbiddenError ( res ) unless req . user ? . isAdmin ( )
2016-06-29 18:22:38 -04:00
Payment . find { $where: ' this.purchaser && this.recipient && this.purchaser.valueOf() != this.recipient.valueOf() ' } , (err, payments) =>
2015-07-08 20:34:31 -04:00
return @ sendDatabaseError ( res , err ) if err
2015-07-28 12:11:49 -04:00
sponsorIDs = ( payment . get ( ' purchaser ' ) for payment in payments )
User . find { $and: [ { _id: { $in: sponsorIDs } } , { " stripe.sponsorSubscriptionID " : { $exists: true } } ] } , (err, users) =>
return @ sendDatabaseError ( res , err ) if err
sponsors = ( @ formatEntity ( req , doc ) for doc in users when doc . get ( ' stripe ' ) . recipients ? . length > 0 )
@ sendSuccess ( res , sponsors )
2015-07-08 20:34:31 -04:00
2015-02-04 16:54:35 -05:00
sendOneTimeEmail: (req, res) ->
2015-02-06 17:49:47 -05:00
# TODO: Should this API be somewhere else?
# TODO: Where should email types be stored?
# TODO: How do we schema validate an update db call?
2015-02-04 16:54:35 -05:00
return @ sendForbiddenError ( res ) unless req . user
email = req . query . email or req . body . email
type = req . query . type or req . body . type
return @ sendBadInputError res , ' No email given. ' unless email ?
return @ sendBadInputError res , ' No type given. ' unless type ?
2015-02-06 17:49:47 -05:00
# log.warn "sendOneTimeEmail #{type} #{email}"
2015-04-09 00:48:21 -04:00
unless type in [ ' subscribe modal parent ' , ' share progress modal parent ' ]
2015-02-06 17:49:47 -05:00
return @ sendBadInputError res , " Unknown one-time email type #{ type } "
2015-02-04 16:54:35 -05:00
2015-02-06 17:49:47 -05:00
sendMail = (emailParams) =>
sendwithus . api . send emailParams , (err, result) =>
if err
log . error " sendwithus one-time email error: #{ err } , result: #{ result } "
return @ sendError res , 500 , ' send mail failed. '
req . user . update { $push: { " emails.oneTimes " : { type: type , email: email , sent: new Date ( ) } } } , (err) =>
return @ sendDatabaseError ( res , err ) if err
@ sendSuccess ( res , { result: ' success ' } )
AnalyticsLogEvent . logEvent req . user , ' Sent one time email ' , email: email , type: type
# Generic email data
2015-02-04 16:54:35 -05:00
emailParams =
recipient:
address: email
email_data:
name: req . user . get ( ' name ' ) or ' '
2015-02-04 17:29:16 -05:00
if codeLanguage = req . user . get ( ' aceConfig.language ' )
codeLanguage = codeLanguage [ 0 ] . toUpperCase ( ) + codeLanguage . slice ( 1 )
2015-04-09 00:48:21 -04:00
codeLanguage = codeLanguage . replace ' script ' , ' Script '
2015-02-04 17:29:16 -05:00
emailParams [ ' email_data ' ] [ ' codeLanguage ' ] = codeLanguage
2015-02-13 17:04:57 -05:00
if senderEmail = req . user . get ( ' email ' )
emailParams [ ' email_data ' ] [ ' senderEmail ' ] = senderEmail
2015-02-06 17:49:47 -05:00
# Type-specific email data
if type is ' subscribe modal parent '
2016-06-06 19:53:05 -04:00
emailParams [ ' email_id ' ] = sendwithus . templates . parent_subscribe_email
2015-04-09 00:48:21 -04:00
else if type is ' share progress modal parent '
2016-06-06 19:53:05 -04:00
emailParams [ ' email_id ' ] = sendwithus . templates . share_progress_email
2015-02-06 17:49:47 -05:00
sendMail emailParams
2015-02-04 16:54:35 -05:00
2015-09-25 13:03:44 -04:00
getPrepaidCodes: (req, res) ->
2015-11-17 19:09:34 -05:00
return @ sendSuccess ( res , [ ] ) unless req . user ?
2015-09-25 13:03:44 -04:00
orQuery = [ { creator: req . user . _id } , { ' redeemers.userID ' : req . user . _id } ]
Prepaid . find ( { } ) . or ( orQuery ) . exec (err, documents) =>
@ sendSuccess ( res , documents )
2016-02-04 20:31:25 -05:00
getSchoolCounts: (req, res) ->
return @ sendSuccess ( res , [ ] ) unless req . user ? . isAdmin ( )
minCount = req . body . minCount ? 20
query = { $and: [
{ anonymous: false } ,
{ schoolName: { $exists: true } } ,
{ schoolName: { $ne: ' ' } }
] }
User . find ( query , { schoolName: 1 } ) . exec (err, documents) =>
return @ sendDatabaseError ( res , err ) if err
schoolCountMap = { }
for doc in documents
schoolName = doc . get ( ' schoolName ' )
schoolCountMap [ schoolName ] ? = 0 ;
schoolCountMap [ schoolName ] ++ ;
schoolCounts = [ ]
for schoolName , count of schoolCountMap
continue unless count >= minCount
schoolCounts . push schoolName: schoolName , count: count
@ sendSuccess ( res , schoolCounts )
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 ' } )
2016-03-21 08:36:44 -04:00
slack . sendSlackMessage " #{ req . body . githubUsername or req . user . get ( ' name ' ) } just signed the CLA. " , [ ' dev-feed ' ]
2014-01-03 13:32:13 -05:00
2014-02-07 19:11:07 -05:00
avatar: (req, res, id) ->
2016-03-17 16:09:00 -04:00
if not isID ( id )
return @ sendBadInputError ( res , ' Invalid avatar 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
2015-10-29 08:44:54 -04:00
LevelSession . find ( query ) . select ( projection ) . exec (err, documents) =>
2014-07-28 15:25:11 -04:00
return @ sendDatabaseError ( res , err ) if err
2015-10-29 08:44:54 -04:00
if req . query . order
documents = _ . sortBy documents , ' changed '
if req . query . order + ' ' is ' -1 '
documents . reverse ( )
2014-07-28 15:25:11 -04:00
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 )
@ 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 )
2015-04-06 14:17:13 -04:00
getClans: (req, res, userIDOrSlug) ->
@ getDocumentForIdOrSlug userIDOrSlug , (err, user) =>
2015-04-21 14:22:17 -04:00
return @ sendNotFoundError ( res ) unless user
2015-04-06 14:17:13 -04:00
clanIDs = user . get ( ' clans ' ) ? [ ]
2015-04-10 19:04:36 -04:00
query = { $and: [ { _id: { $in: clanIDs } } ] }
2015-04-21 14:22:17 -04:00
query [ ' $and ' ] . push { type: ' public ' } unless req . user ? . id is user . id
2015-04-10 19:04:36 -04:00
Clan . find query , (err, documents) =>
2015-04-06 14:17:13 -04:00
return @ sendDatabaseError ( res , err ) if err
@ sendSuccess ( res , documents )
2015-04-02 20:00:28 -04:00
2015-09-03 14:04:40 -04:00
getCourseInstances: (req, res, userIDOrSlug) ->
@ getDocumentForIdOrSlug userIDOrSlug , (err, user) =>
return @ sendNotFoundError ( res ) unless user
CourseInstance . find { members: { $in: [ user . get ( ' _id ' ) ] } } , (err, documents) =>
return @ sendDatabaseError ( res , err ) if err
@ sendSuccess ( res , documents )
2014-04-07 18:21:05 -04:00
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
2016-05-31 12:30:07 -04:00
fallback ? = ' https://codecombat.com/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png '
fallback = " https://codecombat.com #{ fallback } " unless /^http/ . test fallback
2014-04-12 17:53:09 -04:00
" 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
2015-11-12 18:27:28 -05:00
resetProgress: (req, res, userID) ->
return @ sendMethodNotAllowed res unless req . method is ' POST '
return @ sendForbiddenError res unless userID and userID is req . user ? . _id + ' ' # Only you can reset your own progress
return @ sendForbiddenError res if req . user ? . isAdmin ( ) # Protect admins from resetting their progress
2015-11-27 15:12:47 -05:00
@ constructor . resetProgressForUser req . user , (err, results) =>
2015-11-12 18:27:28 -05:00
return @ sendDatabaseError res , err if err
@ sendSuccess res , result: ' success '
2015-11-27 15:12:47 -05:00
@resetProgressForUser: (user, cb) ->
2015-11-26 09:54:23 -05:00
async . parallel [
(cb) -> LevelSession . remove { creator: user . _id + ' ' } , cb
(cb) -> EarnedAchievement . remove { user: user . _id + ' ' } , cb
(cb) -> UserPollsRecord . remove { user: user . _id + ' ' } , cb
(cb) -> user . update { points: 0 , ' stats.gamesCompleted ' : 0 , ' stats.concepts ' : { } , ' earned.gems ' : 0 , ' earned.levels ' : [ ] , ' earned.items ' : [ ] , ' earned.heroes ' : [ ] , ' purchased.items ' : [ ] , ' purchased.heroes ' : [ ] , spent: 0 } , cb
] , cb
2014-08-30 20:09:57 -04:00
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 ?
2015-02-17 23:51:22 -05:00
userStream = User . find ( { anonymous: false } ) . sort ( ' _id ' ) . stream ( )
2014-08-14 13:17:44 -04:00
streamFinished = false
usersTotal = 0
usersFinished = 0
2015-02-17 23:51:22 -05:00
numberRunning = 0
2014-08-14 13:17:44 -04:00
doneWithUser = (err) ->
log . error err if err ?
++ usersFinished
2015-02-17 23:51:22 -05:00
- - numberRunning
userStream . resume ( )
2014-08-14 13:17:44 -04:00
done ? ( ) if streamFinished and usersFinished is usersTotal
userStream . on ' error ' , (err) -> log . error err
userStream . on ' close ' , -> streamFinished = true
userStream . on ' data ' , (user) ->
2015-02-17 23:51:22 -05:00
++ usersTotal
++ numberRunning
userStream . pause ( ) if numberRunning > 20
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 ] = ' '
2016-06-17 13:35:22 -04:00
log . info " ... updating #{ userStringID } patches #{ statKey } to #{ count } , #{ usersTotal } players found so far. " if count
2014-08-14 19:40:35 -04:00
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
2016-04-06 13:56:06 -04:00
Patch = require ' ../models/Patch '
2014-07-24 13:42:43 -04:00
# 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 ' '
2016-06-17 13:35:22 -04:00
log . info " ... updating #{ user . get ( ' _id ' ) } patches #{ JSON . stringify ( query ) } #{ statName } to #{ count } , #{ usersTotal } players found so far. " if count
2014-07-24 13:42:43 -04:00
User . findByIdAndUpdate user . get ( ' _id ' ) , update , doneUpdatingUser
2015-02-17 23:51:22 -05:00
userStream = User . find ( { anonymous: false } ) . sort ( ' _id ' ) . stream ( )
2014-08-14 13:17:44 -04:00
streamFinished = false
usersTotal = 0
usersFinished = 0
2015-02-17 23:51:22 -05:00
numberRunning = 0
2014-08-14 13:17:44 -04:00
doneWithUser = (err) ->
log . error err if err ?
++ usersFinished
2015-02-17 23:51:22 -05:00
- - numberRunning
userStream . resume ( )
2014-08-14 13:17:44 -04:00
done ? ( ) if streamFinished and usersFinished is usersTotal
userStream . on ' error ' , (err) -> log . error err
userStream . on ' close ' , -> streamFinished = true
userStream . on ' data ' , (user) ->
2015-02-17 23:51:22 -05:00
++ usersTotal
++ numberRunning
userStream . pause ( ) if numberRunning > 20
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) ->
2016-04-06 13:56:06 -04:00
Patch = require ' ../models/Patch '
2014-07-24 08:41:06 -04:00
2015-02-17 23:51:22 -05:00
userStream = User . find ( { anonymous: false } ) . sort ( ' _id ' ) . stream ( )
2014-08-14 13:17:44 -04:00
streamFinished = false
usersTotal = 0
usersFinished = 0
2015-02-17 23:51:22 -05:00
numberRunning = 0
2014-08-14 13:17:44 -04:00
doneWithUser = (err) ->
log . error err if err ?
++ usersFinished
2015-02-17 23:51:22 -05:00
- - numberRunning
userStream . resume ( )
2014-08-14 13:17:44 -04:00
done ? ( ) if streamFinished and usersFinished is usersTotal
userStream . on ' error ' , (err) -> log . error err
userStream . on ' close ' , -> streamFinished = true
userStream . on ' data ' , (user) ->
2015-02-17 23:51:22 -05:00
++ usersTotal
++ numberRunning
userStream . pause ( ) if numberRunning > 20
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 ' '
2016-06-17 13:35:22 -04:00
log . info " ... updating #{ userStringID } patches #{ query } to #{ count } , #{ usersTotal } players found so far. " if count
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) ->
2016-04-06 13:56:06 -04:00
LevelSession = require ' ../models/LevelSession '
2014-06-24 12:14:26 -04:00
2015-02-17 23:51:22 -05:00
userStream = User . find ( { anonymous: false } ) . sort ( ' _id ' ) . stream ( )
2014-08-14 13:17:44 -04:00
streamFinished = false
usersTotal = 0
usersFinished = 0
2015-02-17 23:51:22 -05:00
numberRunning = 0
2014-08-14 13:17:44 -04:00
doneWithUser = (err) ->
log . error err if err ?
++ usersFinished
2015-02-17 23:51:22 -05:00
- - numberRunning
userStream . resume ( )
if streamFinished and usersFinished is usersTotal
2016-06-17 13:35:22 -04:00
log . info " ----------- Finished recalculating statistics for gamesCompleted for #{ usersFinished } players. ----------- "
2015-02-17 23:51:22 -05:00
done ? ( )
2014-08-14 13:17:44 -04:00
userStream . on ' error ' , (err) -> log . error err
userStream . on ' close ' , -> streamFinished = true
userStream . on ' data ' , (user) ->
2015-02-17 23:51:22 -05:00
++ usersTotal
++ numberRunning
userStream . pause ( ) if numberRunning > 20
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 ' : ' ' }
2016-06-17 13:35:22 -04:00
log . info " ... updating #{ userID } gamesCompleted to #{ count } , #{ usersTotal } players found so far. " if Math . random ( ) < 0.001
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) ->
2016-04-06 13:56:06 -04:00
Article = require ' ../models/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) ->
2016-04-06 13:56:06 -04:00
Level = require ' ../models/Level '
2014-06-27 15:30:31 -04:00
countEdits Level , done
2014-06-25 14:04:39 -04:00
levelComponentEdits: (done) ->
2016-04-06 13:56:06 -04:00
LevelComponent = require ' ../models/LevelComponent '
2014-06-27 15:30:31 -04:00
countEdits LevelComponent , done
2014-06-25 14:04:39 -04:00
levelSystemEdits: (done) ->
2016-04-06 13:56:06 -04:00
LevelSystem = require ' ../models/LevelSystem '
2014-06-27 15:30:31 -04:00
countEdits LevelSystem , done
2014-06-25 14:04:39 -04:00
thangTypeEdits: (done) ->
2016-04-06 13:56:06 -04:00
ThangType = require ' ../models/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 ( )