codecombat/server/models/User.coffee

512 lines
19 KiB
CoffeeScript
Raw Normal View History

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'
Classroom = require '../models/Classroom'
languages = require '../routes/languages'
Improve student account recovery This adds the ability to verify email addresses of a user, so we know they have access to the email address on their account. Until a user has verified their email address, any teacher of a class they're in can reset their password for them via the Teacher Dashboard. When a user's email address is verified, a teacher may trigger a password recovery email to be sent to the student. Verification links are valid forever, until the user changes the email address they have on file. They are created using a timestamp, with a sha256 of timestamp+salt+userID+email. Currently the hash value is rather long, could be shorter. Squashed commit messages: Add server endpoints for verifying email address Add server endpoints for verifying email address (pt 2) Add Server+Client endpoint for sending verification email Add client view for verification links Add Edit Student Modal for resetting passwords Add specs for EditStudentModal Tweak method name in EditStudentModal Add edit student button to TeacherClassView Fix up frontend for teacher password resetting Add middleware for teacher password resetting Improve button UX in EditStudentModal Add JoinClassModal Add welcome emails, use broad name Use email without domain as fallback instead of full email Fetch user on edit student modal open Don't allow password reset if student email is verified Set role to student on user signup with classCode Tweak interface for joinClassModal Add button to request verification email for yourself Fix verify email template ID Move text to en.coffee Minor tweaks Fix code review comments Fix some tests, disable a broken one Fix misc tests Fix more tests Refactor recovery email sending to auth Fix overbroad sass Add options to refactored recovery email function Rename getByCode to fetchByCode Fix error message Fix up error handling in users middleware Use .get instead of .toObject Use findById Fix more code review comments Disable still-broken test
2016-05-11 17:39:26 -04:00
_ = require 'lodash'
errors = require '../commons/errors'
Promise = require 'bluebird'
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})
2016-09-13 18:18:19 -04:00
UserSchema.index({'cleverID': 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})
UserSchema.index({'coursePrepaid._id': 1}, {name: 'course prepaid id index', sparse: true})
UserSchema.post('init', ->
@set('anonymous', false) if @get('email')
)
Improve student account recovery This adds the ability to verify email addresses of a user, so we know they have access to the email address on their account. Until a user has verified their email address, any teacher of a class they're in can reset their password for them via the Teacher Dashboard. When a user's email address is verified, a teacher may trigger a password recovery email to be sent to the student. Verification links are valid forever, until the user changes the email address they have on file. They are created using a timestamp, with a sha256 of timestamp+salt+userID+email. Currently the hash value is rather long, could be shorter. Squashed commit messages: Add server endpoints for verifying email address Add server endpoints for verifying email address (pt 2) Add Server+Client endpoint for sending verification email Add client view for verification links Add Edit Student Modal for resetting passwords Add specs for EditStudentModal Tweak method name in EditStudentModal Add edit student button to TeacherClassView Fix up frontend for teacher password resetting Add middleware for teacher password resetting Improve button UX in EditStudentModal Add JoinClassModal Add welcome emails, use broad name Use email without domain as fallback instead of full email Fetch user on edit student modal open Don't allow password reset if student email is verified Set role to student on user signup with classCode Tweak interface for joinClassModal Add button to request verification email for yourself Fix verify email template ID Move text to en.coffee Minor tweaks Fix code review comments Fix some tests, disable a broken one Fix misc tests Fix more tests Refactor recovery email sending to auth Fix overbroad sass Add options to refactored recovery email function Rename getByCode to fetchByCode Fix error message Fix up error handling in users middleware Use .get instead of .toObject Use findById Fix more code review comments Disable still-broken test
2016-05-11 17:39:26 -04:00
UserSchema.methods.broadName = ->
return '(deleted)' if @get('deleted')
name = _.filter([@get('firstName'), @get('lastName')]).join(' ')
return name if name
name = @get('name')
return name if name
Improve student account recovery This adds the ability to verify email addresses of a user, so we know they have access to the email address on their account. Until a user has verified their email address, any teacher of a class they're in can reset their password for them via the Teacher Dashboard. When a user's email address is verified, a teacher may trigger a password recovery email to be sent to the student. Verification links are valid forever, until the user changes the email address they have on file. They are created using a timestamp, with a sha256 of timestamp+salt+userID+email. Currently the hash value is rather long, could be shorter. Squashed commit messages: Add server endpoints for verifying email address Add server endpoints for verifying email address (pt 2) Add Server+Client endpoint for sending verification email Add client view for verification links Add Edit Student Modal for resetting passwords Add specs for EditStudentModal Tweak method name in EditStudentModal Add edit student button to TeacherClassView Fix up frontend for teacher password resetting Add middleware for teacher password resetting Improve button UX in EditStudentModal Add JoinClassModal Add welcome emails, use broad name Use email without domain as fallback instead of full email Fetch user on edit student modal open Don't allow password reset if student email is verified Set role to student on user signup with classCode Tweak interface for joinClassModal Add button to request verification email for yourself Fix verify email template ID Move text to en.coffee Minor tweaks Fix code review comments Fix some tests, disable a broken one Fix misc tests Fix more tests Refactor recovery email sending to auth Fix overbroad sass Add options to refactored recovery email function Rename getByCode to fetchByCode Fix error message Fix up error handling in users middleware Use .get instead of .toObject Use findById Fix more code review comments Disable still-broken test
2016-05-11 17:39:26 -04:00
[emailName, emailDomain] = @get('email').split('@')
return emailName if emailName
return 'Anonymous'
Improve student account recovery This adds the ability to verify email addresses of a user, so we know they have access to the email address on their account. Until a user has verified their email address, any teacher of a class they're in can reset their password for them via the Teacher Dashboard. When a user's email address is verified, a teacher may trigger a password recovery email to be sent to the student. Verification links are valid forever, until the user changes the email address they have on file. They are created using a timestamp, with a sha256 of timestamp+salt+userID+email. Currently the hash value is rather long, could be shorter. Squashed commit messages: Add server endpoints for verifying email address Add server endpoints for verifying email address (pt 2) Add Server+Client endpoint for sending verification email Add client view for verification links Add Edit Student Modal for resetting passwords Add specs for EditStudentModal Tweak method name in EditStudentModal Add edit student button to TeacherClassView Fix up frontend for teacher password resetting Add middleware for teacher password resetting Improve button UX in EditStudentModal Add JoinClassModal Add welcome emails, use broad name Use email without domain as fallback instead of full email Fetch user on edit student modal open Don't allow password reset if student email is verified Set role to student on user signup with classCode Tweak interface for joinClassModal Add button to request verification email for yourself Fix verify email template ID Move text to en.coffee Minor tweaks Fix code review comments Fix some tests, disable a broken one Fix misc tests Fix more tests Refactor recovery email sending to auth Fix overbroad sass Add options to refactored recovery email function Rename getByCode to fetchByCode Fix error message Fix up error handling in users middleware Use .get instead of .toObject Use findById Fix more code review comments Disable still-broken test
2016-05-11 17:39:26 -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
Improve student account recovery This adds the ability to verify email addresses of a user, so we know they have access to the email address on their account. Until a user has verified their email address, any teacher of a class they're in can reset their password for them via the Teacher Dashboard. When a user's email address is verified, a teacher may trigger a password recovery email to be sent to the student. Verification links are valid forever, until the user changes the email address they have on file. They are created using a timestamp, with a sha256 of timestamp+salt+userID+email. Currently the hash value is rather long, could be shorter. Squashed commit messages: Add server endpoints for verifying email address Add server endpoints for verifying email address (pt 2) Add Server+Client endpoint for sending verification email Add client view for verification links Add Edit Student Modal for resetting passwords Add specs for EditStudentModal Tweak method name in EditStudentModal Add edit student button to TeacherClassView Fix up frontend for teacher password resetting Add middleware for teacher password resetting Improve button UX in EditStudentModal Add JoinClassModal Add welcome emails, use broad name Use email without domain as fallback instead of full email Fetch user on edit student modal open Don't allow password reset if student email is verified Set role to student on user signup with classCode Tweak interface for joinClassModal Add button to request verification email for yourself Fix verify email template ID Move text to en.coffee Minor tweaks Fix code review comments Fix some tests, disable a broken one Fix misc tests Fix more tests Refactor recovery email sending to auth Fix overbroad sass Add options to refactored recovery email function Rename getByCode to fetchByCode Fix error message Fix up error handling in users middleware Use .get instead of .toObject Use findById Fix more code review comments Disable still-broken test
2016-05-11 17:39:26 -04:00
UserSchema.methods.isStudent = ->
return @get('role') is 'student'
UserSchema.methods.getUserInfo = ->
id: @get('_id')
email: if @get('anonymous') then 'Unregistered User' else @get('email')
UserSchema.methods.removeFromClassrooms = ->
userID = @get('_id')
yield Classroom.update(
{ members: userID }
{
$addToSet: { deletedMembers: userID }
$pull: { members: userID }
}
{ multi: true }
)
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
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)
UserSchema.statics.findByName = (name, done=_.noop) ->
nameLower = name.toLowerCase()
User.findOne({nameLower: nameLower}).exec(done)
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')
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
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_component': 'stats.levelComponentEdits'
'level.system': 'stats.levelSystemEdits'
'level_system': 'stats.levelSystemEdits'
'thang.type': 'stats.thangTypeEdits'
'thang_type': 'stats.thangTypeEdits'
'Achievement': 'stats.achievementEdits'
'achievement': 'stats.achievementEdits'
'campaign': 'stats.campaignEdits'
'poll': 'stats.pollEdits'
'course': 'stats.courseEdits'
translations:
article: 'stats.articleTranslationPatches'
level: 'stats.levelTranslationPatches'
'level.component': 'stats.levelComponentTranslationPatches'
'level_component': 'stats.levelComponentTranslationPatches'
'level.system': 'stats.levelSystemTranslationPatches'
'level_system': 'stats.levelSystemTranslationPatches'
'thang.type': 'stats.thangTypeTranslationPatches'
'thang_type': 'stats.thangTypeTranslationPatches'
'Achievement': 'stats.achievementTranslationPatches'
'achievement': 'stats.achievementTranslationPatches'
'campaign': 'stats.campaignTranslationPatches'
'poll': 'stats.pollTranslationPatches'
'course': 'stats.courseTranslationPatches'
misc:
article: 'stats.articleMiscPatches'
level: 'stats.levelMiscPatches'
'level.component': 'stats.levelComponentMiscPatches'
'level_component': 'stats.levelComponentMiscPatches'
'level.system': 'stats.levelSystemMiscPatches'
'level_system': 'stats.levelSystemMiscPatches'
'thang.type': 'stats.thangTypeMiscPatches'
'thang_type': 'stats.thangTypeMiscPatches'
'Achievement': 'stats.achievementMiscPatches'
'achievement': 'stats.achievementMiscPatches'
'campaign': 'stats.campaignMiscPatches'
'poll': 'stats.pollMiscPatches'
'course': 'stats.courseMiscPatches'
# TODO: Migrate from incrementStat to incrementStatAsync
UserSchema.statics.incrementStatAsync = Promise.promisify (id, statName, options={}, done) ->
# A shim over @incrementStat, providing a Promise interface
if _.isFunction(options)
done = options
options = {}
@incrementStat(id, statName, done, options.inc or 1)
UserSchema.statics.incrementStat = (id, statName, done=_.noop, inc=1) ->
_id = if _.isString(id) then mongoose.Types.ObjectId(id) else id
update = {$inc: {}}
update.$inc[statName] = inc
@update({_id}, update).exec((err, res) ->
if not res.nModified
log.warn "Did not update user stat '#{statName}' for '#{id}'"
done?()
)
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
UserSchema.statics.unconflictNameAsync = Promise.promisify(unconflictName)
2016-06-30 18:32:58 -04:00
UserSchema.methods.sendWelcomeEmail = ->
return if not @get('email')
Improve student account recovery This adds the ability to verify email addresses of a user, so we know they have access to the email address on their account. Until a user has verified their email address, any teacher of a class they're in can reset their password for them via the Teacher Dashboard. When a user's email address is verified, a teacher may trigger a password recovery email to be sent to the student. Verification links are valid forever, until the user changes the email address they have on file. They are created using a timestamp, with a sha256 of timestamp+salt+userID+email. Currently the hash value is rather long, could be shorter. Squashed commit messages: Add server endpoints for verifying email address Add server endpoints for verifying email address (pt 2) Add Server+Client endpoint for sending verification email Add client view for verification links Add Edit Student Modal for resetting passwords Add specs for EditStudentModal Tweak method name in EditStudentModal Add edit student button to TeacherClassView Fix up frontend for teacher password resetting Add middleware for teacher password resetting Improve button UX in EditStudentModal Add JoinClassModal Add welcome emails, use broad name Use email without domain as fallback instead of full email Fetch user on edit student modal open Don't allow password reset if student email is verified Set role to student on user signup with classCode Tweak interface for joinClassModal Add button to request verification email for yourself Fix verify email template ID Move text to en.coffee Minor tweaks Fix code review comments Fix some tests, disable a broken one Fix misc tests Fix more tests Refactor recovery email sending to auth Fix overbroad sass Add options to refactored recovery email function Rename getByCode to fetchByCode Fix error message Fix up error handling in users middleware Use .get instead of .toObject Use findById Fix more code review comments Disable still-broken test
2016-05-11 17:39:26 -04:00
{ welcome_email_student, welcome_email_user } = sendwithus.templates
timestamp = (new Date).getTime()
data =
email_id: if @isStudent() then welcome_email_student else welcome_email_user
Improve student account recovery This adds the ability to verify email addresses of a user, so we know they have access to the email address on their account. Until a user has verified their email address, any teacher of a class they're in can reset their password for them via the Teacher Dashboard. When a user's email address is verified, a teacher may trigger a password recovery email to be sent to the student. Verification links are valid forever, until the user changes the email address they have on file. They are created using a timestamp, with a sha256 of timestamp+salt+userID+email. Currently the hash value is rather long, could be shorter. Squashed commit messages: Add server endpoints for verifying email address Add server endpoints for verifying email address (pt 2) Add Server+Client endpoint for sending verification email Add client view for verification links Add Edit Student Modal for resetting passwords Add specs for EditStudentModal Tweak method name in EditStudentModal Add edit student button to TeacherClassView Fix up frontend for teacher password resetting Add middleware for teacher password resetting Improve button UX in EditStudentModal Add JoinClassModal Add welcome emails, use broad name Use email without domain as fallback instead of full email Fetch user on edit student modal open Don't allow password reset if student email is verified Set role to student on user signup with classCode Tweak interface for joinClassModal Add button to request verification email for yourself Fix verify email template ID Move text to en.coffee Minor tweaks Fix code review comments Fix some tests, disable a broken one Fix misc tests Fix more tests Refactor recovery email sending to auth Fix overbroad sass Add options to refactored recovery email function Rename getByCode to fetchByCode Fix error message Fix up error handling in users middleware Use .get instead of .toObject Use findById Fix more code review comments Disable still-broken test
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
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 = ->
return true if @get('country') in ['brazil']
return true if @get('country') in ['china'] and @isPremium()
return false
UserSchema.methods.isOnFreeOnlyServer = ->
return true if @get('country') in ['china'] and not @isPremium()
return false
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
UserSchema.methods.isEnrolled = ->
coursePrepaid = @get('coursePrepaid')
return false unless coursePrepaid
return true unless coursePrepaid.endDate
return coursePrepaid.endDate > new Date().toISOString()
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) ->
if _.isNaN(@get('purchased')?.gems)
return next(new errors.InternalServerError('Attempting to save NaN to user'))
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())
else
@set('email', undefined)
@set('emailLower', undefined)
if name = @get('name')
if not @allowEmailNames # for testing
filter = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,63}$/i # https://news.ycombinator.com/item?id=5763990
if filter.test(name)
return next(new errors.UnprocessableEntity('Username may not be an email'))
@set('nameLower', name.toLowerCase())
else
@set('name', undefined)
@set('nameLower', undefined)
if _.isEmpty(@get('firstName'))
@set('firstName', undefined)
if _.isEmpty(@get('lastName'))
@set('lastName', undefined)
unless email or name or @get('anonymous') or @get('deleted')
return next(new errors.UnprocessableEntity('User needs a username or email address'))
pwd = @get('password')
if @get('password')
@set('passwordHash', User.hashPassword(pwd))
@set('password', undefined)
2016-06-30 18:32:58 -04:00
next()
)
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)
UserSchema.post 'init', (doc) ->
doc.wasTeacher = doc.isTeacher()
doc.startingEmails = _.cloneDeep(doc.get('emails'))
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)
UserSchema.statics.hashPassword = (password) ->
password = password.toLowerCase()
shasum = crypto.createHash('sha512')
shasum.update(salt + password)
shasum.digest('hex')
Improve student account recovery This adds the ability to verify email addresses of a user, so we know they have access to the email address on their account. Until a user has verified their email address, any teacher of a class they're in can reset their password for them via the Teacher Dashboard. When a user's email address is verified, a teacher may trigger a password recovery email to be sent to the student. Verification links are valid forever, until the user changes the email address they have on file. They are created using a timestamp, with a sha256 of timestamp+salt+userID+email. Currently the hash value is rather long, could be shorter. Squashed commit messages: Add server endpoints for verifying email address Add server endpoints for verifying email address (pt 2) Add Server+Client endpoint for sending verification email Add client view for verification links Add Edit Student Modal for resetting passwords Add specs for EditStudentModal Tweak method name in EditStudentModal Add edit student button to TeacherClassView Fix up frontend for teacher password resetting Add middleware for teacher password resetting Improve button UX in EditStudentModal Add JoinClassModal Add welcome emails, use broad name Use email without domain as fallback instead of full email Fetch user on edit student modal open Don't allow password reset if student email is verified Set role to student on user signup with classCode Tweak interface for joinClassModal Add button to request verification email for yourself Fix verify email template ID Move text to en.coffee Minor tweaks Fix code review comments Fix some tests, disable a broken one Fix misc tests Fix more tests Refactor recovery email sending to auth Fix overbroad sass Add options to refactored recovery email function Rename getByCode to fetchByCode Fix error message Fix up error handling in users middleware Use .get instead of .toObject Use findById Fix more code review comments Disable still-broken test
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}"
UserSchema.statics.privateProperties = [
'permissions', 'email', 'mailChimp', 'firstName', 'lastName', 'gender', 'facebookID',
'gplusID', 'music', 'volume', 'aceConfig', 'employerAt', 'signedEmployerAgreement',
'emailSubscriptions', 'emails', 'activity', 'stripe', 'stripeCustomerID', 'chinaVersion', 'country',
'schoolName', 'ageRange', 'role', 'enrollmentRequestSent'
]
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',
'heroConfig', 'iosIdentifierForVendor', 'siteref', 'referrer', 'schoolName', 'role', 'birthday',
'enrollmentRequestSent'
]
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
})
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
UserSchema.plugin plugins.NamedPlugin
module.exports = User = mongoose.model('User', UserSchema)
User.idCounter = 0
AchievablePlugin = require '../plugins/achievements'
UserSchema.plugin(AchievablePlugin)