mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-02-17 00:40:56 -05:00
Add return-to-admin (turn off espionage mode)
This commit is contained in:
parent
59e8c42ddb
commit
a2249f8df1
15 changed files with 212 additions and 52 deletions
|
@ -81,6 +81,7 @@
|
|||
// Placeholder for iPad, which inspects the user object at the bottom of an injected page.
|
||||
window.serverConfig = "serverConfigTag";
|
||||
window.userObject = "userObjectTag";
|
||||
window.amActually = "amActuallyTag";
|
||||
window.me = {
|
||||
get: function(attribute) { return window.userObject[attribute]; }
|
||||
}
|
||||
|
|
|
@ -102,3 +102,12 @@ module.exports.updateSelects = (el) ->
|
|||
module.exports.validateEmail = (email) ->
|
||||
filter = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,63}$/i # https://news.ycombinator.com/item?id=5763990
|
||||
return filter.test(email)
|
||||
|
||||
module.exports.disableSubmit = (el, message='...') ->
|
||||
$el = $(el)
|
||||
$el.data('original-text', $el.text())
|
||||
$el.text(message).attr('disabled', true)
|
||||
|
||||
module.exports.enableSubmit = (el) ->
|
||||
$el = $(el)
|
||||
$el.text($el.data('original-text')).attr('disabled', false)
|
||||
|
|
|
@ -180,6 +180,19 @@ module.exports = class User extends CocoModel
|
|||
|
||||
isOnPremiumServer: ->
|
||||
me.get('country') in ['china', 'brazil']
|
||||
|
||||
spy: (user, options={}) ->
|
||||
user = user.id or user # User instance, user ID, email or username
|
||||
options.url = '/auth/spy'
|
||||
options.type = 'POST'
|
||||
options.data ?= {}
|
||||
options.data.user = user
|
||||
@fetch(options)
|
||||
|
||||
stopSpying: (options={}) ->
|
||||
options.url = '/auth/stop-spying'
|
||||
options.type = 'POST'
|
||||
@fetch(options)
|
||||
|
||||
tiersByLevel = [-1, 0, 0.05, 0.14, 0.18, 0.32, 0.41, 0.5, 0.64, 0.82, 0.91, 1.04, 1.22, 1.35, 1.48, 1.65, 1.78, 1.96, 2.1, 2.24, 2.38, 2.55, 2.69, 2.86, 3.03, 3.16, 3.29, 3.42, 3.58, 3.74, 3.89, 4.04, 4.19, 4.32, 4.47, 4.64, 4.79, 4.96,
|
||||
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 10, 10.5, 11, 11.5, 12, 12.5, 13, 13.5, 14, 14.5, 15
|
||||
|
|
|
@ -3,7 +3,7 @@ extends /templates/base
|
|||
block content
|
||||
|
||||
.form-horizontal
|
||||
.form-group
|
||||
form#espionage-form.form-group
|
||||
label.control-label.col-sm-2(for="espionage-name-or-email" data-i18n="admin.av_espionage") Espionage
|
||||
.col-sm-4
|
||||
input.form-control#espionage-name-or-email(data-i18n="[placeholder]admin.av_espionage_placeholder", placeholder="Email or username", type="text")
|
||||
|
@ -11,7 +11,12 @@ block content
|
|||
button.btn.btn-primary.btn-large#enter-espionage-mode 007
|
||||
label.control-label.col-sm-5(for="espionage-name-or-email")
|
||||
em you are currently #{me.get('name')} at #{me.get('email')}
|
||||
.form-group
|
||||
if view.amActually
|
||||
br
|
||||
em but you are actually #{view.amActually.get('name')} at #{view.amActually.get('email')}
|
||||
br
|
||||
button#stop-spying-btn.btn.btn-xs Stop Spying
|
||||
form#user-search-form.form-group
|
||||
label.control-label.col-sm-2(for="user-search" data-i18n="admin.av_usersearch") User Search
|
||||
.col-sm-4
|
||||
input.form-control#user-search(data-i18n="[placeholder]admin.av_usersearch_placeholder", placeholder="Email, username, name, whatever", type="text")
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
{backboneFailure, genericFailure} = require 'core/errors'
|
||||
errors = require 'core/errors'
|
||||
RootView = require 'views/core/RootView'
|
||||
template = require 'templates/admin'
|
||||
AdministerUserModal = require 'views/admin/AdministerUserModal'
|
||||
forms = require 'core/forms'
|
||||
User = require 'models/User'
|
||||
|
||||
module.exports = class MainAdminView extends RootView
|
||||
id: 'admin-view'
|
||||
|
@ -9,38 +12,48 @@ module.exports = class MainAdminView extends RootView
|
|||
lastUserSearchValue: ''
|
||||
|
||||
events:
|
||||
'keyup': 'checkForFormSubmissionEnterPress'
|
||||
'click #enter-espionage-mode': 'enterEspionageMode'
|
||||
'click #user-search-button': 'searchForUser'
|
||||
'submit #espionage-form': 'onSubmitEspionageForm'
|
||||
'submit #user-search-form': 'onSubmitUserSearchForm'
|
||||
'click #stop-spying-btn': 'onClickStopSpyingButton'
|
||||
'click #increment-button': 'incrementUserAttribute'
|
||||
'click #user-search-result': 'onClickUserSearchResult'
|
||||
'click #create-free-sub-btn': 'onClickFreeSubLink'
|
||||
'click #terminal-create': 'onClickTerminalSubLink'
|
||||
|
||||
initialize: ->
|
||||
if window.amActually
|
||||
@amActually = new User({_id: window.amActually})
|
||||
@amActually.fetch()
|
||||
@supermodel.trackModel(@amActually)
|
||||
|
||||
checkForFormSubmissionEnterPress: (e) ->
|
||||
if e.which is 13 and @$el.find('#espionage-name-or-email').val() isnt ''
|
||||
@enterEspionageMode()
|
||||
return
|
||||
if @$el.find('#user-search').val() isnt @lastUserSearchValue
|
||||
@searchForUser()
|
||||
onClickStopSpyingButton: ->
|
||||
button = @$('#stop-spying-btn')
|
||||
forms.disableSubmit(button)
|
||||
me.stopSpying({
|
||||
success: -> document.location.reload()
|
||||
error: ->
|
||||
forms.enableSubmit(button)
|
||||
errors.showNotyNetworkError(arguments...)
|
||||
})
|
||||
|
||||
enterEspionageMode: ->
|
||||
onSubmitEspionageForm: (e) ->
|
||||
e.preventDefault()
|
||||
button = @$('#enter-espionage-mode')
|
||||
userNameOrEmail = @$el.find('#espionage-name-or-email').val().toLowerCase()
|
||||
$.ajax
|
||||
type: 'POST',
|
||||
url: '/auth/spy'
|
||||
data: {nameOrEmailLower: userNameOrEmail}
|
||||
success: @espionageSuccess
|
||||
error: @espionageFailure
|
||||
forms.disableSubmit(button)
|
||||
me.spy(userNameOrEmail, {
|
||||
success: -> window.location.reload()
|
||||
error: ->
|
||||
forms.enableSubmit(button)
|
||||
errors.showNotyNetworkError(arguments...)
|
||||
})
|
||||
|
||||
espionageSuccess: (model) ->
|
||||
window.location.reload()
|
||||
|
||||
espionageFailure: (jqxhr, status, error)->
|
||||
console.log "There was an error entering espionage mode: #{error}"
|
||||
|
||||
searchForUser: ->
|
||||
return @onSearchRequestSuccess [] unless @lastUserSearchValue = @$el.find('#user-search').val().toLowerCase()
|
||||
onSubmitUserSearchForm: (e) ->
|
||||
e.preventDefault()
|
||||
searchValue = @$el.find('#user-search').val()
|
||||
return if searchValue is @lastUserSearchValue
|
||||
return @onSearchRequestSuccess [] unless @lastUserSearchValue = searchValue.toLowerCase()
|
||||
forms.disableSubmit(@$('#user-search-button'))
|
||||
$.ajax
|
||||
type: 'POST',
|
||||
url: '/db/user/-/admin_search'
|
||||
|
@ -49,6 +62,7 @@ module.exports = class MainAdminView extends RootView
|
|||
error: @onSearchRequestFailure
|
||||
|
||||
onSearchRequestSuccess: (users) =>
|
||||
forms.enableSubmit(@$('#user-search-button'))
|
||||
result = ''
|
||||
if users.length
|
||||
result = ("<tr data-user-id='#{user._id}'><td><code>#{user._id}</code></td><td>#{_.escape(user.name or 'Anoner')}</td><td>#{_.escape(user.email)}</td></tr>" for user in users)
|
||||
|
@ -57,6 +71,7 @@ module.exports = class MainAdminView extends RootView
|
|||
|
||||
onSearchRequestFailure: (jqxhr, status, error) =>
|
||||
return if @destroyed
|
||||
forms.enableSubmit(@$('#user-search-button'))
|
||||
console.warn "There was an error looking up #{@lastUserSearchValue}:", error
|
||||
|
||||
incrementUserAttribute: (e) ->
|
||||
|
|
|
@ -34,6 +34,9 @@ module.exports = class RootView extends CocoView
|
|||
subscriptions:
|
||||
'achievements:new': 'handleNewAchievements'
|
||||
'modal:open-modal-view': 'onOpenModalView'
|
||||
|
||||
shortcuts:
|
||||
'ctrl+shift+a': 'navigateToAdmin'
|
||||
|
||||
showNewAchievement: (achievement, earnedAchievement) ->
|
||||
earnedAchievement.set('notified', true)
|
||||
|
@ -183,3 +186,7 @@ module.exports = class RootView extends CocoView
|
|||
return false
|
||||
|
||||
logoutRedirectURL: '/'
|
||||
|
||||
navigateToAdmin: ->
|
||||
if window.amActually or me.isAdmin()
|
||||
application.router.navigate('/admin', {trigger: true})
|
|
@ -1,6 +1,11 @@
|
|||
# Middleware for both authentication and authorization
|
||||
|
||||
errors = require '../commons/errors'
|
||||
utils = require '../lib/utils'
|
||||
wrap = require 'co-express'
|
||||
Promise = require 'bluebird'
|
||||
mongoose = require 'mongoose'
|
||||
User = require '../models/User'
|
||||
|
||||
module.exports = {
|
||||
checkDocumentPermissions: (req, res, next) ->
|
||||
|
@ -28,4 +33,34 @@ module.exports = {
|
|||
return next new errors.Forbidden('You do not have permissions necessary.')
|
||||
next()
|
||||
|
||||
spy: wrap (req, res) ->
|
||||
throw new errors.Unauthorized('You must be logged in to enter espionage mode') unless req.user
|
||||
throw new errors.Forbidden('You must be an admin to enter espionage mode') unless req.user.isAdmin()
|
||||
|
||||
user = req.body.user
|
||||
throw new errors.UnprocessableEntity('Specify an id, username or email to espionage.') unless user
|
||||
if utils.isID(user)
|
||||
query = {_id: mongoose.Types.ObjectId(user)}
|
||||
else
|
||||
user = user.toLowerCase()
|
||||
query = $or: [{nameLower: user}, {emailLower: user}]
|
||||
user = yield User.findOne(query)
|
||||
amActually = req.user
|
||||
throw new errors.NotFound() unless user
|
||||
req.loginAsync = Promise.promisify(req.login)
|
||||
yield req.loginAsync user
|
||||
req.session.amActually = amActually.id
|
||||
res.status(200).send(user.toObject({req: req}))
|
||||
|
||||
stopSpying: wrap (req, res) ->
|
||||
throw new errors.Unauthorized('You must be logged in to leave espionage mode') unless req.user
|
||||
throw new errors.Forbidden('You must be in espionage mode to leave it') unless req.session.amActually
|
||||
|
||||
user = yield User.findById(req.session.amActually)
|
||||
delete req.session.amActually
|
||||
throw new errors.NotFound() unless user
|
||||
req.loginAsync = Promise.promisify(req.login)
|
||||
yield req.loginAsync user
|
||||
res.status(200).send(user.toObject({req: req}))
|
||||
|
||||
}
|
3
server/models/User.coffee
Normal file
3
server/models/User.coffee
Normal file
|
@ -0,0 +1,3 @@
|
|||
# TODO: Migrate User to here
|
||||
|
||||
module.exports = require '../users/User'
|
|
@ -44,21 +44,6 @@ module.exports.setup = (app) ->
|
|||
)
|
||||
))
|
||||
|
||||
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
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
mw = require '../middleware'
|
||||
|
||||
module.exports.setup = (app) ->
|
||||
app.post('/auth/spy', mw.auth.spy)
|
||||
app.post('/auth/stop-spying', mw.auth.stopSpying)
|
||||
|
||||
Article = require '../models/Article'
|
||||
app.get('/db/article', mw.rest.get(Article))
|
||||
app.post('/db/article', mw.auth.checkHasPermission(['admin', 'artisan']), mw.rest.post(Article))
|
||||
|
|
|
@ -335,6 +335,20 @@ UserSchema.statics.editableProperties = [
|
|||
'heroConfig', 'iosIdentifierForVendor', 'siteref', 'referrer', 'schoolName', 'role'
|
||||
]
|
||||
|
||||
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))
|
||||
delete ret[prop] for prop in User.privateProperties unless includePrivates
|
||||
delete ret[prop] for prop in User.candidateProperties
|
||||
return ret
|
||||
})
|
||||
UserSchema.plugin plugins.NamedPlugin
|
||||
|
||||
module.exports = User = mongoose.model('User', UserSchema)
|
||||
|
|
|
@ -45,13 +45,14 @@ UserHandler = class UserHandler extends Handler
|
|||
props
|
||||
|
||||
formatEntity: (req, document, publicOnly=false) =>
|
||||
# TODO: Delete. This function is duplicated in server User model toObject transform.
|
||||
return null unless document?
|
||||
obj = document.toObject()
|
||||
delete obj[prop] for prop in serverProperties
|
||||
includePrivates = not publicOnly and (req.user and (req.user.isAdmin() or req.user._id.equals(document._id)))
|
||||
delete obj[prop] for prop in @privateProperties unless includePrivates
|
||||
delete obj[prop] for prop in User.serverProperties
|
||||
includePrivates = not publicOnly and (req.user and (req.user.isAdmin() or req.user._id.equals(document._id) or req.session.amActually is document.id))
|
||||
delete obj[prop] for prop in User.privateProperties unless includePrivates
|
||||
includeCandidate = not publicOnly and (includePrivates or (obj.jobProfile?.active and req.user and ('employer' in (req.user.get('permissions') ? [])) and @employerCanViewCandidate req.user, obj))
|
||||
delete obj[prop] for prop in candidateProperties unless includeCandidate
|
||||
delete obj[prop] for prop in User.candidateProperties unless includeCandidate
|
||||
return obj
|
||||
|
||||
waterfallFunctions: [
|
||||
|
|
|
@ -85,10 +85,13 @@ setupExpressMiddleware = (app) ->
|
|||
app.use(useragent.express())
|
||||
|
||||
app.use(express.favicon())
|
||||
app.use(express.cookieParser(config.cookie_secret))
|
||||
app.use(express.cookieParser())
|
||||
app.use(express.bodyParser())
|
||||
app.use(express.methodOverride())
|
||||
app.use(express.cookieSession({secret:'defenestrate'}))
|
||||
app.use(express.cookieSession({
|
||||
key:'codecombat.sess'
|
||||
secret:config.cookie_secret
|
||||
}))
|
||||
|
||||
setupPassportMiddleware = (app) ->
|
||||
app.use(authentication.initialize())
|
||||
|
@ -180,6 +183,7 @@ setupFallbackRouteToIndex = (app) ->
|
|||
production: config.isProduction
|
||||
|
||||
data = data.replace('"userObjectTag"', user)
|
||||
data = data.replace('"amActuallyTag"', JSON.stringify(req.session.amActually))
|
||||
res.header 'Cache-Control', 'no-cache, no-store, must-revalidate'
|
||||
res.header 'Pragma', 'no-cache'
|
||||
res.header 'Expires', 0
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
require '../common'
|
||||
User = require '../../../server/users/User'
|
||||
utils = require '../utils'
|
||||
_ = require 'lodash'
|
||||
Promise = require 'bluebird'
|
||||
|
||||
urlLogin = getURL('/auth/login')
|
||||
urlReset = getURL('/auth/reset')
|
||||
|
@ -199,3 +202,66 @@ describe '/auth/name', ->
|
|||
expect(response.body.name).not.toBe 'joe'
|
||||
expect(response.body.name.length).toBe 4 # 'joe' and a random number
|
||||
done()
|
||||
|
||||
|
||||
describe 'POST /auth/spy', ->
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([User])
|
||||
@admin = yield utils.initAdmin()
|
||||
@user1 = yield utils.initUser({name: 'Test User 1'})
|
||||
@user2 = yield utils.initUser({name: 'Test User 2'})
|
||||
done()
|
||||
|
||||
it 'logs in an admin as an arbitrary user', utils.wrap (done) ->
|
||||
yield utils.loginUser(@admin)
|
||||
[res, body] = yield request.postAsync {uri: getURL('/auth/spy'), json: {user: @user1.id}}
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body._id).toBe(@user1.id)
|
||||
[res, body] = yield request.getAsync {uri: getURL('/auth/whoami'), json: true}
|
||||
expect(body._id).toBe(@user1.id)
|
||||
done()
|
||||
|
||||
it 'accepts the user\'s email as input', utils.wrap (done) ->
|
||||
yield utils.loginUser(@admin)
|
||||
[res, body] = yield request.postAsync {uri: getURL('/auth/spy'), json: {user: @user1.get('email')}}
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body._id).toBe(@user1.id)
|
||||
done()
|
||||
|
||||
it 'accepts the user\'s username as input', utils.wrap (done) ->
|
||||
yield utils.loginUser(@admin)
|
||||
[res, body] = yield request.postAsync {uri: getURL('/auth/spy'), json: {user: @user1.get('name')}}
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body._id).toBe(@user1.id)
|
||||
done()
|
||||
|
||||
it 'does not work for anonymous users', utils.wrap (done) ->
|
||||
[res, body] = yield request.postAsync {uri: getURL('/auth/spy'), json: {user: @user1.get('name')}}
|
||||
expect(res.statusCode).toBe(401)
|
||||
done()
|
||||
|
||||
it 'does not work for non-admins', utils.wrap (done) ->
|
||||
yield utils.loginUser(@user1)
|
||||
[res, body] = yield request.postAsync {uri: getURL('/auth/spy'), json: {user: @user1.get('name')}}
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
fdescribe 'POST /auth/stop-spying', ->
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([User])
|
||||
@admin = yield utils.initAdmin()
|
||||
@user = yield utils.initUser()
|
||||
yield utils.loginUser(@admin)
|
||||
[res, body] = yield request.postAsync {uri: getURL('/auth/spy'), json: {user: @user.id}}
|
||||
expect(res.statusCode).toBe(200)
|
||||
done()
|
||||
|
||||
it 'it reverts the spying user back to the admin', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {uri: getURL('/auth/whoami'), json: true}
|
||||
expect(body._id).toBe(@user.id)
|
||||
[res, body] = yield request.postAsync {uri: getURL('/auth/stop-spying'), json: true}
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body._id).toBe(@admin.id)
|
||||
[res, body] = yield request.getAsync {uri: getURL('/auth/whoami'), json: true}
|
||||
expect(body._id).toBe(@admin.id)
|
||||
done()
|
|
@ -20,13 +20,12 @@ module.exports = mw =
|
|||
if _.isFunction(options)
|
||||
done = options
|
||||
options = {}
|
||||
options = _.extend({permissions: []}, options)
|
||||
doc = {
|
||||
options = _.extend({
|
||||
permissions: []
|
||||
email: 'user'+_.uniqueId()+'@gmail.com'
|
||||
password: 'password'
|
||||
permissions: options.permissions
|
||||
}
|
||||
user = new User(doc)
|
||||
}, options)
|
||||
user = new User(options)
|
||||
promise = user.save()
|
||||
return promise
|
||||
|
||||
|
|
Loading…
Reference in a new issue