mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-14 07:00:01 -04:00
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:
parent
92fecd8d5c
commit
f1f1c23fd4
12 changed files with 263 additions and 243 deletions
40
server/commons/auth.coffee
Normal file
40
server/commons/auth.coffee
Normal 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)
|
||||
)
|
||||
))
|
|
@ -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)
|
||||
|
|
|
@ -47,7 +47,6 @@ module.exports.handlerUrlOverrides =
|
|||
module.exports.routes =
|
||||
[
|
||||
'routes/admin'
|
||||
'routes/auth'
|
||||
'routes/contact'
|
||||
'routes/db'
|
||||
'routes/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)
|
|
@ -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}))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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))
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -211,6 +211,5 @@ _drop = (done) ->
|
|||
done()
|
||||
|
||||
GLOBAL.resetUserIDCounter = (number=0) ->
|
||||
auth = require '../../server/routes/auth'
|
||||
auth.idCounter = number
|
||||
User.idCounter = number
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in a new issue