Refactor /auth endpoints for #3469

* Take `/server/routes/auth` and move most of the logic to `/server/middleware/auth`, refactoring to use generators.
* List all `/auth/*` endpoints in `/server/routes/index.coffee`.
* Fill in testing gaps for `/auth/unsubscribe`.
* Add debug log when `sendwithus` is not operational, so it 'works' in development and testing.
* Use passport properly!
* Track Facebook and G+ logins in user activity as well as passport logins.
This commit is contained in:
Scott Erickson 2016-04-11 16:51:51 -07:00
parent 92fecd8d5c
commit f1f1c23fd4
12 changed files with 263 additions and 243 deletions

View file

@ -0,0 +1,40 @@
authentication = require 'passport'
LocalStrategy = require('passport-local').Strategy
User = require '../models/User'
config = require '../../server_config'
errors = require '../commons/errors'
module.exports.setup = ->
authentication.serializeUser((user, done) -> done(null, user._id))
authentication.deserializeUser((id, done) ->
User.findById(id, (err, user) -> done(err, user)))
if config.picoCTF
pico = require('../lib/picoctf');
authentication.use new pico.PicoStrategy()
return
authentication.use(new LocalStrategy(
(username, password, done) ->
# kind of a hacky way to make it possible for iPads to 'log in' with their unique device id
if username.length is 36 and '@' not in username # must be an identifier for vendor
q = { iosIdentifierForVendor: username }
else
q = { emailLower: username.toLowerCase() }
User.findOne(q).exec((err, user) ->
return done(err) if err
if not user
return done(new errors.Unauthorized('not found', { property: 'email' }))
passwordReset = (user.get('passwordReset') or '').toLowerCase()
if passwordReset and password.toLowerCase() is passwordReset
User.update {_id: user.get('_id')}, {$unset: {passwordReset: ''}}, {}, ->
return done(null, user)
hash = User.hashPassword(password)
unless user.get('passwordHash') is hash
return done(new errors.Unauthorized('is wrong', { property: 'password' }))
return done(null, user)
)
))

View file

@ -88,6 +88,10 @@ errorResponseSchema = {
type: 'string'
description: 'Property which is related to the error (conflict, validation).'
}
name: {
type: 'string'
description: 'Provided for /auth/name.' # TODO: refactor out
}
}
}
errorProps = _.keys(errorResponseSchema.properties)

View file

