codecombat/server/middleware/users.coffee
phoenixeliot bb6262483f Allow username-only signup for classroom users
Address some code review feedback

Correct error code in test

Don't try to send emails to empty addresses

Add tests for subscriptions

Add tests for Next Steps email

Fix specs

Add reason for disabled test
2016-07-18 10:41:17 -07:00

207 lines
8.3 KiB
CoffeeScript

_ = require 'lodash'
co = require 'co'
countryList = require('country-list')()
errors = require '../commons/errors'
geoip = require 'geoip-lite'
wrap = require 'co-express'
Promise = require 'bluebird'
parse = require '../commons/parse'
request = require 'request'
mongoose = require 'mongoose'
sendwithus = require '../sendwithus'
User = require '../models/User'
Classroom = require '../models/Classroom'
facebook = require '../lib/facebook'
gplus = require '../lib/gplus'
TrialRequest = require '../models/TrialRequest'
log = require 'winston'
module.exports =
fetchByGPlusID: wrap (req, res, next) ->
gpID = req.query.gplusID
gpAT = req.query.gplusAccessToken
return next() unless gpID and gpAT
googleResponse = yield gplus.fetchMe(gpAT)
idsMatch = gpID is googleResponse.id
throw new errors.UnprocessableEntity('Invalid G+ Access Token.') unless idsMatch
dbq = User.find()
dbq.select(parse.getProjectFromReq(req))
user = yield User.findOne({gplusID: gpID})
throw new errors.NotFound('No user with that G+ ID') unless user
res.status(200).send(user.toObject({req: req}))
fetchByFacebookID: wrap (req, res, next) ->
fbID = req.query.facebookID
fbAT = req.query.facebookAccessToken
return next() unless fbID and fbAT
facebookResponse = yield facebook.fetchMe(fbAT)
idsMatch = fbID is facebookResponse.id
throw new errors.UnprocessableEntity('Invalid Facebook Access Token.') unless idsMatch
dbq = User.find()
dbq.select(parse.getProjectFromReq(req))
user = yield User.findOne({facebookID: fbID})
throw new errors.NotFound('No user with that Facebook ID') unless user
res.status(200).send(user.toObject({req: req}))
removeFromClassrooms: wrap (req, res, next) ->
yield req.user.removeFromClassrooms()
next()
remainTeacher: wrap (req, res, next) ->
yield req.user.removeFromClassrooms()
user = yield User.findById req.user.id
res.status(200).send(user.toObject({req: req}))
becomeStudent: wrap (req, res, next) ->
userID = mongoose.Types.ObjectId(req.user.id)
yield Classroom.remove({ ownerID: userID }, false)
userID = mongoose.Types.ObjectId(req.user.id)
yield User.update({ _id: userID }, { $set: { "role": "student" } })
user = yield User.findById req.user.id
res.status(200).send(user.toObject({req: req}))
verifyEmailAddress: wrap (req, res, next) ->
user = yield User.findOne({ _id: mongoose.Types.ObjectId(req.params.userID) })
[timestamp, hash] = req.params.verificationCode.split(':')
unless user
throw new errors.UnprocessableEntity('User not found')
unless req.params.verificationCode is user.verificationCode(timestamp)
throw new errors.UnprocessableEntity('Verification code does not match')
yield User.update({ _id: user.id }, { emailVerified: true })
res.status(200).send({ role: user.get('role') })
resetEmailVerifiedFlag: wrap (req, res, next) ->
newEmail = req.body.email
_id = mongoose.Types.ObjectId(req.body._id)
if newEmail
user = yield User.findOne({ _id })
oldEmail = user.get('email')
if newEmail isnt oldEmail
yield User.update({ _id }, { $set: { emailVerified: false } })
next()
sendVerificationEmail: wrap (req, res, next) ->
user = yield User.findById(req.params.userID)
timestamp = (new Date).getTime()
if not user
throw new errors.NotFound('User not found')
if not user.get('email')
throw new errors.UnprocessableEntity('User must have an email address to receive a verification email')
context =
email_id: sendwithus.templates.verify_email
recipient:
address: user.get('email')
name: user.broadName()
email_data:
name: user.broadName()
verify_link: "http://codecombat.com/user/#{user._id}/verify/#{user.verificationCode(timestamp)}"
sendwithus.api.send context, (err, result) ->
res.status(200).send({})
getStudents: wrap (req, res, next) ->
throw new errors.Unauthorized('You must be an administrator.') unless req.user?.isAdmin()
query = $or: [{role: 'student'}, {$and: [{schoolName: {$exists: true}}, {schoolName: {$ne: ''}}, {anonymous: false}]}]
users = yield User.find(query).select('lastIP schoolName').lean()
for user in users
if ip = user.lastIP
user.geo = geoip.lookup(ip)
if country = user.geo?.country
user.geo.countryName = countryList.getName(country)
res.status(200).send(users)
getTeachers: wrap (req, res, next) ->
throw new errors.Unauthorized('You must be an administrator.') unless req.user?.isAdmin()
teacherRoles = ['teacher', 'technology coordinator', 'advisor', 'principal', 'superintendent', 'parent']
users = yield User.find(anonymous: false, role: {$in: teacherRoles}).select('lastIP').lean()
for user in users
if ip = user.lastIP
user.geo = geoip.lookup(ip)
if country = user.geo?.country
user.geo.countryName = countryList.getName(country)
res.status(200).send(users)
signupWithPassword: wrap (req, res) ->
unless req.user.isAnonymous()
throw new errors.Forbidden('You are already signed in.')
{ name, email, password } = req.body
unless password
throw new errors.UnprocessableEntity('Requires password')
unless name or email
throw new errors.UnprocessableEntity('Requires username or email')
if not _.isEmpty(email) and yield User.findByEmail(email)
throw new errors.Conflict('Email already taken')
if not _.isEmpty(name) and yield User.findByName(name)
throw new errors.Conflict('Name already taken')
req.user.set({ name, email, password, anonymous: false })
yield module.exports.finishSignup(req, res)
signupWithFacebook: wrap (req, res) ->
unless req.user.isAnonymous()
throw new errors.Forbidden('You are already signed in.')
{ facebookID, facebookAccessToken, email } = req.body
unless _.all([facebookID, facebookAccessToken, email])
throw new errors.UnprocessableEntity('Requires facebookID, facebookAccessToken and email')
facebookResponse = yield facebook.fetchMe(facebookAccessToken)
emailsMatch = email is facebookResponse.email
idsMatch = facebookID is facebookResponse.id
unless emailsMatch and idsMatch
throw new errors.UnprocessableEntity('Invalid facebookAccessToken')
req.user.set({ facebookID, email, anonymous: false })
yield module.exports.finishSignup(req, res)
signupWithGPlus: wrap (req, res) ->
unless req.user.isAnonymous()
throw new errors.Forbidden('You are already signed in.')
{ gplusID, gplusAccessToken, email } = req.body
unless _.all([gplusID, gplusAccessToken, email])
throw new errors.UnprocessableEntity('Requires gplusID, gplusAccessToken and email')
gplusResponse = yield gplus.fetchMe(gplusAccessToken)
emailsMatch = email is gplusResponse.email
idsMatch = gplusID is gplusResponse.id
unless emailsMatch and idsMatch
throw new errors.UnprocessableEntity('Invalid gplusAccessToken')
req.user.set({ gplusID, email, anonymous: false })
yield module.exports.finishSignup(req, res)
finishSignup: co.wrap (req, res) ->
try
yield req.user.save()
catch e
if e.code is 11000 # Duplicate key error
throw new errors.Conflict('Email already taken')
else
throw e
# post-successful account signup tasks
req.user.sendWelcomeEmail()
# If person A creates a trial request without creating an account, then person B uses that computer
# to create an account, then person A's trial request is associated with person B's account. To prevent
# this, we check that the signup email matches the trial request email, for every signup. If they do
# not match, the trial request applicant field is cleared, disassociating the trial request from this
# account.
trialRequest = yield TrialRequest.findOne({applicant: req.user._id})
if trialRequest
email = trialRequest.get('properties')?.email or ''
emailLower = email.toLowerCase()
if emailLower and emailLower isnt req.user.get('emailLower')
log.warn('User submitted trial request and created account with different emails. Disassociating trial request.')
yield trialRequest.update({$unset: {applicant: ''}})
res.status(200).send(req.user.toObject({req: req}))