codecombat/server/routes/auth.coffee
2016-01-11 09:52:46 -08:00

216 lines
9 KiB
CoffeeScript

authentication = require 'passport'
LocalStrategy = require('passport-local').Strategy
User = require '../users/User'
UserHandler = require '../users/user_handler'
LevelSession = require '../levels/sessions/LevelSession'
config = require '../../server_config'
errors = require '../commons/errors'
languages = require '../routes/languages'
sendwithus = require '../sendwithus'
log = require 'winston'
module.exports.setup = (app) ->
authentication.serializeUser((user, done) -> done(null, user._id))
authentication.deserializeUser((id, done) ->
User.findById(id, (err, user) -> done(err, user)))
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/spy', (req, res, next) ->
if req?.user?.isAdmin()
target = req.body.nameOrEmailLower
return errors.badInput res, 'Specify a username or email to espionage.' unless target
query = $or: [{nameLower: target}, {emailLower: target}]
User.findOne query, (err, user) ->
if err? then return errors.serverError res, 'There was an error finding the specified user'
unless user then return errors.badInput res, 'The specified user couldn\'t be found'
req.logIn user, (err) ->
if err? then return errors.serverError res, 'There was an error logging in with the specified user'
res.send(UserHandler.formatEntity(req, user))
return res.end()
else
return errors.unauthorized res, 'You must be an admin to enter espionage mode'
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.end()
)
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', Math.random().toString(36).slice(2, 7).toUpperCase())
user.save (err) =>
return errors.serverError(res) if err
unless config.unittest
context =
email_id: sendwithus.templates.generic_email
recipient:
address: req.body.email
email_data:
subject: 'CodeCombat Recovery Password'
title: 'Recovery Password'
content: "<p>Your CodeCombat recovery password for email #{req.body.email} is: #{user.get('passwordReset')}</p><p>Log in at <a href=\"http://codecombat.com/account/settings\">http://codecombat.com/account/settings</a> and change it.</p><p>Hope this helps!</p>"
sendwithus.api.send context, (err, result) ->
if err
console.error "Error sending password reset email: #{err.message or err}"
return errors.serverError(res) if err
else
return res.end()
else
console.log 'password is', user.get('passwordReset')
res.send user.get('passwordReset')
return 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