@ -47,7 +47,6 @@ module.exports.handlerUrlOverrides =
module.exports.routes =
[
'routes/admin'
'routes/auth'
'routes/contact'
'routes/db'
'routes/file'

View file

@ -8,6 +8,9 @@ request = require 'request'
User = require '../models/User'
utils = require '../lib/utils'
mongoose = require 'mongoose'
authentication = require 'passport'
sendwithus = require '../sendwithus'
LevelSession = require '../models/LevelSession'
module.exports =
checkDocumentPermissions: (req, res, next) ->
@ -34,8 +37,26 @@ module.exports =
if not _.size(_.intersection(req.user.get('permissions'), permissions))
return next new errors.Forbidden('You do not have permissions necessary.')
next()
whoAmI: wrap (req, res) ->
if not req.user
user = User.makeNew(req)
yield user.save()
req.logInAsync = Promise.promisify(req.logIn)
yield req.logInAsync(user)
if req.query.callback
res.jsonp(req.user.toObject({req, publicOnly: true}))
else
res.send(req.user.toObject({req, publicOnly: false}))
res.end()
loginByGPlus: wrap (req, res) ->
afterLogin: wrap (req, res, next) ->
activity = req.user.trackActivity 'login', 1
yield req.user.update {activity: activity}
res.status(200).send(req.user.toObject({req: req}))
loginByGPlus: wrap (req, res, next) ->
gpID = req.body.gplusID
gpAT = req.body.gplusAccessToken
throw new errors.UnprocessableEntity('gplusID and gplusAccessToken required.') unless gpID and gpAT
@ -48,9 +69,9 @@ module.exports =
throw new errors.NotFound('No user with that G+ ID') unless user
req.logInAsync = Promise.promisify(req.logIn)
yield req.logInAsync(user)
res.status(200).send(user.formatEntity(req))
next()
loginByFacebook: wrap (req, res) ->
loginByFacebook: wrap (req, res, next) ->
fbID = req.body.facebookID
fbAT = req.body.facebookAccessToken
throw new errors.UnprocessableEntity('facebookID and facebookAccessToken required.') unless fbID and fbAT
@ -63,7 +84,7 @@ module.exports =
throw new errors.NotFound('No user with that Facebook ID') unless user
req.logInAsync = Promise.promisify(req.logIn)
yield req.logInAsync(user)
res.status(200).send(user.formatEntity(req))
next()
spy: wrap (req, res) ->
throw new errors.Unauthorized('You must be logged in to enter espionage mode') unless req.user
@ -94,3 +115,89 @@ module.exports =
req.loginAsync = Promise.promisify(req.login)
yield req.loginAsync user
res.status(200).send(user.toObject({req: req}))
logout: (req, res) ->
req.logout()
res.send({})
reset: wrap (req, res) ->
unless req.body.email
throw new errors.UnprocessableEntity('Need an email specified.', {property: 'email'})
user = yield User.findOne({emailLower: req.body.email.toLowerCase()})
if not user
throw new errors.NotFound('not found', {property: 'email'})
user.set('passwordReset', utils.getCodeCamel())
emailContent = "<h3>Your temporary password: <b>#{user.get('passwordReset')}</b></h3>"
emailContent += "<p>Reset your password at <a href=\"http://codecombat.com/account/settings\">http://codecombat.com/account/settings</a></p>"
emailContent += "<p>Your old password cannot be retrieved.</p>"
yield user.save()
context =
email_id: sendwithus.templates.generic_email
recipient:
address: req.body.email
email_data:
subject: 'CodeCombat Recovery Password'
title: ''
content: emailContent
sendwithus.api.sendAsync = Promise.promisify(sendwithus.api.send)
yield sendwithus.api.sendAsync(context)
res.end()
unsubscribe: wrap (req, res) ->
email = req.query.email
unless email
throw new errors.UnprocessableEntity 'No email provided to unsubscribe.'
email = decodeURIComponent(email)
if req.query.session
# Unsubscribe from just one session's notifications instead.
session = yield LevelSession.findOne({_id: req.query.session})
if not session
throw new errors.NotFound "Level session not found"
session.set 'unsubscribed', true
yield session.save()
res.send "Unsubscribed #{email} from CodeCombat emails for #{session.get('levelName')} #{session.get('team')} ladder updates. Sorry to see you go! <p><a href='/play/ladder/#{session.levelID}#my-matches'>Ladder preferences</a></p>"
res.end()
return
user = yield User.findOne({emailLower: email.toLowerCase()})
if not user
throw new errors.NotFound "No user found with email '#{email}'"
emails = _.clone(user.get('emails')) or {}
msg = ''
if req.query.recruitNotes
emails.recruitNotes ?= {}
emails.recruitNotes.enabled = false
msg = "Unsubscribed #{email} from recruiting emails."
else if req.query.employerNotes
emails.employerNotes ?= {}
emails.employerNotes.enabled = false
msg = "Unsubscribed #{email} from employer emails."
else
msg = "Unsubscribed #{email} from all CodeCombat emails. Sorry to see you go!"
emailSettings.enabled = false for emailSettings in _.values(emails)
emails.generalNews ?= {}
emails.generalNews.enabled = false
emails.anyNotes ?= {}
emails.anyNotes.enabled = false
yield user.update {$set: {emails: emails}}
res.send msg + '<p><a href="/account/settings">Account settings</a></p>'
res.end()
name: wrap (req, res) ->
if not req.params.name
throw new errors.UnprocessableEntity 'No name provided.'
originalName = req.params.name
User.unconflictNameAsync = Promise.promisify(User.unconflictName)
name = yield User.unconflictNameAsync originalName
response = name: name
if originalName is name
res.send 200, response
else
throw new errors.Conflict('Name is taken', response)

View file

@ -20,7 +20,7 @@ module.exports =
throw new errors.UnprocessableEntity('Invalid G+ Access Token.') unless idsMatch
user = yield User.findOne({gplusID: gpID})
throw new errors.NotFound('No user with that G+ ID') unless user
res.status(200).send(user.formatEntity(req))
res.status(200).send(user.toObject({req: req}))
fetchByFacebookID: wrap (req, res, next) ->
fbID = req.query.facebookID
@ -31,10 +31,8 @@ module.exports =
dbq.select(parse.getProjectFromReq(req))
url = "https://graph.facebook.com/me?access_token=#{fbAT}"
[facebookRes, body] = yield request.getAsync(url, {json: true})
console.log '...', body, facebookRes.statusCode
idsMatch = fbID is body.id
throw new errors.UnprocessableEntity('Invalid Facebook Access Token.') unless idsMatch
user = yield User.findOne({facebookID: fbID})
throw new errors.NotFound('No user with that Facebook ID') unless user
console.log 'okay done'
res.status(200).send(user.formatEntity(req))
res.status(200).send(user.toObject({req: req}))

View file

@ -6,6 +6,7 @@ mail = require '../commons/mail'
log = require 'winston'
plugins = require '../plugins/plugins'
AnalyticsUsersActive = require './AnalyticsUsersActive'
languages = require '../routes/languages'
config = require '../../server_config'
stripe = require('stripe')(config.stripe.secretKey)
@ -253,16 +254,6 @@ UserSchema.methods.isPremium = ->
return true if @hasSubscription()
return false
UserSchema.methods.formatEntity = (req, publicOnly=false) ->
obj = @toObject()
serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset', 'lastIP']
delete obj[prop] for prop in serverProperties
candidateProperties = ['jobProfile', 'jobProfileApproved', 'jobProfileNotes']
delete obj[prop] for prop in candidateProperties
includePrivates = not publicOnly and (req.user and (req.user.isAdmin() or req.user._id.equals(@_id)))
delete obj[prop] for prop in User.privateProperties unless includePrivates
return obj
UserSchema.methods.isOnPremiumServer = ->
@get('country') in ['china', 'brazil']
@ -374,9 +365,27 @@ UserSchema.set('toObject', {
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)

View file

@ -1,203 +0,0 @@
authentication = require 'passport'
LocalStrategy = require('passport-local').Strategy
User = require '../models/User'
UserHandler = require '../handlers/user_handler'
LevelSession = require '../models/LevelSession'
config = require '../../server_config'
errors = require '../commons/errors'
languages = require '../routes/languages'
sendwithus = require '../sendwithus'
log = require 'winston'
utils = require '../lib/utils'
module.exports.setup = (app) ->
authentication.serializeUser((user, done) -> done(null, user._id))
authentication.deserializeUser((id, done) ->
User.findById(id, (err, user) -> done(err, user)))
if config.picoCTF
pico = require('../lib/picoctf');
authentication.use new pico.PicoStrategy()
return
authentication.use(new LocalStrategy(
(username, password, done) ->
# kind of a hacky way to make it possible for iPads to 'log in' with their unique device id
if username.length is 36 and '@' not in username # must be an identifier for vendor
q = { iosIdentifierForVendor: username }
else
q = { emailLower: username.toLowerCase() }
User.findOne(q).exec((err, user) ->
return done(err) if err
return done(null, false, {message: 'not found', property: 'email'}) if not user
passwordReset = (user.get('passwordReset') or '').toLowerCase()
if passwordReset and password.toLowerCase() is passwordReset
User.update {_id: user.get('_id')}, {passwordReset: ''}, {}, ->
return done(null, user)
hash = User.hashPassword(password)
unless user.get('passwordHash') is hash
return done(null, false, {message: 'is wrong', property: 'password'})
return done(null, user)
)
))
app.post('/auth/login', (req, res, next) ->
authentication.authenticate('local', (err, user, info) ->
return next(err) if err
if not user
return errors.unauthorized(res, [{message: info.message, property: info.property}])
req.logIn(user, (err) ->
return next(err) if (err)
activity = req.user.trackActivity 'login', 1
user.update {activity: activity}, (err) ->
return next(err) if (err)
res.send(UserHandler.formatEntity(req, req.user))
return res.end()
)
)(req, res, next)
)
app.get('/auth/whoami', (req, res) ->
if req.user
sendSelf(req, res)
else
user = makeNewUser(req)
makeNext = (req, res) -> -> sendSelf(req, res)
next = makeNext(req, res)
loginUser(req, res, user, false, next)
)
sendSelf = (req, res) ->
res.setHeader('Content-Type', 'text/json')
if req.query.callback
res.jsonp UserHandler.formatEntity(req, req.user, true)
else
res.send UserHandler.formatEntity(req, req.user, false)
res.end()
app.post('/auth/logout', (req, res) ->
req.logout()
res.send({})
)
app.post('/auth/reset', (req, res) ->
unless req.body.email
return errors.badInput(res, [{message: 'Need an email specified.', property: 'email'}])
User.findOne({emailLower: req.body.email.toLowerCase()}).exec((err, user) ->
if not user
return errors.notFound(res, [{message: 'not found', property: 'email'}])
user.set('passwordReset', utils.getCodeCamel())
emailContent = "<h3>Your temporary password: <b>#{user.get('passwordReset')}</b></h3>"
emailContent += "<p>Reset your password at <a href=\"http://codecombat.com/account/settings\">http://codecombat.com/account/settings</a></p>"
emailContent += "<p>Your old password cannot be retrieved.</p>"
user.save (err) =>
return errors.serverError(res) if err
context =
email_id: sendwithus.templates.generic_email
recipient:
address: req.body.email
email_data:
subject: 'CodeCombat Recovery Password'
title: ''
content: emailContent
sendwithus.api.send context, (err, result) ->
if err
console.error "Error sending password reset email: #{err.message or err}"
res.end()
)
)
app.get '/auth/unsubscribe', (req, res) ->
req.query.email = decodeURIComponent(req.query.email)
email = req.query.email
unless req.query.email
return errors.badInput res, 'No email provided to unsubscribe.'
if req.query.session
# Unsubscribe from just one session's notifications instead.
return LevelSession.findOne({_id: req.query.session}).exec (err, session) ->
return errors.serverError res, 'Could not unsubscribe: #{req.query.session}, #{req.query.email}: #{err}' if err
session.set 'unsubscribed', true
session.save (err) ->
return errors.serverError res, 'Database failure.' if err
res.send "Unsubscribed #{req.query.email} from CodeCombat emails for #{session.get('levelName')} #{session.get('team')} ladder updates. Sorry to see you go! <p><a href='/play/ladder/#{session.levelID}#my-matches'>Ladder preferences</a></p>"
res.end()
User.findOne({emailLower: req.query.email.toLowerCase()}).exec (err, user) ->
if not user
return errors.notFound res, "No user found with email '#{req.query.email}'"
emails = _.clone(user.get('emails')) or {}
msg = ''
if req.query.recruitNotes
emails.recruitNotes ?= {}
emails.recruitNotes.enabled = false
msg = "Unsubscribed #{req.query.email} from recruiting emails."
else if req.query.employerNotes
emails.employerNotes ?= {}
emails.employerNotes.enabled = false
msg = "Unsubscribed #{req.query.email} from employer emails."
else
msg = "Unsubscribed #{req.query.email} from all CodeCombat emails. Sorry to see you go!"
emailSettings.enabled = false for emailSettings in _.values(emails)
emails.generalNews ?= {}
emails.generalNews.enabled = false
emails.anyNotes ?= {}
emails.anyNotes.enabled = false
user.update {$set: {emails: emails}}, {}, =>
return errors.serverError res, 'Database failure.' if err
res.send msg + '<p><a href="/account/settings">Account settings</a></p>'
res.end()
app.get '/auth/name*', (req, res) ->
parts = req.path.split '/'
originalName = decodeURI parts[3]
return errors.badInput res, 'No name provided.' unless parts.length > 3 and originalName? and originalName isnt ''
return errors.notFound res if parts.length isnt 4
User.unconflictName originalName, (err, name) ->
return errors.serverError res, err if err
response = name: name
if originalName is name
res.send 200, response
else
errors.conflict res, response
module.exports.loginUser = loginUser = (req, res, user, send=true, next=null) ->
user.save((err) ->
return errors.serverError res, err if err?
req.logIn(user, (err) ->
return errors.serverError res, err if err?
return res.send(user) and res.end() if send
next() if next
)
)
module.exports.idCounter = 0
module.exports.makeNewUser = makeNewUser = (req) ->
user = new User({anonymous: true})
if global.testing
# allows tests some control over user id creation
newID = _.pad((module.exports.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
#log.info "making new user #{user.get('_id')} with language #{user.get('preferredLanguage')} of #{req.acceptedLanguages} and country #{req.country} on #{if config.tokyo then 'Tokyo' else (if config.saoPaulo then 'Brazil' else 'US')} server and lastIP #{user.get('lastIP')}."
user

View file

@ -2,10 +2,17 @@ mw = require '../middleware'
module.exports.setup = (app) ->
app.post('/auth/login-facebook', mw.auth.loginByFacebook)
app.post('/auth/login-gplus', mw.auth.loginByGPlus)
passport = require('passport')
app.post('/auth/login', passport.authenticate('local'), mw.auth.afterLogin)
app.post('/auth/login-facebook', mw.auth.loginByFacebook, mw.auth.afterLogin)
app.post('/auth/login-gplus', mw.auth.loginByGPlus, mw.auth.afterLogin)
app.post('/auth/logout', mw.auth.logout)
app.get('/auth/name/?(:name)?', mw.auth.name)
app.post('/auth/reset', mw.auth.reset)
app.post('/auth/spy', mw.auth.spy)
app.post('/auth/stop-spying', mw.auth.stopSpying)
app.get('/auth/unsubscribe', mw.auth.unsubscribe)
app.get('/auth/whoami', mw.auth.whoAmI)
Achievement = require '../models/Achievement'
app.get('/db/achievement', mw.achievements.fetchByRelated, mw.rest.get(Achievement))

View file

@ -1,14 +1,20 @@
config = require '../server_config'
sendwithusAPI = require 'sendwithus'
swuAPIKey = config.mail.sendwithusAPIKey
log = require 'winston'
module.exports.setupRoutes = (app) ->
return
debug = not config.isProduction
module.exports.api = new sendwithusAPI swuAPIKey, debug
if config.unittest
module.exports.api.send = ->
module.exports.api =
send: (context, cb) ->
log.debug('Tried to send email with context: ', JSON.stringify(context, null, '\t'))
setTimeout(cb, 10)
if swuAPIKey
module.exports.api = new sendwithusAPI swuAPIKey, debug
module.exports.templates =
parent_subscribe_email: 'tem_2APERafogvwKhmcnouigud'
share_progress_email: 'tem_VHE3ihhGmVa3727qds9zY8'

View file

@ -13,7 +13,7 @@ baseRoute = require './server/routes/base'
user = require './server/handlers/user_handler'
logging = require './server/commons/logging'
config = require './server_config'
auth = require './server/routes/auth'
auth = require './server/commons/auth'
routes = require './server/routes'
UserHandler = require './server/handlers/user_handler'
slack = require './server/slack'
@ -108,6 +108,7 @@ setupPassportMiddleware = (app) ->
require('./server/lib/picoctf').init app
else
app.use(authentication.session())
auth.setup()
setupCountryRedirectMiddleware = (app, country="china", countryCode="CN", languageCode="zh", serverID="tokyo") ->
shouldRedirectToCountryServer = (req) ->

View file

@ -211,6 +211,5 @@ _drop = (done) ->
done()
GLOBAL.resetUserIDCounter = (number=0) ->
auth = require '../../server/routes/auth'
auth.idCounter = number
User.idCounter = number

View file

@ -6,6 +6,7 @@ Promise = require 'bluebird'
nock = require 'nock'
request = require '../request'
sendwithus = require '../../../server/sendwithus'
LevelSession = require '../../../server/models/LevelSession'
urlLogin = getURL('/auth/login')
urlReset = getURL('/auth/reset')
@ -24,7 +25,7 @@ describe 'POST /auth/login', ->
done()
it 'allows logging in by iosIdentifierForVendor', utils.wrap (done) ->
user = yield utils.initUser({
yield utils.initUser({
'iosIdentifierForVendor': '012345678901234567890123456789012345'
'password': '12345'
})
@ -44,7 +45,7 @@ describe 'POST /auth/login', ->
done()
it 'returns 200 when the user does exist', utils.wrap (done) ->
user = yield utils.initUser({
yield utils.initUser({
'email': 'some@email.com'
'password': '12345'
})
@ -56,7 +57,7 @@ describe 'POST /auth/login', ->
done()
it 'rejects wrong passwords', utils.wrap (done) ->
user = yield utils.initUser({
yield utils.initUser({
'email': 'some@email.com'
'password': '12345'
})
@ -68,7 +69,7 @@ describe 'POST /auth/login', ->
done()
it 'is completely case insensitive', utils.wrap (done) ->
user = yield utils.initUser({
yield utils.initUser({
'email': 'Some@Email.com'
'password': 'AbCdE'
})
@ -104,7 +105,9 @@ describe 'POST /auth/reset', ->
done()
it 'resets the user password', utils.wrap (done) ->
spyOn(sendwithus.api, 'send')
spyOn(sendwithus.api, 'send').and.callFake (options, cb) ->
expect(options.recipient.address).toBe('some@email.com')
cb()
[res, body] = yield request.postAsync(
{uri: urlReset, json: {email: 'some@email.com'}}
)
@ -120,9 +123,11 @@ describe 'POST /auth/reset', ->
expect(res.statusCode).toBe(200)
done()
# TODO: Finish refactoring the rest of these old tests
it 'resetting password is not idempotent', utils.wrap (done) ->
spyOn(sendwithus.api, 'send').and.callFake (options, cb) ->
expect(options.recipient.address).toBe('some@email.com')
cb()
[res, body] = yield request.postAsync(
{uri: urlReset, json: {email: 'some@email.com'}}
)
@ -145,17 +150,65 @@ describe 'GET /auth/unsubscribe', ->
beforeEach utils.wrap (done) ->
yield utils.clearModels([User])
@user = yield utils.initUser()
done()
it 'returns 422 if email is not included', utils.wrap (done) ->
url = getURL('/auth/unsubscribe')
[res, body] = yield request.getAsync(url)
expect(res.statusCode).toBe(422)
done()
it 'removes just recruitment emails if you include ?recruitNotes=1', utils.wrap (done) ->
user = yield utils.initUser()
url = getURL('/auth/unsubscribe?recruitNotes=1&email='+user.get('email'))
it 'returns 404 if email is not found', utils.wrap (done) ->
url = getURL('/auth/unsubscribe?email=ladeeda')
[res, body] = yield request.getAsync(url)
expect(res.statusCode).toBe(200)
user = yield User.findOne(user._id)
expect(user.get('emails').recruitNotes.enabled).toBe(false)
expect(user.isEmailSubscriptionEnabled('generalNews')).toBeTruthy()
expect(res.statusCode).toBe(404)
done()
describe '?recruitNotes=1', ->
it 'unsubscribes the user from recruitment emails', utils.wrap (done) ->
url = getURL('/auth/unsubscribe?recruitNotes=1&email='+@user.get('email'))
[res, body] = yield request.getAsync(url)
expect(res.statusCode).toBe(200)
user = yield User.findOne(@user._id)
expect(user.get('emails').recruitNotes.enabled).toBe(false)
expect(user.isEmailSubscriptionEnabled('generalNews')).toBeTruthy()
done()
describe '?employerNotes=1', ->
it 'unsubscribes the user from employer emails', utils.wrap (done) ->
url = getURL('/auth/unsubscribe?employerNotes=1&email='+@user.get('email'))
[res, body] = yield request.getAsync(url)
expect(res.statusCode).toBe(200)
user = yield User.findOne(@user._id)
expect(user.get('emails').employerNotes.enabled).toBe(false)
expect(user.isEmailSubscriptionEnabled('generalNews')).toBeTruthy()
done()
describe '?session=:id', ->
it 'sets the given LevelSession\'s unsubscribed property to true', utils.wrap (done) ->
session = new LevelSession({permissions:[target: @user._id, access: 'owner']})
yield session.save()
url = getURL("/auth/unsubscribe?session=#{session.id}&email=#{@user.get('email')}")
[res, body] = yield request.getAsync(url)
expect(res.statusCode).toBe(200)
session = yield LevelSession.findById(session.id)
expect(session.get('unsubscribed')).toBe(true)
done()
describe 'no GET query params', ->
it 'unsubscribes the user from all emails', utils.wrap (done) ->
url = getURL("/auth/unsubscribe?email=#{@user.get('email')}")
[res, body] = yield request.getAsync(url)
expect(res.statusCode).toBe(200)
user = yield User.findOne(@user._id)
expect(user.get('emails').generalNews.enabled).toBe(false)
expect(user.get('emails').anyNotes.enabled).toBe(false)
done()
describe 'GET /auth/name', ->
url = '/auth/name'