2016-04-06 13:56:06 -04:00
mongoose = require ' mongoose '
jsonschema = require ' ../../app/schemas/models/user '
crypto = require ' crypto '
{ salt , isProduction } = require ' ../../server_config '
mail = require ' ../commons/mail '
log = require ' winston '
plugins = require ' ../plugins/plugins '
AnalyticsUsersActive = require ' ./AnalyticsUsersActive '
2016-05-16 17:33:20 -04:00
Classroom = require ' ../models/Classroom '
2016-04-11 19:51:51 -04:00
languages = require ' ../routes/languages '
2016-05-11 17:39:26 -04:00
_ = require ' lodash '
2016-06-20 19:44:07 -04:00
errors = require ' ../commons/errors '
2016-03-03 17:22:50 -05:00
2016-04-06 13:56:06 -04:00
config = require ' ../../server_config '
stripe = require ( ' stripe ' ) ( config . stripe . secretKey )
sendwithus = require ' ../sendwithus '
UserSchema = new mongoose . Schema ( {
dateCreated:
type: Date
' default ' : Date . now
} , { strict: false } )
UserSchema . index ( { ' dateCreated ' : 1 } )
UserSchema . index ( { ' emailLower ' : 1 } , { unique: true , sparse: true , name: ' emailLower_1 ' } )
UserSchema . index ( { ' facebookID ' : 1 } , { sparse: true } )
UserSchema . index ( { ' gplusID ' : 1 } , { sparse: true } )
UserSchema . index ( { ' iosIdentifierForVendor ' : 1 } , { name: ' iOS identifier for vendor ' , sparse: true , unique: true } )
UserSchema . index ( { ' mailChimp.leid ' : 1 } , { sparse: true } )
UserSchema . index ( { ' nameLower ' : 1 } , { sparse: true , name: ' nameLower_1 ' } )
UserSchema . index ( { ' simulatedBy ' : 1 } )
UserSchema . index ( { ' slug ' : 1 } , { name: ' slug index ' , sparse: true , unique: true } )
UserSchema . index ( { ' stripe.subscriptionID ' : 1 } , { unique: true , sparse: true } )
UserSchema . index ( { ' siteref ' : 1 } , { name: ' siteref index ' , sparse: true } )
UserSchema . index ( { ' schoolName ' : 1 } , { name: ' schoolName index ' , sparse: true } )
UserSchema . index ( { ' country ' : 1 } , { name: ' country index ' , sparse: true } )
UserSchema . index ( { ' role ' : 1 } , { name: ' role index ' , sparse: true } )
2016-05-09 18:16:54 -04:00
UserSchema . index ( { ' coursePrepaid._id ' : 1 } , { name: ' course prepaid id index ' , sparse: true } )
2016-04-06 13:56:06 -04:00
UserSchema . post ( ' init ' , ->
@ set ( ' anonymous ' , false ) if @ get ( ' email ' )
)
2016-05-11 17:39:26 -04:00
UserSchema.methods.broadName = ->
return ' (deleted) ' if @ get ( ' deleted ' )
name = @ get ( ' name ' )
return name if name
name = _ . filter ( [ @ get ( ' firstName ' ) , @ get ( ' lastName ' ) ] ) . join ( ' ' )
return name if name
[ emailName , emailDomain ] = @ get ( ' email ' ) . split ( ' @ ' )
return emailName if emailName
2016-05-26 19:46:03 -04:00
return ' Anonymous '
2016-05-11 17:39:26 -04:00
2016-04-06 13:56:06 -04:00
UserSchema.methods.isInGodMode = ->
p = @ get ( ' permissions ' )
return p and ' godmode ' in p
UserSchema.methods.isAdmin = ->
p = @ get ( ' permissions ' )
return p and ' admin ' in p
UserSchema.methods.hasPermission = (neededPermissions) ->
permissions = @ get ( ' permissions ' ) or [ ]
if _ . contains ( permissions , ' admin ' )
return true
if _ . isString ( neededPermissions )
neededPermissions = [ neededPermissions ]
return _ . size ( _ . intersection ( permissions , neededPermissions ) )
UserSchema.methods.isArtisan = ->
p = @ get ( ' permissions ' )
return p and ' artisan ' in p
UserSchema.methods.isAnonymous = ->
@ get ' anonymous '
UserSchema.statics.teacherRoles = [ ' teacher ' , ' technology coordinator ' , ' advisor ' , ' principal ' , ' superintendent ' , ' parent ' ]
UserSchema.methods.isTeacher = ->
return @ get ( ' role ' ) in User . teacherRoles
2016-05-11 17:39:26 -04:00
UserSchema.methods.isStudent = ->
return @ get ( ' role ' ) is ' student '
2016-04-06 13:56:06 -04:00
UserSchema.methods.getUserInfo = ->
id: @ get ( ' _id ' )
email: if @ get ( ' anonymous ' ) then ' Unregistered User ' else @ get ( ' email ' )
2016-05-16 17:33:20 -04:00
UserSchema.methods.removeFromClassrooms = ->
userID = @ get ( ' _id ' )
yield Classroom . update (
{ members: userID }
{
$addToSet: { deletedMembers: userID }
$pull: { members: userID }
}
{ multi: true }
)
2016-04-06 13:56:06 -04:00
UserSchema.methods.trackActivity = (activityName, increment) ->
now = new Date ( )
increment ? = parseInt increment or 1
increment = Math . max increment , 0
activity = @ get ( ' activity ' ) ? { }
activity [ activityName ] ? = { first: now , count: 0 }
activity [ activityName ] . count += increment
activity [ activityName ] . last = now
@ set ' activity ' , activity
activity
2016-05-09 18:16:54 -04:00
UserSchema.statics.search = (term, done) ->
utils = require ' ../lib/utils '
if utils . isID ( term )
query = { _id: mongoose . Types . ObjectId ( term ) }
else
term = term . toLowerCase ( )
query = $or: [ { nameLower: term } , { emailLower: term } ]
return User . findOne ( query ) . exec ( done )
2016-06-30 18:32:58 -04:00
UserSchema.statics.findByEmail = (email, done=_.noop) ->
emailLower = email . toLowerCase ( )
User . findOne ( { emailLower: emailLower } ) . exec ( done )
2016-04-06 13:56:06 -04:00
emailNameMap =
generalNews: ' announcement '
adventurerNews: ' tester '
artisanNews: ' level_creator '
archmageNews: ' developer '
scribeNews: ' article_editor '
diplomatNews: ' translator '
ambassadorNews: ' support '
anyNotes: ' notification '
teacherNews: ' teacher '
UserSchema.methods.setEmailSubscription = (newName, enabled) ->
oldSubs = _ . clone @ get ( ' emailSubscriptions ' )
if oldSubs and oldName = emailNameMap [ newName ]
oldSubs = ( s for s in oldSubs when s isnt oldName )
oldSubs . push ( oldName ) if enabled
@ set ( ' emailSubscriptions ' , oldSubs )
newSubs = _ . clone ( @ get ( ' emails ' ) or _ . cloneDeep ( jsonschema . properties . emails . default ) )
newSubs [ newName ] ? = { }
newSubs [ newName ] . enabled = enabled
@ set ( ' emails ' , newSubs )
@newsSubsChanged = true if newName in mail . NEWS_GROUPS
UserSchema.methods.gems = ->
gemsEarned = @ get ( ' earned ' ) ? . gems ? 0
gemsEarned = gemsEarned + 100000 if @ isInGodMode ( )
gemsPurchased = @ get ( ' purchased ' ) ? . gems ? 0
gemsSpent = @ get ( ' spent ' ) ? 0
gemsEarned + gemsPurchased - gemsSpent
UserSchema.methods.isEmailSubscriptionEnabled = (newName) ->
emails = @ get ' emails '
if not emails
oldSubs = @ get ( ' emailSubscriptions ' )
oldName = emailNameMap [ newName ]
return oldName and oldName in oldSubs if oldSubs
emails ? = { }
_ . defaults emails , _ . cloneDeep ( jsonschema . properties . emails . default )
return emails [ newName ] ? . enabled
UserSchema.statics.updateServiceSettings = (doc, callback) ->
return callback ? ( ) unless isProduction or GLOBAL . testing
return callback ? ( ) if doc . updatedMailChimp
return callback ? ( ) unless doc . get ( ' email ' )
2016-05-31 15:30:36 -04:00
return callback ? ( ) unless doc . get ( ' dateCreated ' )
accountAgeMinutes = ( new Date ( ) . getTime ( ) - doc . get ( ' dateCreated ' ) . getTime ? ( ) ? 0 ) / 1000 / 60
return callback ? ( ) unless accountAgeMinutes > 30 or GLOBAL . testing
2016-04-06 13:56:06 -04:00
existingProps = doc . get ( ' mailChimp ' )
emailChanged = ( not existingProps ) or existingProps ? . email isnt doc . get ( ' email ' )
if emailChanged and customerID = doc . get ( ' stripe ' ) ? . customerID
unless stripe ? . customers
console . error ( ' Oh my god, Stripe is not imported correctly-how could we have done this (again)? ' )
stripe ? . customers ? . update customerID , { email : doc . get ( ' email ' ) } , (err, customer) ->
console . error ( ' Error updating stripe customer... ' , err ) if err
return callback ? ( ) unless emailChanged or doc . newsSubsChanged
newGroups = [ ]
for [ mailchimpEmailGroup , emailGroup ] in _ . zip ( mail . MAILCHIMP_GROUPS , mail . NEWS_GROUPS )
newGroups . push ( mailchimpEmailGroup ) if doc . isEmailSubscriptionEnabled ( emailGroup )
if ( not existingProps ) and newGroups . length is 0
return callback ? ( ) # don't add totally unsubscribed people to the list
params = { }
params.id = mail . MAILCHIMP_LIST_ID
params.email = if existingProps then { leid: existingProps . leid } else { email: doc . get ( ' email ' ) }
params.merge_vars = {
groupings: [ { id: mail . MAILCHIMP_GROUP_ID , groups: newGroups } ]
' new-email ' : doc . get ( ' email ' )
}
params.update_existing = true
onSuccess = (data) ->
data.email = doc . get ( ' email ' ) # Make sure that we don't spam opt-in emails even if MailChimp doesn't update the email it gets in this object until they have confirmed.
doc . set ( ' mailChimp ' , data )
doc.updatedMailChimp = true
doc . save ( )
callback ? ( )
onFailure = (error) ->
log . error ' failed to subscribe ' , error , callback ?
doc.updatedMailChimp = true
callback ? ( )
mc ? . lists . subscribe params , onSuccess , onFailure
UserSchema.statics.statsMapping =
edits:
article: ' stats.articleEdits '
level: ' stats.levelEdits '
' level.component ' : ' stats.levelComponentEdits '
' level.system ' : ' stats.levelSystemEdits '
' thang.type ' : ' stats.thangTypeEdits '
' Achievement ' : ' stats.achievementEdits '
' campaign ' : ' stats.campaignEdits '
' poll ' : ' stats.pollEdits '
translations:
article: ' stats.articleTranslationPatches '
level: ' stats.levelTranslationPatches '
' level.component ' : ' stats.levelComponentTranslationPatches '
' level.system ' : ' stats.levelSystemTranslationPatches '
' thang.type ' : ' stats.thangTypeTranslationPatches '
' Achievement ' : ' stats.achievementTranslationPatches '
' campaign ' : ' stats.campaignTranslationPatches '
' poll ' : ' stats.pollTranslationPatches '
misc:
article: ' stats.articleMiscPatches '
level: ' stats.levelMiscPatches '
' level.component ' : ' stats.levelComponentMiscPatches '
' level.system ' : ' stats.levelSystemMiscPatches '
' thang.type ' : ' stats.thangTypeMiscPatches '
' Achievement ' : ' stats.achievementMiscPatches '
' campaign ' : ' stats.campaignMiscPatches '
' poll ' : ' stats.pollMiscPatches '
UserSchema.statics.incrementStat = (id, statName, done, inc=1) ->
id = mongoose . Types . ObjectId id if _ . isString id
@ findById id , (err, user) ->
log . error err if err ?
err = new Error " Could ' t find user with id ' #{ id } ' " unless user or err
return done ( ) if err ?
user . incrementStat statName , done , inc
UserSchema.methods.incrementStat = (statName, done, inc=1) ->
if /^concepts\./ . test statName
# Concept stats are nested a level deeper.
concepts = @ get ( ' concepts ' ) or { }
concept = statName . split ( ' . ' ) [ 1 ]
concepts [ concept ] = ( concepts [ concept ] or 0 ) + inc
@ set ' concepts ' , concepts
else
@ set statName , ( @ get ( statName ) or 0 ) + inc
@ save (err) -> done ? ( err )
UserSchema.statics.unconflictName = unconflictName = (name, done) ->
User . findOne { slug: _ . str . slugify ( name ) } , (err, otherUser) ->
return done err if err ?
return done null , name unless otherUser
suffix = _ . random ( 0 , 9 ) + ' '
unconflictName name + suffix , done
2016-06-30 18:32:58 -04:00
UserSchema.methods.sendWelcomeEmail = ->
2016-05-11 17:39:26 -04:00
{ welcome_email_student , welcome_email_user } = sendwithus . templates
timestamp = ( new Date ) . getTime ( )
data =
2016-06-06 19:53:05 -04:00
email_id: if @ isStudent ( ) then welcome_email_student else welcome_email_user
2016-05-11 17:39:26 -04:00
recipient:
address: @ get ( ' email ' )
name: @ broadName ( )
email_data:
name: @ broadName ( )
verify_link: " http://codecombat.com/user/ #{ @ _id } /verify/ #{ @ verificationCode ( timestamp ) } "
sendwithus . api . send data , (err, result) ->
log . error " sendwithus post-save error: #{ err } , result: #{ result } " if err
2016-04-06 13:56:06 -04:00
UserSchema.methods.hasSubscription = ->
return false unless stripeObject = @ get ( ' stripe ' )
return true if stripeObject . sponsorID
return true if stripeObject . subscriptionID
return true if stripeObject . free is true
return true if _ . isString ( stripeObject . free ) and new Date ( ) < new Date ( stripeObject . free )
UserSchema.methods.isPremium = ->
return true if @ isInGodMode ( )
return true if @ isAdmin ( )
return true if @ hasSubscription ( )
return false
UserSchema.methods.isOnPremiumServer = ->
@ get ( ' country ' ) in [ ' china ' , ' brazil ' ]
UserSchema.methods.level = ->
xp = @ get ( ' points ' ) or 0
a = 5
b = c = 100
if xp > 0 then Math . floor ( a * Math . log ( ( 1 / b ) * ( xp + c ) ) ) + 1 else 1
2016-05-09 18:16:54 -04:00
UserSchema.methods.isEnrolled = ->
coursePrepaid = @ get ( ' coursePrepaid ' )
return false unless coursePrepaid
return true unless coursePrepaid . endDate
return coursePrepaid . endDate > new Date ( ) . toISOString ( )
2016-04-06 13:56:06 -04:00
UserSchema.statics.saveActiveUser = (id, event, done=null) ->
# TODO: Disabling this until we know why our app servers CPU grows out of control.
return done ? ( )
id = mongoose . Types . ObjectId id if _ . isString id
@ findById id , (err, user) ->
if err ?
log . error err
else
user ? . saveActiveUser event
done ? ( )
UserSchema.methods.saveActiveUser = (event, done=null) ->
# TODO: Disabling this until we know why our app servers CPU grows out of control.
return done ? ( )
try
return done ? ( ) if @ isAdmin ( )
userID = @ get ( ' _id ' )
# Create if no active user entry for today
today = new Date ( )
minDate = new Date ( Date . UTC ( today . getUTCFullYear ( ) , today . getUTCMonth ( ) , today . getUTCDate ( ) ) )
AnalyticsUsersActive . findOne ( { created: { $gte: minDate } , creator: mongoose . Types . ObjectId ( userID ) } ) . exec (err, activeUser) ->
if err ?
log . error " saveActiveUser error retrieving active users: #{ err } "
else if not activeUser
newActiveUser = new AnalyticsUsersActive ( )
newActiveUser . set ' creator ' , userID
newActiveUser . set ' event ' , event
newActiveUser . save (err) ->
log . error " Level session saveActiveUser error saving active user: #{ err } " if err ?
done ? ( )
catch err
log . error err
done ? ( )
UserSchema . pre ( ' save ' , (next) ->
2016-06-20 19:44:07 -04:00
if _ . isNaN ( @ get ( ' purchased ' ) ? . gems )
return next ( new errors . InternalServerError ( ' Attempting to save NaN to user ' ) )
2016-04-06 13:56:06 -04:00
Classroom = require ' ./Classroom '
if @ isTeacher ( ) and not @ wasTeacher
Classroom . update ( { members: @ _id } , { $pull: { members: @ _id } } , { multi: true } ) . exec (err, res) ->
if email = @ get ( ' email ' )
@ set ( ' emailLower ' , email . toLowerCase ( ) )
if name = @ get ( ' name ' )
@ set ( ' nameLower ' , name . toLowerCase ( ) )
pwd = @ get ( ' password ' )
if @ get ( ' password ' )
@ set ( ' passwordHash ' , User . hashPassword ( pwd ) )
@ set ( ' password ' , undefined )
2016-06-30 18:32:58 -04:00
next ( )
2016-04-06 13:56:06 -04:00
)
UserSchema . post ' save ' , (doc) ->
doc.newsSubsChanged = not _ . isEqual ( _ . pick ( doc . get ( ' emails ' ) , mail . NEWS_GROUPS ) , _ . pick ( doc . startingEmails , mail . NEWS_GROUPS ) )
UserSchema . statics . updateServiceSettings ( doc )
2016-05-09 18:16:54 -04:00
2016-04-06 13:56:06 -04:00
UserSchema . post ' init ' , (doc) ->
doc.wasTeacher = doc . isTeacher ( )
doc.startingEmails = _ . cloneDeep ( doc . get ( ' emails ' ) )
2016-05-09 18:16:54 -04:00
if @ get ( ' coursePrepaidID ' ) and not @ get ( ' coursePrepaid ' )
Prepaid = require ' ./Prepaid '
@ set ( ' coursePrepaid ' , {
_id: @ get ( ' coursePrepaidID ' )
startDate: Prepaid . DEFAULT_START_DATE
endDate: Prepaid . DEFAULT_END_DATE
} )
@ set ( ' coursePrepaidID ' , undefined )
2016-04-06 13:56:06 -04:00
UserSchema.statics.hashPassword = (password) ->
password = password . toLowerCase ( )
shasum = crypto . createHash ( ' sha512 ' )
shasum . update ( salt + password )
shasum . digest ( ' hex ' )
2016-05-11 17:39:26 -04:00
UserSchema.methods.verificationCode = (timestamp) ->
{ _id , email } = this . toObject ( )
shasum = crypto . createHash ( ' sha256 ' )
hash = shasum . update ( timestamp + salt + _id + email ) . digest ( ' hex ' )
return " #{ timestamp } : #{ hash } "
2016-04-06 13:56:06 -04:00
UserSchema.statics.privateProperties = [
' permissions ' , ' email ' , ' mailChimp ' , ' firstName ' , ' lastName ' , ' gender ' , ' facebookID ' ,
' gplusID ' , ' music ' , ' volume ' , ' aceConfig ' , ' employerAt ' , ' signedEmployerAgreement ' ,
' emailSubscriptions ' , ' emails ' , ' activity ' , ' stripe ' , ' stripeCustomerID ' , ' chinaVersion ' , ' country ' ,
2016-05-09 18:16:54 -04:00
' schoolName ' , ' ageRange ' , ' role ' , ' enrollmentRequestSent '
2016-04-06 13:56:06 -04:00
]
UserSchema.statics.jsonSchema = jsonschema
UserSchema.statics.editableProperties = [
' name ' , ' photoURL ' , ' password ' , ' anonymous ' , ' wizardColor1 ' , ' volume ' ,
' firstName ' , ' lastName ' , ' gender ' , ' ageRange ' , ' facebookID ' , ' gplusID ' , ' emails ' ,
' testGroupNumber ' , ' music ' , ' hourOfCode ' , ' hourOfCodeComplete ' , ' preferredLanguage ' ,
' wizard ' , ' aceConfig ' , ' autocastDelay ' , ' lastLevel ' , ' jobProfile ' , ' savedEmployerFilterAlerts ' ,
2016-05-09 18:16:54 -04:00
' heroConfig ' , ' iosIdentifierForVendor ' , ' siteref ' , ' referrer ' , ' schoolName ' , ' role ' , ' birthday ' ,
' enrollmentRequestSent '
2016-04-06 13:56:06 -04:00
]
UserSchema.statics.serverProperties = [ ' passwordHash ' , ' emailLower ' , ' nameLower ' , ' passwordReset ' , ' lastIP ' ]
UserSchema.statics.candidateProperties = [ ' jobProfile ' , ' jobProfileApproved ' , ' jobProfileNotes ' ]
UserSchema . set ( ' toObject ' , {
transform: (doc, ret, options) ->
req = options . req
return ret unless req # TODO: Make deleting properties the default, but the consequences are far reaching
publicOnly = options . publicOnly
delete ret [ prop ] for prop in User . serverProperties
includePrivates = not publicOnly and ( req . user and ( req . user . isAdmin ( ) or req . user . _id . equals ( doc . _id ) or req . session . amActually is doc . id ) )
if options . includedPrivates
excludedPrivates = _ . reject User . privateProperties , (prop) ->
prop in options . includedPrivates
else
excludedPrivates = User . privateProperties
delete ret [ prop ] for prop in excludedPrivates unless includePrivates
delete ret [ prop ] for prop in User . candidateProperties
return ret
} )
2016-04-11 19:51:51 -04:00
UserSchema.statics.makeNew = (req) ->
user = new User ( { anonymous: true } )
if global . testing
# allows tests some control over user id creation
newID = _ . pad ( ( User . idCounter ++ ) . toString ( 16 ) , 24 , ' 0 ' )
user . set ( ' _id ' , newID )
user . set ' testGroupNumber ' , Math . floor ( Math . random ( ) * 256 ) # also in app/core/auth
lang = languages . languageCodeFromAcceptedLanguages req . acceptedLanguages
user . set ' preferredLanguage ' , lang if lang [ . . . 2 ] isnt ' en '
user . set ' preferredLanguage ' , ' pt-BR ' if not user . get ( ' preferredLanguage ' ) and /br\.codecombat\.com/ . test ( req . get ( ' host ' ) )
user . set ' preferredLanguage ' , ' zh-HANS ' if not user . get ( ' preferredLanguage ' ) and /cn\.codecombat\.com/ . test ( req . get ( ' host ' ) )
user . set ' lastIP ' , ( req . headers [ ' x-forwarded-for ' ] or req . connection . remoteAddress ) ? . split ( /,? / ) [ 0 ]
user . set ' country ' , req . country if req . country
user
2016-04-06 13:56:06 -04:00
UserSchema . plugin plugins . NamedPlugin
module.exports = User = mongoose . model ( ' User ' , UserSchema )
2016-04-11 19:51:51 -04:00
User.idCounter = 0
2016-04-06 13:56:06 -04:00
AchievablePlugin = require ' ../plugins/achievements '
UserSchema . plugin ( AchievablePlugin )