Refactor CreateAccountModal out of AuthModal, add class code to signup

This commit is contained in:
Scott Erickson 2016-02-25 15:24:16 -08:00
parent 910953d383
commit ebc98f988f
56 changed files with 1292 additions and 451 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View file

@ -1,6 +1,3 @@
gplusClientID = '800329290710-j9sivplv2gpcdgkrsis9rff3o417mlfa.apps.googleusercontent.com'
# TODO: Move to GPlusHandler
go = (path) -> -> @routeDirectly path, arguments
module.exports = class CocoRouter extends Backbone.Router
@ -196,7 +193,7 @@ module.exports = class CocoRouter extends Backbone.Router
initializeSocialMediaServices: ->
return if application.testing or application.demoing
require('core/services/facebook')()
require('core/services/google')()
application.gplusHandler.loadAPI()
require('core/services/twitter')()
renderLoginButtons: =>
@ -204,22 +201,8 @@ module.exports = class CocoRouter extends Backbone.Router
$('.share-buttons, .partner-badges').addClass('fade-in').delay(10000).removeClass('fade-in', 5000)
setTimeout(FB.XFBML.parse, 10) if FB?.XFBML?.parse # Handles FB login and Like
twttr?.widgets?.load?()
return unless gapi?.plusone?
gapi.plusone.go?() # Handles +1 button
for gplusButton in $('.gplus-login-button')
params = {
callback: 'signinCallback',
clientid: gplusClientID,
cookiepolicy: 'single_host_origin',
scope: 'https://www.googleapis.com/auth/plus.login email',
height: 'short',
}
if gapi.signin?.render
gapi.signin.render(gplusButton, params)
else
console.warn 'Didn\'t have gapi.signin to render G+ login button. (DoNotTrackMe extension?)'
application.gplusHandler.renderLoginButtons()
activateTab: ->
base = _.string.words(document.location.pathname[1..], '/')[0]
$("ul.nav li.#{base}").addClass('active')

View file

@ -1,16 +0,0 @@
module.exports = initializeGoogle = ->
window.onGPlusLoaded = ->
Backbone.Mediator.publish 'auth:gplus-api-loaded', {}
return
window.signinCallback = (authResult) ->
Backbone.Mediator.publish 'auth:logged-in-with-gplus', authResult if authResult.access_token
return
(->
po = document.createElement('script')
po.type = 'text/javascript'
po.async = true
po.src = 'https://apis.google.com/js/client:platform.js?onload=onGPlusLoaded'
s = document.getElementsByTagName('script')[0]
s.parentNode.insertBefore po, s
return
)()

View file

@ -17,6 +17,14 @@ module.exports = FacebookHandler = class FacebookHandler extends CocoClass
'auth:logged-in-with-facebook': 'onFacebookLoggedIn'
loggedIn: false
token: -> @authResponse?.accessToken
fakeFacebookLogin: ->
@onFacebookLoggedIn({
response:
authResponse: { accessToken: '1234' }
})
onFacebookLoggedIn: (e) ->
# user is logged in also when the page first loads, so check to see
@ -28,46 +36,21 @@ module.exports = FacebookHandler = class FacebookHandler extends CocoClass
@loggedIn = true
break
if @waitingForLogin and @loggedIn
@fetchMeForLogin()
@trigger 'logged-into-facebook'
loginThroughFacebook: ->
if @loggedIn
@fetchMeForLogin()
return true
else
FB.login ((response) ->
console.log 'Received FB login response:', response
), scope: 'email'
@waitingForLogin = true
fetchMeForLogin: ->
FB.api('/me', {fields: 'email,last_name,first_name,gender'}, @onReceiveMeInfo)
onReceiveMeInfo: (r) =>
console.log "Got Facebook user info:", r
unless r.email
console.error('could not get data, since no email provided')
return
oldEmail = me.get('email')
me.set('firstName', r.first_name) if r.first_name
me.set('lastName', r.last_name) if r.last_name
me.set('gender', r.gender) if r.gender
me.set('email', r.email) if r.email
me.set('facebookID', r.id) if r.id
Backbone.Mediator.publish 'auth:logging-in-with-facebook', {}
window.tracker?.identify()
beforeID = me.id
me.patch({
error: backboneFailure,
url: "/db/user/#{me.id}?facebookID=#{r.id}&facebookAccessToken=#{@authResponse.accessToken}"
success: (model) ->
window.tracker?.trackEvent 'Facebook Login', category: "Signup", label: 'Facebook'
if model.id is beforeID
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'Facebook'
window.location.reload() if model.get('email') isnt oldEmail
})
destroy: ->
super()
loadPerson: ->
FB.api '/me', {fields: 'email,last_name,first_name,gender'}, (person) =>
attrs = {}
for fbProp, userProp of userPropsToSave
value = person[fbProp]
if value
attrs[userProp] = value
@trigger 'person-loaded', attrs

View file

@ -20,13 +20,27 @@ scope = 'https://www.googleapis.com/auth/plus.login email'
module.exports = GPlusHandler = class GPlusHandler extends CocoClass
constructor: ->
@accessToken = storage.load GPLUS_TOKEN_KEY, false
window.onGPlusLogin = _.bind(@onGPlusLogin, @)
super()
subscriptions:
'auth:logged-in-with-gplus':'onGPlusLogin'
'auth:gplus-api-loaded':'onGPlusLoaded'
token: -> @accessToken?.access_token
onGPlusLoaded: ->
loadAPI: ->
return if @loadedAPI
@loadedAPI = true
(=>
po = document.createElement('script')
po.type = 'text/javascript'
po.async = true
po.src = 'https://apis.google.com/js/client:platform.js?onload=onGPlusLoaded'
s = document.getElementsByTagName('script')[0]
s.parentNode.insertBefore po, s
window.onGPlusLoaded = _.bind(@onLoadAPI, @)
return
)()
onLoadAPI: ->
Backbone.Mediator.publish 'auth:gplus-api-loaded', {}
session_state = null
if @accessToken and me.get('gplusID')
# We need to check the current state, given our access token
@ -39,6 +53,26 @@ module.exports = GPlusHandler = class GPlusHandler extends CocoClass
func = => @trigger 'checked-state'
setTimeout func, 1
renderLoginButtons: ->
return false unless gapi?.plusone?
gapi.plusone.go?() # Handles +1 button
if not gapi.signin?.render
console.warn 'Didn\'t have gapi.signin to render G+ login button. (DoNotTrackMe extension?)'
return
for gplusButton in $('.gplus-login-button')
params = {
callback: 'onGPlusLogin',
clientid: clientID,
cookiepolicy: 'single_host_origin',
scope: 'https://www.googleapis.com/auth/plus.login email',
height: 'short',
}
if gapi.signin?.render
gapi.signin.render(gplusButton, params)
@trigger 'render-login-buttons'
onCheckedSessionState: (@loggedIn) =>
@trigger 'checked-state'
@ -47,9 +81,16 @@ module.exports = GPlusHandler = class GPlusHandler extends CocoClass
'client_id' : clientID
'scope' : scope
gapi.auth.authorize params, @onGPlusLogin
fakeGPlusLogin: ->
@onGPlusLogin({
access_token: '1234'
})
onGPlusLogin: (e) =>
onGPlusLogin: (e) ->
return unless e.access_token
@loggedIn = true
Backbone.Mediator.publish 'auth:logged-in-with-gplus', e
try
# Without removing this, we sometimes get a cross-domain error
d = _.omit(e, 'g-oauth-window')
@ -58,55 +99,29 @@ module.exports = GPlusHandler = class GPlusHandler extends CocoClass
console.error 'Unable to save G+ token key', e
@accessToken = e
@trigger 'logged-in'
@trigger 'logged-into-google'
loginCodeCombat: ->
loadPerson: (options={}) ->
@reloadOnLogin = options.reloadOnLogin
# email and profile data loaded separately
gapi.client.load('plus', 'v1', =>
gapi.client.plus.people.get({userId: 'me'}).execute(@onPersonReceived))
onPersonReceived: (r) =>
attrs = {}
for gpProp, userProp of userPropsToSave
keys = gpProp.split('.')
value = r
for key in keys
value = value[key]
if value and not me.get(userProp)
me.set(userProp, value)
if value
attrs[userProp] = value
newEmail = r.emails?.length and r.emails[0] isnt me.get('email')
return unless newEmail or me.get('anonymous', true)
me.set('email', r.emails[0].value)
@trigger 'person-loaded'
@save()
save: =>
console.debug 'Email, gplusID:', me.get('email'), me.get('gplusID')
return unless me.get('email') and me.get('gplusID')
Backbone.Mediator.publish 'auth:logging-in-with-gplus', {}
gplusID = me.get('gplusID')
window.tracker?.identify()
patch = {}
patch[key] = me.get(key) for gplusKey, key of userPropsToSave
patch._id = beforeID = me.id
patch.email = me.get('email')
wasAnonymous = me.get('anonymous')
@trigger 'logging-into-codecombat'
console.debug('Logging into GPlus.')
me.save(patch, {
patch: true
type: 'PUT'
error: ->
console.warn('Logging into GPlus fail.', arguments)
backboneFailure(arguments...)
url: "/db/user?gplusID=#{gplusID}&gplusAccessToken=#{@accessToken.access_token}"
success: (model) ->
console.info('GPLus login success!')
window.tracker?.trackEvent 'Google Login', category: "Signup"
if model.id is beforeID
window.tracker?.trackEvent 'Finished Signup', label: 'GPlus'
window.location.reload() if wasAnonymous and not model.get('anonymous')
})
if r.emails?.length
attrs.email = r.emails[0].value
@trigger 'person-loaded', attrs
loadFriends: (friendsCallback) ->
return friendsCallback() unless @loggedIn

View file

@ -189,6 +189,14 @@
school_name: "School Name and City"
optional: "optional"
school_name_placeholder: "Example High School, Springfield, IL"
or_sign_up_with: "or sign up with"
connected_gplus_header: "You've successfully connected with Google+!"
connected_gplus_p: "Finish signing up so you can log in with your Google+ account."
gplus_exists: "You already have an account associated with Google+!"
connected_facebook_header: "You've successfully connected with Facebook!"
connected_facebook_p: "Finish signing up so you can log in with your Facebook account."
facebook_exists: "You already have an account associated with Facebook!"
hey_students: "Students, enter the class code from your teacher."
recover:
recover_account_title: "Recover Account"

View file

@ -197,6 +197,34 @@ module.exports = class User extends CocoModel
options.type = 'POST'
@fetch(options)
fetchGPlusUser: (gplusID, options={}) ->
options.data ?= {}
options.data.gplusID = gplusID
options.data.gplusAccessToken = application.gplusHandler.token()
@fetch(options)
loginGPlusUser: (gplusID, options={}) ->
options.url = '/auth/login-gplus'
options.type = 'POST'
options.data ?= {}
options.data.gplusID = gplusID
options.data.gplusAccessToken = application.gplusHandler.token()
@fetch(options)
fetchFacebookUser: (facebookID, options={}) ->
options.data ?= {}
options.data.facebookID = facebookID
options.data.facebookAccessToken = application.facebookHandler.token()
@fetch(options)
loginFacebookUser: (facebookID, options={}) ->
options.url = '/auth/login-facebook'
options.type = 'POST'
options.data ?= {}
options.data.facebookID = facebookID
options.data.facebookAccessToken = application.facebookHandler.token()
@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
]

View file

@ -8,7 +8,7 @@
.modal-dialog
padding: 0
width: 666px
height: 694px
height: 582px
//- Background
@ -143,9 +143,9 @@
//- Primary auth button
#login-button, #signup-button
#login-button
position: absolute
top: 298px
top: 186px
height: 70px
font-size: 32px
line-height: 42px
@ -155,7 +155,7 @@
.auth-network-logins
position: absolute
top: 470px
top: 358px
width: 580px
left: 48px
padding-left: 29px
@ -216,7 +216,7 @@
height: 139px
padding: 23px 23px 23px 168px
position: absolute
top: 520px
top: 408px
.switch-explanation
margin: 25px 10px 0 0
@ -236,20 +236,6 @@
height: 70px
line-height: 40px
//- Login-specific styling
&.login
.modal-dialog
height: 582px
#login-button, #signup-button
top: 186px
.auth-network-logins
top: 358px
.extra-pane
top: 408px
.ie10 #auth-modal .auth-network-logins .btn.btn-lg.network-login .network-logo, .lt-ie10 #auth-modal .auth-network-logins .btn.btn-lg.network-login .network-logo
left: 15px

View file

@ -0,0 +1,246 @@
@import "app/styles/mixins"
@import "app/styles/bootstrap/variables"
#create-account-modal
//- Clear modal defaults
.modal-dialog
padding: 0
width: 666px
height: 694px
//- Background
.auth-modal-background
position: absolute
top: -90px
left: -40px
//- Header
h1
position: absolute
left: 183px
top: 0px
margin: 0
width: 255px
text-align: center
color: rgb(254,188,68)
font-size: 32px
text-shadow: black 2px 2px 0, black -2px -2px 0, black 2px -2px 0, black -2px 2px 0, black 2px 0px 0, black 0px -2px 0, black -2px 0px 0, black 0px 2px 0
&.long-title
top: -14px
//- Close modal button
#close-modal
position: absolute
left: 442px
top: -15px
width: 60px
height: 60px
color: white
text-align: center
font-size: 30px
padding-top: 15px
cursor: pointer
@include rotate(-3deg)
&:hover
color: yellow
//- Modal body content
#gplus-account-exists-row, #facebook-account-exists-row
margin-bottom: 20px
#facebook-signup-btn
margin-bottom: 5px
.auth-form-content
position: absolute
top: 100px
left: 40px
width: 588px
.help-block
margin: 0
.alert
margin-top: -25px
margin-bottom: 0
padding: 10px 15px
.form-group
color: rgb(51,51,51)
padding: 0
margin: 0
.input-border
border: 2px solid rgb(233, 221, 194)
border-radius: 4px
margin-bottom: 7px
input
background-color: rgb(239, 232, 216)
border: 2px solid rgb(26, 21, 18)
border-radius: 4px
label
font-size: 20px
text-transform: uppercase
font-family: $headings-font-family
margin-bottom: 0
.optional-note
font-size: 14px
.well
font-size: 18px
position: absolute
right: 0
bottom: 0
width: 278px
margin-bottom: 0
//- Check boxes
.form-group.checkbox
margin: 10px 0
label
position: relative
line-height: 34px
span:not(.custom-checkbox)
margin-left: 40px
input
display: none
& + .custom-checkbox
.glyphicon
display: none
&:checked + .custom-checkbox .glyphicon
display: inline
color: rgb(248,169,67)
text-align: center
text-shadow: 0 0 3px black, 0 0 3px black, 0 0 3px black
font-size: 20px
position: relative
top: -2px
.input-border
border-radius: 4px
height: 34px
width: 34px
position: absolute
.custom-checkbox
border-radius: 4px
position: absolute
height: 30px
width: 30px
border: 2px solid rgb(26,21,18)
background: rgb(228,217,196)
text-align: center
//- Primary auth button
#signup-button
position: absolute
top: 405px
height: 70px
font-size: 32px
line-height: 42px
//- Footer area
.auth-network-logins
.btn.btn-lg.network-login
width: 251px
height: 60px
text-align: center
position: relative
.network-logo
height: 30px
position: absolute
left: -10px
top: 2px
.sign-in-blurb
line-height: 34px
margin-left: 12px
.fb-login-button
$scaleX: 251 / 64
$scaleY: 60 / 23
transform: scale($scaleX, $scaleY)
position: absolute
top: 4px
left: 74px
@include opacity(0.01)
.gplus-login-wrapper
position: absolute
left: 65px
top: -6px
$scaleX: 251 / 84
$scaleY: 60 / 24
transform: scale($scaleX, $scaleY)
@include opacity(0.01)
#github-login-button
position: relative
top: -1px
border-radius: 5px
img
width: 16px
margin: 0 5px 0 -5px
#gplus-login-button
position: relative
top: 8px
//- Extra bottom pane area
.extra-pane
background-image: url(/images/pages/modal/auth/extra-pane.png)
width: 633px
height: 139px
padding: 23px 23px 23px 168px
position: absolute
top: 580px
.switch-explanation
margin: 25px 10px 0 0
width: 200px
color: rgb(254,188,68)
font-size: 20px
font-family: $headings-font-family
font-weight: bold
text-transform: uppercase
text-shadow: black 1px 1px 0, black -1px -1px 0, black 1px -1px 0, black -1px 1px 0, black 1px 0px 0, black 0px -1px 0, black -1px 0px 0, black 0px 1px 0
float: left
.btn
float: right
margin-top: 20px
width: 230px
height: 70px
line-height: 40px
.ie10 #create-account-modal .auth-network-logins .btn.btn-lg.network-login .network-logo, .lt-ie10 #create-account-modal .auth-network-logins .btn.btn-lg.network-login .network-logo
left: 15px
top: 15px

View file

@ -11,7 +11,7 @@ block content
if me.isAdmin() || !newModelsAdminOnly
if me.get('anonymous')
a.btn.btn-primary.open-modal-button(data-toggle="coco-modal", data-target="core/AuthModal", role="button", data-i18n="#{currentNewSignup}") Log in to Create a New Something
a.btn.btn-primary.open-modal-button(data-toggle="coco-modal", data-target="core/CreateAccountModal", role="button", data-i18n="#{currentNewSignup}") Log in to Create a New Something
else
a.btn.btn-primary.open-modal-button#new-model-button(data-i18n="#{currentNew}") Create a New Something
input#search(data-i18n="[placeholder]#{currentSearch}")

View file

@ -1,87 +1,38 @@
.modal-dialog
.modal-content
if mode === 'login'
img(src="/images/pages/modal/auth/login-background.png", draggable="false").auth-modal-background
else
img(src="/images/pages/modal/auth/signup-background.png", draggable="false").auth-modal-background
if mode === 'login'
h1(data-i18n="login.log_in") Log In
if mode === 'signup'
h1(data-i18n="login.sign_up") Create Account
img(src="/images/pages/modal/auth/login-background.png", draggable="false").auth-modal-background
h1(data-i18n="login.log_in")
div#close-modal
span.glyphicon.glyphicon-remove
.auth-form-content
if showRequiredError
.alert.alert-success
span(data-i18n="signup.required") You need to log in before you can that way.
else if mode === 'signup' && showSignupRationale
.alert.alert-info
span(data-i18n="play_level.victory_sign_up_poke") Want to save your code? Create a free account!
span(data-i18n="signup.required")
form.form
.form-group
label.control-label(for="email")
span(data-i18n="general.email") Email
span(data-i18n="general.email")
| :
.input-border
input#email.input-large.form-control(name="email", type="email", value=formValues.email)
.form-group
if mode === 'login'
div#recover-account-wrapper
a(data-toggle="coco-modal", data-target="core/RecoverModal", data-i18n="login.forgot_password")#link-to-recover Forgot your password?
div#recover-account-wrapper
a(data-toggle="coco-modal", data-target="core/RecoverModal", data-i18n="login.forgot_password")#link-to-recover
label.control-label(for="password")
span(data-i18n="general.password") Password
span(data-i18n="general.password")
| :
.input-border
input#password.input-large.form-control(name="password", type="password", value=formValues.password)
if mode === 'signup'
.row
.col-md-6
.form-group
label.control-label(for="name")
span(data-i18n="general.name") Name
| :
.input-border
if me.get('name')
input#name.input-large.form-control(name="name", type="text", value=me.get('name'))
else
input#name.input-large.form-control(name="name", type="text", value="", placeholder="e.g. Alex W the Skater")
.col-md-6
.form-group
label.control-label(for="school-input")
span.spr(data-i18n="signup.school_name") School Name and City
em.optional-note
| (
span(data-i18n="signup.optional") optional
| ):
.input-border
input#school-input.input-large.form-control(name="schoolName", data-i18n="[placeholder]signup.school_name_placeholder", value=formValues.schoolName || '')
.form-group.checkbox
label.control-label(for="subscribe")
.input-border
input#subscribe(name="subscribe", type="checkbox", checked='checked')
span.custom-checkbox
.glyphicon.glyphicon-ok
span(data-i18n="signup.email_announcements") Receive announcements by email
if mode === 'login'
input.btn.btn-lg.btn-illustrated.btn-block.btn-success#login-button(value=translate("login.log_in"), type="submit")
else if mode === 'signup'
input.btn.btn-lg.btn-illustrated.btn-block.btn-success#signup-button(value=translate("signup.sign_up"), type="submit")
input.btn.btn-lg.btn-illustrated.btn-block.btn-success#login-button(value=translate("login.log_in"), type="submit")
.wait.secret
if mode === 'login'
h3(data-i18n="login.logging_in") Logging In
if mode === 'signup'
h3(data-i18n="signup.creating") Creating Account...
h3(data-i18n="login.logging_in")
.auth-network-logins
// GitHub login complete, but the button does not fit in with the design yet. Hidden for now
//div.network-login
@ -90,17 +41,13 @@
// | GitHub
#facebook-login-btn.btn.btn-primary.btn-lg.btn-illustrated.network-login
img.network-logo(src="/images/pages/community/logo_facebook.png", draggable="false")
span.sign-in-blurb(data-i18n="login.sign_in_with_facebook") Sign in with Facebook
.btn.btn-danger.btn-lg.btn-illustrated.network-login
span.sign-in-blurb(data-i18n="login.sign_in_with_facebook")
#gplus-login-btn.btn.btn-danger.btn-lg.btn-illustrated.network-login(disabled=true)
img.network-logo(src="/images/pages/community/logo_g+.png", draggable="false")
span.sign-in-blurb(data-i18n="login.sign_in_with_gplus") Sign in with G+
span.sign-in-blurb(data-i18n="login.sign_in_with_gplus")
.gplus-login-wrapper
.gplus-login-button#gplus-login-button
.gplus-login-button
.extra-pane
if mode === 'login'
.switch-explanation(data-i18n="login.signup_switch") Want to create an account?
.btn.btn-lg.btn-illustrated.btn-warning#switch-to-signup-button(data-i18n="login.sign_up") Create Account
else if mode === 'signup'
.switch-explanation(data-i18n="signup.login_switch") Already have an account?
.btn.btn-lg.btn-illustrated.btn-warning#switch-to-login-button(data-i18n="login.log_in")
.switch-explanation(data-i18n="login.signup_switch")
.btn.btn-lg.btn-illustrated.btn-warning#switch-to-signup-btn(data-i18n="login.sign_up")

View file

@ -0,0 +1,108 @@
.modal-dialog
.modal-content
img(src="/images/pages/modal/auth/signup-background.png", draggable="false").auth-modal-background
h1(data-i18n="login.sign_up")
div#close-modal
span.glyphicon.glyphicon-remove
.auth-form-content
if view.options.showSignupRationale
.alert.alert-info
span(data-i18n="play_level.victory_sign_up_poke")
#email-password-row.row
.col-md-6
form
.form-group
label.control-label(for="email")
span(data-i18n="general.email")
| :
.input-border
input#email.input-large.form-control(name="email", type="email", value=view.previousFormInputs.email, tabindex=1)
.form-group
label.control-label(for="password")
span(data-i18n="general.password")
| :
.input-border
input#password.input-large.form-control(name="password", type="password", value=view.previousFormInputs.password, tabindex=2)
.col-md-6
.auth-network-logins.text-center
strong(data-i18n="signup.or_sign_up_with")
#facebook-signup-btn.btn.btn-primary.btn-lg.btn-illustrated.network-login
img.network-logo(src="/images/pages/community/logo_facebook.png", draggable="false")
span.sign-in-blurb(data-i18n="login.sign_in_with_facebook")
#gplus-signup-btn.btn.btn-danger.btn-lg.btn-illustrated.network-login(disabled=true)
img.network-logo(src="/images/pages/community/logo_g+.png", draggable="false")
span.sign-in-blurb(data-i18n="login.sign_in_with_gplus")
.gplus-login-wrapper
.gplus-login-button
#gplus-logged-in-row.row.text-center.hide
h2(data-i18n="signup.connected_gplus_header")
p(data-i18n="signup.connected_gplus_p")
#gplus-account-exists-row.row.text-center.hide
h2(data-i18n="signup.gplus_exists")
a.btn.btn-primary#gplus-login-btn(data-i18n="login.log_in")
#facebook-logged-in-row.row.text-center.hide
h2(data-i18n="signup.connected_facebook_header")
p(data-i18n="signup.connected_facebook_p")
#facebook-account-exists-row.row.text-center.hide
h2(data-i18n="signup.facebook_exists")
a.btn.btn-primary#facebook-login-btn(data-i18n="login.log_in")
form
.row
.col-md-6
.form-group
label.control-label(for="name")
span(data-i18n="general.name") Name
| :
.input-border
if me.get('name')
input#name.input-large.form-control(name="name", type="text", value=me.get('name'), tabindex=3)
else
input#name.input-large.form-control(name="name", type="text", value="", placeholder="e.g. Alex W the Skater", tabindex=3)
.form-group
label.control-label(for="school-input")
span.spr(data-i18n="signup.school_name")
em.optional-note
| (
span(data-i18n="signup.optional")
| ):
.input-border
input#school-input.input-large.form-control(name="schoolName", data-i18n="[placeholder]signup.school_name_placeholder", value=view.previousFormInputs.schoolName || '', tabindex=4)
.form-group
label.control-label(for="school-input")
span.spr(data-i18n="courses.class_code")
em.optional-note
| (
span(data-i18n="signup.optional")
| ):
.input-border
input#school-input.input-large.form-control(name="classCode", value=view.previousFormInputs.classCode || '', tabindex=5)
.col-md-6
.form-group.checkbox
label.control-label(for="subscribe")
.input-border
input#subscribe(type="checkbox", checked='checked')
span.custom-checkbox
.glyphicon.glyphicon-ok
span(data-i18n="signup.email_announcements")
.well(data-i18n="signup.hey_students")
input#signup-button.btn.btn-lg.btn-illustrated.btn-block.btn-success(value=translate("signup.sign_up"), type="submit")
.extra-pane
.switch-explanation(data-i18n="signup.login_switch")
.btn.btn-lg.btn-illustrated.btn-warning#switch-to-login-btn(data-i18n="login.log_in")

View file

@ -119,7 +119,7 @@ else
//if me.isAdmin()
// button.btn.settings(data-toggle='coco-modal', data-target='play/modal/PlaySettingsModal', data-i18n="[title]play.settings")
if me.get('anonymous', true)
button.btn.settings(data-toggle='coco-modal', data-target='core/AuthModal', data-i18n="[title]play.settings")
button.btn.settings(data-toggle='coco-modal', data-target='core/CreateAccountModal', data-i18n="[title]play.settings")
.user-status.header-font.picoctf-hide
.user-status-line

View file

@ -65,7 +65,7 @@
h4.friends-header(data-i18n="ladder.friends_playing") Friends Playing
if me.get('anonymous')
div.alert.alert-info
a(data-toggle="coco-modal", data-target="core/AuthModal", data-i18n="ladder.log_in_for_friends") Log in to play with your friends!
a(data-toggle="coco-modal", data-target="core/CreateAccountModal", data-i18n="ladder.log_in_for_friends") Log in to play with your friends!
else
if !onFacebook || !onGPlus

View file

@ -43,4 +43,4 @@ else
button.btn.btn-xs.btn-primary.banner#level-done-button(data-i18n="play_level.done") Done
if me.get('anonymous')
button.btn.btn-xs.btn-primary.banner#control-bar-sign-up-button(data-toggle='coco-modal', data-target='core/AuthModal', data-i18n="signup.sign_up")
button.btn.btn-xs.btn-primary.banner#control-bar-sign-up-button(data-toggle='coco-modal', data-target='core/CreateAccountModal', data-i18n="signup.sign_up")

View file

@ -19,8 +19,8 @@
- var i18nKey = 'game_menu.' + submenu.replace('-', '_');
span(data-i18n='[title]' + i18nKey + '_caption;' + i18nKey + '_tab')
if me.get('anonymous')
li.auth-tab(data-toggle='coco-modal', data-target="core/AuthModal")
a(data-toggle='coco-modal', data-target="core/AuthModal")
li.auth-tab(data-toggle='coco-modal', data-target="core/CreateAccountModal")
a(data-toggle='coco-modal', data-target="core/CreateAccountModal")
span.glyphicon.glyphicon-pencil
span(data-i18n='[title]game_menu.auth_caption;game_menu.auth_tab')

View file

@ -1,5 +1,6 @@
RootView = require 'views/core/RootView'
template = require 'templates/home-view'
CreateAccountModal = require 'views/core/CreateAccountModal'
module.exports = class HomeView extends RootView
id: 'home-view'
@ -40,6 +41,8 @@ module.exports = class HomeView extends RootView
afterInsert: ->
super(arguments...)
modal = new CreateAccountModal()
@openModalView(modal)
justPlaysCourses: ->
# This heuristic could be better, but currently we don't add to me.get('courseInstances') for single-player anonymous intro courses, so they have to beat a level without choosing a hero.

View file

@ -3,6 +3,7 @@ forms = require 'core/forms'
TrialRequest = require 'models/TrialRequest'
TrialRequests = require 'collections/TrialRequests'
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
storage = require 'core/storage'
formSchema = {
@ -108,10 +109,7 @@ module.exports = class RequestQuoteView extends RootView
forms.scrollToFirstError()
onClickEmailExistsLoginLink: ->
modal = new AuthModal({
mode: 'login'
initialValues: { email: @trialRequest.get('properties')?.email }
})
modal = new AuthModal({ initialValues: { email: @trialRequest.get('properties')?.email } })
@openModalView(modal)
onTrialRequestSubmit: ->
@ -119,18 +117,14 @@ module.exports = class RequestQuoteView extends RootView
window.tracker?.trackEvent 'Submit Trial Request', category: 'Teachers', label: 'Trial Request', ['Mixpanel']
onClickLoginButton: ->
modal = new AuthModal({
mode: 'login'
initialValues: { email: @trialRequest.get('properties')?.email }
})
modal = new AuthModal({ initialValues: { email: @trialRequest.get('properties')?.email } })
@openModalView(modal)
window.nextURL = '/courses/teachers' unless @trialRequest.isNew()
onClickSignupButton: ->
props = @trialRequest.get('properties') or {}
me.set('name', props.name)
modal = new AuthModal({
mode: 'signup'
modal = new CreateAccountModal({
initialValues: {
email: props.email
schoolName: props.organization

View file

@ -2,6 +2,7 @@ app = require 'core/application'
AuthModal = require 'views/core/AuthModal'
RootView = require 'views/core/RootView'
template = require 'templates/sales-view'
CreateAccountModal = require 'views/core/CreateAccountModal'
module.exports = class SalesView extends RootView
id: 'sales-view'
@ -20,11 +21,11 @@ module.exports = class SalesView extends RootView
app.router.navigate '/teachers/quote', trigger: true
onClickLogin: (e) ->
@openModalView new AuthModal(mode: 'login') if me.get('anonymous')
@openModalView new AuthModal() if me.get('anonymous')
window.tracker?.trackEvent 'Started Login', category: 'Sales', label: 'Sales Login', ['Mixpanel']
onClickSignup: (e) ->
@openModalView new AuthModal() if me.get('anonymous')
@openModalView new CreateAccountModal() if me.get('anonymous')
window.tracker?.trackEvent 'Started Signup', category: 'Sales', label: 'Sales Create', ['Mixpanel']
logoutRedirectURL: false

View file

@ -1,6 +1,7 @@
AuthModal = require 'views/core/AuthModal'
RootView = require 'views/core/RootView'
template = require 'templates/teachers'
CreateAccountModal = require 'views/core/CreateAccountModal'
module.exports = class TeachersView extends RootView
id: 'teachers-view'
@ -22,11 +23,11 @@ module.exports = class TeachersView extends RootView
application.router.navigate "/courses/teachers", trigger: true
onClickLogin: (e) ->
@openModalView new AuthModal(mode: 'login') if me.get('anonymous')
@openModalView new AuthModal() if me.get('anonymous')
window.tracker?.trackEvent 'Started Signup', category: 'Teachers', label: 'Teachers Login'
onClickSignup: (e) ->
@openModalView new AuthModal() if me.get('anonymous')
@openModalView new CreateAccountModal() if me.get('anonymous')
window.tracker?.trackEvent 'Started Signup', category: 'Teachers', label: 'Teachers Create'
logoutRedirectURL: false

View file

@ -3,7 +3,7 @@ template = require 'templates/account/account-settings-view'
{me} = require 'core/auth'
forms = require 'core/forms'
User = require 'models/User'
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
ConfirmModal = require 'views/editor/modal/ConfirmModal'
{logoutUser, me} = require('core/auth')
@ -27,7 +27,7 @@ module.exports = class AccountSettingsView extends CocoView
afterInsert: ->
super()
@openModalView new AuthModal() if me.get('anonymous')
@openModalView new CreateAccountModal() if me.get('anonymous')
getEmailSubsDict: ->
subs = {}

View file

@ -1,7 +1,7 @@
RootView = require 'views/core/RootView'
template = require 'templates/clans/clan-details'
app = require 'core/application'
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
CocoCollection = require 'collections/CocoCollection'
Campaign = require 'models/Campaign'
Clan = require 'models/Clan'
@ -281,7 +281,7 @@ module.exports = class ClanDetailsView extends RootView
window.open url, '_blank'
onDeleteClan: (e) ->
return @openModalView(new AuthModal()) if me.isAnonymous()
return @openModalView(new CreateAccountModal()) if me.isAnonymous()
return unless window.confirm("Delete Clan?")
options =
url: "/db/clan/#{@clanID}"
@ -312,7 +312,7 @@ module.exports = class ClanDetailsView extends RootView
$('.expand-progress-checkbox').attr('checked', @showExpandedProgress)
onJoinClan: (e) ->
return @openModalView(new AuthModal()) if me.isAnonymous()
return @openModalView(new CreateAccountModal()) if me.isAnonymous()
return unless @clan.loaded
if @clan.get('type') is 'private' and not me.isPremium()
@openModalView new SubscribeModal()

View file

@ -1,5 +1,5 @@
app = require 'core/application'
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
RootView = require 'views/core/RootView'
template = require 'templates/clans/clans'
CocoCollection = require 'collections/CocoCollection'
@ -93,7 +93,7 @@ module.exports = class ClansView extends RootView
)
onClickCreateClan: (e) ->
return @openModalView new AuthModal() if me.isAnonymous()
return @openModalView new CreateAccountModal() if me.isAnonymous()
clanType = if $('.private-clan-checkbox').prop('checked') then 'private' else 'public'
if clanType is 'private' and not me.isPremium()
@openModalView new SubscribeModal()
@ -114,7 +114,7 @@ module.exports = class ClansView extends RootView
console.log 'Invalid name'
onJoinClan: (e) ->
return @openModalView(new AuthModal()) if me.isAnonymous()
return @openModalView(new CreateAccountModal()) if me.isAnonymous()
if clanID = $(e.target).data('id')
options =
url: "/db/clan/#{clanID}/join"
@ -144,7 +144,7 @@ module.exports = class ClansView extends RootView
console.error "No clan ID attached to leave button."
onClickPrivateCheckbox: (e) ->
return @openModalView new AuthModal() if me.isAnonymous()
return @openModalView new CreateAccountModal() if me.isAnonymous()
if $('.private-clan-checkbox').prop('checked') and not me.isPremium()
$('.private-clan-checkbox').attr('checked', false)
@openModalView new SubscribeModal()

View file

@ -1,4 +1,4 @@
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
RootView = require 'views/core/RootView'
{me} = require 'core/auth'
contributorSignupAnonymousTemplate = require 'templates/contribute/contributor_signup_anonymous'
@ -37,7 +37,7 @@ module.exports = class ContributeClassView extends RootView
me.setEmailSubscription subscription+'News', checked
me.patch()
@openModalView new AuthModal() if me.get 'anonymous'
@openModalView new CreateAccountModal() if me.get 'anonymous'
el.parent().find('.saved-notification').finish().show('fast').delay(3000).fadeOut(2000)
contributors: []

View file

@ -4,33 +4,36 @@ template = require 'templates/core/auth'
forms = require 'core/forms'
User = require 'models/User'
application = require 'core/application'
errors = require 'core/errors'
module.exports = class AuthModal extends ModalView
id: 'auth-modal'
template: template
mode: 'signup' # or 'login'
events:
# login buttons
'click #switch-to-signup-button': 'onSignupInstead'
'click #switch-to-login-button': 'onLoginInstead'
'click #switch-to-signup-btn': 'onSignupInstead'
'click #github-login-button': 'onGitHubLoginClicked'
'submit': 'onSubmitForm' # handles both submit buttons
'submit form': 'onSubmitForm' # handles both submit buttons
'keyup #name': 'onNameChange'
'click #gplus-login-button': 'onClickGPlusLogin'
'click #close-modal': 'hide'
'click #gplus-login-btn': 'onClickGPlusLogin'
'click #facebook-login-btn': 'onClickFacebookLoginButton'
'click #close-modal': 'hide'
subscriptions:
'errors:server-error': 'onServerError'
'auth:logging-in-with-facebook': 'onLoggingInWithFacebook'
'auth:facebook-api-loaded': 'onFacebookAPILoaded'
constructor: (options) ->
options ?= {}
@onNameChange = _.debounce @checkNameExists, 500
super options
@mode = options.mode if options.mode
# Initialization
initialize: (options={}) ->
@previousFormInputs = options.initialValues or {}
@listenTo application.gplusHandler, 'logged-into-google', @onGPlusHandlerLoggedIntoGoogle
@listenTo application.gplusHandler, 'person-loaded', @onGPlusPersonLoaded
@listenTo application.gplusHandler, 'render-login-buttons', @onGPlusRenderLoginButtons
@listenTo application.facebookHandler, 'logged-into-facebook', @onFacebookHandlerLoggedIntoFacebook
@listenTo application.facebookHandler, 'person-loaded', @onFacebookPersonLoaded
getRenderData: ->
c = super()
@ -43,35 +46,28 @@ module.exports = class AuthModal extends ModalView
afterRender: ->
super()
@$el.toggleClass('signup', @mode is 'signup').toggleClass('login', @mode is 'login')
@playSound 'game-menu-open'
@$('#facebook-login-btn').attr('disabled', true) if not window.FB?
afterInsert: ->
super()
_.delay (=> application.router.renderLoginButtons()), 500
_.delay (=> $('input:visible:first', @$el).focus()), 500
onSignupInstead: (e) ->
@playSound 'menu-button-click'
@mode = 'signup'
@previousFormInputs = forms.formToObject @$el
@render()
_.delay application.router.renderLoginButtons, 500
onGPlusRenderLoginButtons: ->
@$('#gplus-login-btn').attr('disabled', false)
onLoginInstead: (e) ->
@playSound 'menu-button-click'
@mode = 'login'
@previousFormInputs = forms.formToObject @$el
@render()
_.delay application.router.renderLoginButtons, 500
onFacebookAPILoaded: ->
@$('#facebook-login-btn').attr('disabled', false)
onSignupInstead: (e) ->
CreateAccountModal = require('./CreateAccountModal')
modal = new CreateAccountModal({initialValues: forms.formToObject @$el})
currentView.openModalView(modal)
onSubmitForm: (e) ->
@playSound 'menu-button-click'
e.preventDefault()
if @mode is 'login' then @loginAccount() else @createAccount()
false
loginAccount: ->
forms.clearFormAlerts(@$el)
userObject = forms.formToObject @$el
res = tv4.validateMultiple userObject, User.schema
@ -79,89 +75,73 @@ module.exports = class AuthModal extends ModalView
@enableModalInProgress(@$el) # TODO: part of forms
loginUser userObject, null, window.nextURL
emailCheck: ->
email = $('#email', @$el).val()
filter = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,63}$/i # https://news.ycombinator.com/item?id=5763990
unless filter.test(email)
forms.setErrorToProperty @$el, 'email', 'Please enter a valid email address', true
return false
return true
createAccount: ->
forms.clearFormAlerts(@$el)
return unless @emailCheck()
userObject = forms.formToObject @$el
delete userObject.subscribe
delete userObject.name if userObject.name is ''
delete userObject.schoolName if userObject.schoolName is ''
userObject.name = @suggestedName if @suggestedName
for key, val of me.attributes when key in ['preferredLanguage', 'testGroupNumber', 'dateCreated', 'wizardColor1', 'name', 'music', 'volume', 'emails', 'schoolName']
userObject[key] ?= val
subscribe = @$el.find('#subscribe').prop('checked')
userObject.emails ?= {}
userObject.emails.generalNews ?= {}
userObject.emails.generalNews.enabled = subscribe
res = tv4.validateMultiple userObject, User.schema
return forms.applyErrorsToForm(@$el, res.errors) unless res.valid
Backbone.Mediator.publish "auth:signed-up", {}
window.tracker?.trackEvent 'Finished Signup', label: 'CodeCombat'
@enableModalInProgress(@$el)
createUser userObject, null, window.nextURL
onLoggingInWithFacebook: (e) ->
modal = $('.modal:visible', @$el)
@enableModalInProgress(modal) # TODO: part of forms
onServerError: (e) -> # TODO: work error handling into a separate forms system
@disableModalInProgress(@$el)
checkNameExists: =>
name = $('#name', @$el).val()
return forms.clearFormAlerts(@$el) if name is ''
User.getUnconflictedName name, (newName) =>
forms.clearFormAlerts(@$el)
if name is newName
@suggestedName = undefined
else
@suggestedName = newName
forms.setErrorToProperty @$el, 'name', "That name is taken! How about #{newName}?", true
onGitHubLoginClicked: ->
@playSound 'menu-button-click'
Backbone.Mediator.publish 'auth:log-in-with-github', {}
gplusAuthSteps: [
{ i18n: 'login.authenticate_gplus', done: false }
{ i18n: 'login.load_profile', done: false }
{ i18n: 'login.finishing', done: false }
]
# Google Plus
onClickGPlusLogin: ->
@playSound 'menu-button-click'
step.done = false for step in @gplusAuthSteps
handler = application.gplusHandler
@clickedGPlusLogin = true
@listenToOnce handler, 'logged-in', ->
@gplusAuthSteps[0].done = true
@renderGPlusAuthChecklist()
handler.loginCodeCombat()
@listenToOnce handler, 'person-loaded', ->
@gplusAuthSteps[1].done = true
@renderGPlusAuthChecklist()
onGPlusHandlerLoggedIntoGoogle: ->
return unless @clickedGPlusLogin
application.gplusHandler.loadPerson()
btn = @$('#gplus-login-btn')
btn.find('.sign-in-blurb').text($.i18n.t('login.logging_in'))
btn.attr('disabled', true)
@listenToOnce handler, 'logging-into-codecombat', ->
@gplusAuthSteps[2].done = true
@renderGPlusAuthChecklist()
renderGPlusAuthChecklist: ->
template = require 'templates/core/auth-modal-gplus-checklist'
el = $(template({steps: @gplusAuthSteps}))
el.i18n()
@$el.find('.modal-body:visible').empty().append(el)
@$el.find('.modal-footer').remove()
onGPlusPersonLoaded: (gplusAttrs) ->
existingUser = new User()
existingUser.fetchGPlusUser(gplusAttrs.gplusID, {
success: =>
me.loginGPlusUser(gplusAttrs.gplusID, {
success: -> window.location.reload()
error: @onGPlusLoginError
})
error: @onGPlusLoginError
})
onGPlusLoginError: =>
btn = @$('#gplus-login-btn')
btn.find('.sign-in-blurb').text($.i18n.t('login.sign_in_with_gplus'))
btn.attr('disabled', false)
errors.showNotyNetworkError(arguments...)
# Facebook
onClickFacebookLoginButton: ->
application.facebookHandler.loginThroughFacebook()
@clickedFacebookLogin = true
if application.facebookHandler.loggedIn
@onFacebookHandlerLoggedIntoFacebook()
else
application.facebookHandler.loginThroughFacebook()
onFacebookHandlerLoggedIntoFacebook: ->
return unless @clickedFacebookLogin
application.facebookHandler.loadPerson()
btn = @$('#facebook-login-btn')
btn.find('.sign-in-blurb').text($.i18n.t('login.logging_in'))
btn.attr('disabled', true)
onFacebookPersonLoaded: (facebookAttrs) ->
existingUser = new User()
existingUser.fetchFacebookUser(facebookAttrs.facebookID, {
success: =>
me.loginFacebookUser(facebookAttrs.facebookID, {
success: -> window.location.reload()
error: @onFacebookLoginError
})
error: @onFacebookLoginError
})
onFacebookLoginError: =>
btn = @$('#facebook-login-btn')
btn.find('.sign-in-blurb').text($.i18n.t('login.sign_in_with_facebook'))
btn.attr('disabled', false)
errors.showNotyNetworkError(arguments...)
onHidden: ->
super()

View file

@ -170,12 +170,12 @@ module.exports = class CocoView extends Backbone.View
onClickLoadingErrorLoginButton: (e) ->
e.stopPropagation() # Backbone subviews and superviews will handle this call repeatedly otherwise
AuthModal = require 'views/core/AuthModal'
@openModalView(new AuthModal({mode: 'login'}))
@openModalView(new AuthModal())
onClickLoadingErrorCreateAccountButton: (e) ->
e.stopPropagation()
AuthModal = require 'views/core/AuthModal'
@openModalView(new AuthModal({mode: 'signup'}))
CreateAccountModal = require 'views/core/CreateAccountModal'
@openModalView(new CreateAccountModal({mode: 'signup'}))
onClickLoadingErrorLogoutButton: (e) ->
e.stopPropagation()

View file

@ -0,0 +1,250 @@
ModalView = require 'views/core/ModalView'
template = require 'templates/core/create-account-modal'
{loginUser, createUser, me} = require 'core/auth'
forms = require 'core/forms'
User = require 'models/User'
application = require 'core/application'
Classroom = require 'models/Classroom'
errors = require 'core/errors'
# TODO: Avoid using G+ render buttons to login, login directly instead.
# Form object is split in two in template to avoid having rendered buttons triggered on form submit.
module.exports = class CreateAccountModal extends ModalView
id: 'create-account-modal'
template: template
events:
'submit form': 'onSubmitForm'
'keyup #name': 'onNameChange'
'click #gplus-signup-btn': 'onClickGPlusSignupButton'
'click #gplus-login-btn': 'onClickGPlusLoginButton'
'click #facebook-signup-btn': 'onClickFacebookSignupButton'
'click #facebook-login-btn': 'onClickFacebookLoginButton'
'click #close-modal': 'hide'
'click #switch-to-login-btn': 'onClickSwitchToLoginButton'
subscriptions:
'auth:facebook-api-loaded': 'onFacebookAPILoaded'
# Initialization
initialize: (options={}) ->
@onNameChange = _.debounce(_.bind(@checkNameExists, @), 500)
@previousFormInputs = options.initialValues or {}
@listenTo application.gplusHandler, 'logged-into-google', @onGPlusHandlerLoggedIntoGoogle
@listenTo application.gplusHandler, 'person-loaded', @onGPlusPersonLoaded
@listenTo application.gplusHandler, 'render-login-buttons', @onGPlusRenderLoginButtons
@listenTo application.facebookHandler, 'logged-into-facebook', @onFacebookHandlerLoggedIntoFacebook
@listenTo application.facebookHandler, 'person-loaded', @onFacebookPersonLoaded
afterRender: ->
super()
@playSound 'game-menu-open'
@$('#facebook-signup-btn').attr('disabled', true) if not window.FB?
afterInsert: ->
super()
_.delay (-> application.router.renderLoginButtons()), 500
_.delay (=> $('input:visible:first', @$el).focus()), 500
onGPlusRenderLoginButtons: ->
@$('#gplus-signup-btn').attr('disabled', false)
onFacebookAPILoaded: ->
@$('#facebook-signup-btn').attr('disabled', false)
# User creation
onSubmitForm: (e) ->
e.preventDefault()
@playSound 'menu-button-click'
forms.clearFormAlerts(@$el)
attrs = forms.formToObject @$el
attrs.name = @suggestedName if @suggestedName
_.defaults attrs, me.pick([
'preferredLanguage', 'testGroupNumber', 'dateCreated', 'wizardColor1',
'name', 'music', 'volume', 'emails', 'schoolName'
])
attrs.emails ?= {}
attrs.emails.generalNews ?= {}
attrs.emails.generalNews.enabled = @$el.find('#subscribe').prop('checked')
@classCode = attrs.classCode
delete attrs.classCode
_.assign attrs, @gplusAttrs if @gplusAttrs
_.assign attrs, @facebookAttrs if @facebookAttrs
res = tv4.validateMultiple attrs, User.schema
error = false
if not res.valid
forms.applyErrorsToForm(@$el, res.errors)
error = true
if not _.any([attrs.password, @gplusAttrs, @facebookAttrs])
forms.setErrorToProperty @$el, 'password', 'Required'
error = true
if not forms.validateEmail(attrs.email)
forms.setErrorToProperty @$el, 'email', 'Please enter a valid email address'
error = true
return if error
@$('#signup-button').text($.i18n.t('signup.creating')).attr('disabled', true)
@newUser = new User(attrs)
if @classCode
@signupClassroomPrecheck()
else
@createUser()
signupClassroomPrecheck: ->
classroom = new Classroom()
classroom.fetch({ data: { code: @classCode } })
classroom.once 'sync', @createUser, @
classroom.once 'error', @onClassroomFetchError, @
onClassroomFetchError: ->
@$('#signup-button').text($.i18n.t('signup.sign_up')).attr('disabled', false)
forms.setErrorToProperty(@$el, 'classCode', 'Classroom code not found. Please check with your instructor.')
createUser: ->
options = {}
window.tracker?.identify()
if @gplusAttrs
@newUser.set('_id', me.id)
options.url = "/db/user?gplusID=#{@gplusAttrs.gplusID}&gplusAccessToken=#{application.gplusHandler.accessToken.access_token}"
options.type = 'PUT'
if @facebookAttrs
@newUser.set('_id', me.id)
options.url = "/db/user?facebookID=#{@facebookAttrs.facebookID}&facebookAccessToken=#{application.facebookHandler.authResponse.accessToken}"
options.type = 'PUT'
@newUser.save(null, options)
@newUser.once 'sync', @onUserCreated, @
@newUser.once 'error', @onUserSaveError, @
onUserSaveError: (user, jqxhr) ->
@$('#signup-button').text($.i18n.t('signup.sign_up')).attr('disabled', false)
if _.isObject(jqxhr.responseJSON) and jqxhr.responseJSON.property
error = jqxhr.responseJSON
if jqxhr.status is 409 and error.property is 'name'
@newUser.unset 'name'
return @createUser()
return forms.applyErrorsToForm(@$el, [jqxhr.responseJSON])
errors.showNotyNetworkError(jqxhr)
onUserCreated: ->
Backbone.Mediator.publish "auth:signed-up", {}
if @gplusAttrs
window.tracker?.trackEvent 'Google Login', category: "Signup", label: 'GPlus'
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'GPlus'
else if @facebookAttrs
window.tracker?.trackEvent 'Facebook Login', category: "Signup", label: 'Facebook'
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'Facebook'
else
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'CodeCombat'
if @classCode
url = "/courses?_cc="+@classCode
application.router.navigate(url)
window.location.reload()
# Google Plus
onClickGPlusSignupButton: ->
@clickedGPlusLogin = true
onGPlusHandlerLoggedIntoGoogle: ->
return unless @clickedGPlusLogin
application.gplusHandler.loadPerson()
btn = @$('#gplus-signup-btn')
btn.find('.sign-in-blurb').text($.i18n.t('signup.creating'))
btn.attr('disabled', true)
onGPlusPersonLoaded: (@gplusAttrs) ->
existingUser = new User()
existingUser.fetchGPlusUser(@gplusAttrs.gplusID, {
context: @
complete: ->
@$('#email-password-row').remove()
success: =>
@$('#gplus-account-exists-row').removeClass('hide')
error: (user, jqxhr) =>
if jqxhr.status is 404
@$('#gplus-logged-in-row').toggleClass('hide')
else
errors.showNotyNetworkError(jqxhr)
})
onClickGPlusLoginButton: ->
me.loginGPlusUser(@gplusAttrs.gplusID, {
context: @
success: -> window.location.reload()
error: ->
@$('#gplus-login-btn').text($.i18n.t('login.log_in')).attr('disabled', false)
errors.showNotyNetworkError(arguments...)
})
@$('#gplus-login-btn').text($.i18n.t('login.logging_in')).attr('disabled', true)
# Facebook
onClickFacebookSignupButton: ->
@clickedFacebookLogin = true
if application.facebookHandler.loggedIn
@onFacebookHandlerLoggedIntoFacebook()
else
application.facebookHandler.loginThroughFacebook()
onFacebookHandlerLoggedIntoFacebook: ->
return unless @clickedFacebookLogin
application.facebookHandler.loadPerson()
btn = @$('#facebook-signup-btn')
btn.find('.sign-in-blurb').text($.i18n.t('signup.creating'))
btn.attr('disabled', true)
onFacebookPersonLoaded: (@facebookAttrs) ->
existingUser = new User()
existingUser.fetchFacebookUser(@facebookAttrs.facebookID, {
success: =>
@$('#email-password-row').remove()
@$('#facebook-account-exists-row').removeClass('hide')
error: (model, jqxhr) =>
@$('#email-password-row').remove()
if jqxhr.status is 404
@$('#facebook-logged-in-row').toggleClass('hide')
else
errors.showNotyNetworkError(jqxhr)
})
onClickFacebookLoginButton: ->
me.loginFacebookUser(@facebookAttrs.facebookID, {
context: @
success: -> window.location.reload()
error: =>
@$('#facebook-login-btn').text($.i18n.t('login.log_in')).attr('disabled', false)
errors.showNotyNetworkError(jqxhr)
})
@$('#facebook-login-btn').text($.i18n.t('login.logging_in')).attr('disabled', true)
# Misc
onHidden: ->
super()
@playSound 'game-menu-close'
checkNameExists: ->
name = $('#name', @$el).val()
return forms.clearFormAlerts(@$el) if name is ''
User.getUnconflictedName name, (newName) =>
forms.clearFormAlerts(@$el)
if name is newName
@suggestedName = undefined
else
@suggestedName = newName
forms.setErrorToProperty @$el, 'name', "That name is taken! How about #{newName}?"
onClickSwitchToLoginButton: ->
AuthModal = require('./AuthModal')
modal = new AuthModal({initialValues: forms.formToObject @$el})
currentView.openModalView(modal)

View file

@ -60,7 +60,7 @@ module.exports = class RootView extends CocoView
logoutUser($('#login-email').val())
onClickSignupButton: ->
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
switch @id
when 'home-view'
window.tracker?.trackEvent 'Started Signup', category: 'Homepage', label: 'Homepage'
@ -69,12 +69,12 @@ module.exports = class RootView extends CocoView
window.tracker?.trackEvent 'Started Signup', category: 'World Map', label: 'World Map'
else
window.tracker?.trackEvent 'Started Signup', label: @id
@openModalView new AuthModal {mode: 'signup'}
@openModalView new CreateAccountModal()
onClickLoginButton: ->
AuthModal = require 'views/core/AuthModal'
window.tracker?.trackEvent 'Login', category: 'Homepage', ['Google Analytics'] if @id is 'home-view'
@openModalView new AuthModal {mode: 'login'}
@openModalView new AuthModal()
onClickAnchor: (e) ->
return if @destroyed

View file

@ -2,7 +2,7 @@ ModalView = require 'views/core/ModalView'
template = require 'templates/core/subscribe-modal'
stripeHandler = require 'core/services/stripe'
utils = require 'core/utils'
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
Products = require 'collections/Products'
module.exports = class SubscribeModal extends ModalView
@ -134,7 +134,7 @@ module.exports = class SubscribeModal extends ModalView
onClickPurchaseButton: (e) ->
return unless @basicProduct
@playSound 'menu-button-click'
return @openModalView new AuthModal() if me.get('anonymous')
return @openModalView new CreateAccountModal() if me.get('anonymous')
application.tracker?.trackEvent 'Started subscription purchase'
options = {
description: $.i18n.t('subscribe.stripe_description')
@ -148,7 +148,7 @@ module.exports = class SubscribeModal extends ModalView
onClickSaleButton: (e) ->
@playSound 'menu-button-click'
return @openModalView new AuthModal() if me.get('anonymous')
return @openModalView new CreateAccountModal() if me.get('anonymous')
application.tracker?.trackEvent 'Started 1 year subscription purchase'
discount = @basicProduct.get('amount') * 12 - @yearProduct.get('amount')
discountString = (discount/100).toFixed(2)

View file

@ -49,9 +49,9 @@ module.exports = class CourseDetailsView extends RootView
afterRender: ->
super()
if @supermodel.finished() and @courseComplete and me.isAnonymous() and @options.justBeatLevel
# TODO: Make an intermediate modal that tells them they've finished HoC and has some snazzy stuff for convincing players to sign up instead of just throwing up the bare AuthModal
AuthModal = require 'views/core/AuthModal'
@openModalView new AuthModal showSignupRationale: true
# TODO: Make an intermediate modal that tells them they've finished HoC and has some snazzy stuff for convincing players to sign up instead of just throwing up the bare CreateAccountModal
CreateAccountModal = require 'views/core/CreateAccountModal'
@openModalView new CreateAccountModal showSignupRationale: true
onCourseSync: ->
return if @destroyed

View file

@ -1,5 +1,5 @@
app = require 'core/application'
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
CocoCollection = require 'collections/CocoCollection'
Course = require 'models/Course'
RootView = require 'views/core/RootView'
@ -46,7 +46,7 @@ module.exports = class CourseEnrollView extends RootView
@renderNewPrice()
onClickBuy: (e) ->
return @openModalView new AuthModal() if me.isAnonymous()
return @openModalView new CreateAccountModal() if me.isAnonymous()
if @price is 0
@seats = 9999

View file

@ -1,5 +1,4 @@
app = require 'core/application'
AuthModal = require 'views/core/AuthModal'
RootView = require 'views/core/RootView'
template = require 'templates/courses/courses-view'
StudentLogInModal = require 'views/courses/StudentLogInModal'

View file

@ -1,5 +1,4 @@
app = require 'core/application'
AuthModal = require 'views/core/AuthModal'
CocoCollection = require 'collections/CocoCollection'
Course = require 'models/Course'
CourseInstance = require 'models/CourseInstance'

View file

@ -1,5 +1,5 @@
app = require 'core/application'
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
Classroom = require 'models/Classroom'
CocoCollection = require 'collections/CocoCollection'
Course = require 'models/Course'
@ -77,7 +77,7 @@ module.exports = class PurchaseCoursesView extends RootView
numberOfStudentsIsValid: -> @numberOfStudents > 0 and @numberOfStudents < 100000
onClickPurchaseButton: ->
return @openModalView new AuthModal() if me.isAnonymous()
return @openModalView new CreateAccountModal() if me.isAnonymous()
unless @numberOfStudentsIsValid()
alert("Please enter the maximum number of students needed for your class.")
return

View file

@ -1,5 +1,5 @@
app = require 'core/application'
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
CocoCollection = require 'collections/CocoCollection'
Course = require 'models/Course'
Classroom = require 'models/Classroom'
@ -35,7 +35,7 @@ module.exports = class StudentCoursesView extends RootView
super()
onClickJoinClassButton: (e) ->
return @openModalView new AuthModal() if me.isAnonymous()
return @openModalView new CreateAccountModal() if me.isAnonymous()
@classCode = @$('#classroom-code-input').val()
@joinClass()

View file

@ -1,6 +1,6 @@
ActivateLicensesModal = require 'views/courses/ActivateLicensesModal'
app = require 'core/application'
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
CocoCollection = require 'collections/CocoCollection'
CocoModel = require 'models/CocoModel'
Course = require 'models/Course'
@ -70,7 +70,7 @@ module.exports = class TeacherCoursesView extends RootView
application.tracker?.trackEvent 'Classroom started add students', category: 'Courses', classroomID: classroom.id
onClickCreateNewClassButton: ->
return @openModalView new AuthModal() if me.get('anonymous')
return @openModalView new CreateAccountModal() if me.get('anonymous')
modal = new ClassroomSettingsModal({})
@openModalView(modal)
@listenToOnce modal, 'hide', =>

View file

@ -9,7 +9,7 @@ LevelSetupManager = require 'lib/LevelSetupManager'
ThangType = require 'models/ThangType'
MusicPlayer = require 'lib/surface/MusicPlayer'
storage = require 'core/storage'
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
SubscribeModal = require 'views/core/SubscribeModal'
LeaderboardModal = require 'views/play/modal/LeaderboardModal'
Level = require 'models/Level'
@ -159,7 +159,7 @@ module.exports = class CampaignView extends RootView
@preloadTopHeroes() unless me.get('heroConfig')?.thangType
@$el.find('#campaign-status').delay(4000).animate({top: "-=58"}, 1000) unless @terrain is 'dungeon'
if @terrain and me.get('anonymous') and me.get('lastLevel') is 'shadow-guard' and me.level() < 4
@openModalView new AuthModal supermodel: @supermodel, showSignupRationale: true, mode: 'signup'
@openModalView new CreateAccountModal supermodel: @supermodel, showSignupRationale: true
else if @terrain and me.get('name') and me.get('lastLevel') in ['forgetful-gemsmith', 'signs-and-portents'] and me.level() < 5 and not (me.get('ageRange') in ['18-24', '25-34', '35-44', '45-100']) and not storage.load('sent-parent-email') and not me.isPremium()
@openModalView new ShareProgressModal()
@ -262,7 +262,7 @@ module.exports = class CampaignView extends RootView
return unless @getQueryVariable 'signup'
return if me.get('email')
@endHighlight()
authModal = new AuthModal supermodel: @supermodel
authModal = new CreateAccountModal supermodel: @supermodel
authModal.mode = 'signup'
@openModalView authModal

View file

@ -49,8 +49,8 @@ module.exports = class LadderSubmissionView extends CocoView
@$el.find('.last-submitted').toggle(showLastSubmitted)
showApologeticSignupModal: ->
AuthModal = require 'views/core/AuthModal'
@openModalView(new AuthModal({showRequiredError: true}))
CreateAccountModal = require 'views/core/CreateAccountModal'
@openModalView(new CreateAccountModal({showRequiredError: true}))
rankSession: (e) ->
return unless @session.readyToRank()

View file

@ -1,5 +1,5 @@
ModalView = require 'views/core/ModalView'
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
template = require 'templates/play/level/modal/hero-victory-modal'
Achievement = require 'models/Achievement'
EarnedAchievement = require 'models/EarnedAchievement'
@ -481,7 +481,7 @@ module.exports = class HeroVictoryModal extends ModalView
onClickSignupButton: (e) ->
e.preventDefault()
window.tracker?.trackEvent 'Started Signup', category: 'Play Level', label: 'Hero Victory Modal', level: @level.get('slug')
@openModalView new AuthModal {mode: 'signup'}
@openModalView new CreateAccountModal()
showOffer: (@navigationEventUponCompletion) ->
@$el.find('.modal-footer > *').hide()

View file

@ -1,5 +1,5 @@
ModalView = require 'views/core/ModalView'
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
template = require 'templates/play/level/modal/victory'
{me} = require 'core/auth'
LadderSubmissionView = require 'views/play/common/LadderSubmissionView'
@ -59,7 +59,7 @@ module.exports = class VictoryModal extends ModalView
onClickSignupButton: (e) ->
e.preventDefault()
window.tracker?.trackEvent 'Started Signup', category: 'Play Level', label: 'Victory Modal', level: @level.get('slug')
@openModalView new AuthModal {mode: 'signup'}
@openModalView new CreateAccountModal()
onGameSubmitted: (e) ->
ladderURL = "/play/ladder/#{@level.get('slug')}#my-matches"

View file

@ -1,5 +1,5 @@
ModalView = require 'views/core/ModalView'
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
template = require 'templates/play/menu/game-menu-modal'
submenuViews = [
require 'views/play/menu/SaveLoadView'
@ -80,4 +80,4 @@ module.exports = class GameMenuModal extends ModalView
window.tracker?.trackEvent 'Started Signup', category: 'Play Level', label: 'Game Menu', level: @options.levelID
# TODO: Default already seems to be prevented. Need to be explicit?
e.preventDefault()
@openModalView new AuthModal {mode: 'signup'}
@openModalView new CreateAccountModal()

View file

@ -9,7 +9,7 @@ SpriteBuilder = require 'lib/sprites/SpriteBuilder'
ItemDetailsView = require 'views/play/modal/ItemDetailsView'
Purchase = require 'models/Purchase'
BuyGemsModal = require 'views/play/modal/BuyGemsModal'
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
hasGoneFullScreenOnce = false
@ -574,9 +574,8 @@ module.exports = class InventoryModal extends ModalView
button.removeClass('confirm').text($.i18n.t('play.unlock')) if e.target isnt button[0]
askToSignUp: ->
authModal = new AuthModal supermodel: @supermodel
authModal.mode = 'signup'
return @openModalView authModal
createAccountModal = new CreateAccountModal supermodel: @supermodel
return @openModalView createAccountModal
askToBuyGems: (unlockButton) ->
@$el.find('.unlock-button').popover 'destroy'

View file

@ -7,7 +7,7 @@ SpriteBuilder = require 'lib/sprites/SpriteBuilder'
AudioPlayer = require 'lib/AudioPlayer'
utils = require 'core/utils'
BuyGemsModal = require 'views/play/modal/BuyGemsModal'
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
Purchase = require 'models/Purchase'
LayerAdapter = require 'lib/surface/LayerAdapter'
Lank = require 'lib/surface/Lank'
@ -265,9 +265,8 @@ module.exports = class PlayHeroesModal extends ModalView
button.removeClass('confirm').text($.i18n.t('play.unlock')) if e.target isnt button[0]
askToSignUp: ->
authModal = new AuthModal supermodel: @supermodel
authModal.mode = 'signup'
return @openModalView authModal
createAccountModal = new CreateAccountModal supermodel: @supermodel
return @openModalView createAccountModal
askToBuyGems: (unlockButton) ->
@$el.find('.unlock-button').popover 'destroy'

View file

@ -3,7 +3,7 @@ template = require 'templates/play/modal/play-items-modal'
buyGemsPromptTemplate = require 'templates/play/modal/buy-gems-prompt'
ItemDetailsView = require './ItemDetailsView'
BuyGemsModal = require 'views/play/modal/BuyGemsModal'
AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal'
CocoCollection = require 'collections/CocoCollection'
ThangType = require 'models/ThangType'
@ -213,9 +213,8 @@ module.exports = class PlayItemsModal extends ModalView
button.removeClass('confirm').text($.i18n.t('play.unlock')) if e.target isnt button[0]
askToSignUp: ->
authModal = new AuthModal supermodel: @supermodel
authModal.mode = 'signup'
return @openModalView authModal
createAccountModal = new CreateAccountModal supermodel: @supermodel
return @openModalView createAccountModal
askToBuyGems: (unlockButton) ->
@$el.find('.unlock-button').popover 'destroy'

View file

View file

@ -1,13 +1,15 @@
# Middleware for both authentication and authorization
errors = require '../commons/errors'
utils = require '../lib/utils'
wrap = require 'co-express'
Promise = require 'bluebird'
parse = require '../commons/parse'
request = require 'request'
User = require '../users/User'
utils = require '../lib/utils'
mongoose = require 'mongoose'
User = require '../models/User'
module.exports = {
module.exports =
checkDocumentPermissions: (req, res, next) ->
return next() if req.user?.isAdmin()
if not req.doc.hasPermissionsForMethod(req.user, req.method)
@ -33,6 +35,36 @@ module.exports = {
return next new errors.Forbidden('You do not have permissions necessary.')
next()
loginByGPlus: wrap (req, res) ->
gpID = req.body.gplusID
gpAT = req.body.gplusAccessToken
throw new errors.UnprocessableEntity('gplusID and gplusAccessToken required.') unless gpID and gpAT
url = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=#{gpAT}"
[googleRes, body] = yield request.getAsync(url, {json: true})
idsMatch = gpID is body.id
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
req.logInAsync = Promise.promisify(req.logIn)
yield req.logInAsync(user)
res.status(200).send(user.formatEntity(req))
loginByFacebook: wrap (req, res) ->
fbID = req.body.facebookID
fbAT = req.body.facebookAccessToken
throw new errors.UnprocessableEntity('facebookID and facebookAccessToken required.') unless fbID and fbAT
url = "https://graph.facebook.com/me?access_token=#{fbAT}"
[facebookRes, body] = yield request.getAsync(url, {json: true})
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
req.logInAsync = Promise.promisify(req.logIn)
yield req.logInAsync(user)
res.status(200).send(user.formatEntity(req))
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()
@ -62,5 +94,3 @@ module.exports = {
req.loginAsync = Promise.promisify(req.login)
yield req.loginAsync user
res.status(200).send(user.toObject({req: req}))
}

View file

@ -4,4 +4,5 @@ module.exports =
named: require './named'
patchable: require './patchable'
rest: require './rest'
users: require './users'
versions: require './versions'

View file

@ -0,0 +1,40 @@
errors = require '../commons/errors'
wrap = require 'co-express'
Promise = require 'bluebird'
parse = require '../commons/parse'
request = require 'request'
User = require '../users/User'
module.exports =
fetchByGPlusID: wrap (req, res, next) ->
gpID = req.query.gplusID
gpAT = req.query.gplusAccessToken
next() unless gpID and gpAT
dbq = User.find()
dbq.select(parse.getProjectFromReq(req))
url = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=#{gpAT}"
[googleRes, body] = yield request.getAsync(url, {json: true})
idsMatch = gpID is body.id
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))
fetchByFacebookID: wrap (req, res, next) ->
fbID = req.query.facebookID
fbAT = req.query.facebookAccessToken
next() unless fbID and fbAT
dbq = User.find()
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))

View file

@ -1,6 +1,9 @@
mw = require '../middleware'
module.exports.setup = (app) ->
app.post('/auth/login-facebook', mw.auth.loginByFacebook)
app.post('/auth/login-gplus', mw.auth.loginByGPlus)
app.post('/auth/spy', mw.auth.spy)
app.post('/auth/stop-spying', mw.auth.stopSpying)
@ -19,6 +22,8 @@ module.exports.setup = (app) ->
app.get('/db/article/:handle/patches', mw.patchable.patches(Article))
app.post('/db/article/:handle/watchers', mw.patchable.joinWatchers(Article))
app.delete('/db/article/:handle/watchers', mw.patchable.leaveWatchers(Article))
app.get('/db/user', mw.users.fetchByGPlusID, mw.users.fetchByFacebookID)
app.get '/db/products', require('./db/product').get

View file

@ -248,6 +248,16 @@ 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']

View file

@ -21,6 +21,9 @@ global.tv4 = require 'tv4' # required for TreemaUtils to work
global.jsondiffpatch = require 'jsondiffpatch'
global.stripe = require('stripe')(config.stripe.secretKey)
errors = require './server/commons/errors'
request = require 'request'
Promise = require 'bluebird'
Promise.promisifyAll(request, {multiArgs: true})
productionLogging = (tokens, req, res) ->

View file

@ -1,8 +1,8 @@
require '../common'
User = require '../../../server/users/User'
utils = require '../utils'
_ = require 'lodash'
Promise = require 'bluebird'
nock = require 'nock'
urlLogin = getURL('/auth/login')
urlReset = getURL('/auth/reset')
@ -202,7 +202,77 @@ 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/login-facebook', ->
beforeEach utils.wrap (done) ->
yield utils.clearModels([User])
done()
afterEach ->
nock.cleanAll()
url = getURL('/auth/login-facebook')
it 'takes facebookID and facebookAccessToken and logs the user in', utils.wrap (done) ->
nock('https://graph.facebook.com').get('/me').query({access_token: 'abcd'}).reply(200, { id: '1234' })
yield new User({name: 'someone', facebookID: '1234'}).save()
[res, body] = yield request.postAsync url, { json: { facebookID: '1234', facebookAccessToken: 'abcd' }}
expect(res.statusCode).toBe(200)
done()
it 'returns 422 if no token or id is provided', utils.wrap (done) ->
[res, body] = yield request.postAsync url
expect(res.statusCode).toBe(422)
done()
it 'returns 422 if the token is invalid', utils.wrap (done) ->
nock('https://graph.facebook.com').get('/me').query({access_token: 'abcd'}).reply(400, {})
yield new User({name: 'someone', facebookID: '1234'}).save()
[res, body] = yield request.postAsync url, { json: { facebookID: '1234', facebookAccessToken: 'abcd' }}
expect(res.statusCode).toBe(422)
done()
it 'returns 404 if the user does not already exist', utils.wrap (done) ->
nock('https://graph.facebook.com').get('/me').query({access_token: 'abcd'}).reply(200, { id: '1234' })
[res, body] = yield request.postAsync url, { json: { facebookID: '1234', facebookAccessToken: 'abcd' }}
expect(res.statusCode).toBe(404)
done()
describe 'POST /auth/login-gplus', ->
beforeEach utils.wrap (done) ->
yield utils.clearModels([User])
done()
afterEach ->
nock.cleanAll()
url = getURL('/auth/login-gplus')
it 'takes gplusID and gplusAccessToken and logs the user in', utils.wrap (done) ->
nock('https://www.googleapis.com').get('/oauth2/v2/userinfo').query({access_token: 'abcd'}).reply(200, { id: '1234' })
yield new User({name: 'someone', gplusID: '1234'}).save()
[res, body] = yield request.postAsync url, { json: { gplusID: '1234', gplusAccessToken: 'abcd' }}
expect(res.statusCode).toBe(200)
done()
it 'returns 422 if no token or id is provided', utils.wrap (done) ->
[res, body] = yield request.postAsync url
expect(res.statusCode).toBe(422)
done()
it 'returns 422 if the token is invalid', utils.wrap (done) ->
nock('https://www.googleapis.com').get('/oauth2/v2/userinfo').query({access_token: 'abcd'}).reply(400, {})
yield new User({name: 'someone', gplusID: '1234'}).save()
[res, body] = yield request.postAsync url, { json: { gplusID: '1234', gplusAccessToken: 'abcd' }}
expect(res.statusCode).toBe(422)
done()
it 'returns 404 if the user does not already exist', utils.wrap (done) ->
nock('https://www.googleapis.com').get('/oauth2/v2/userinfo').query({access_token: 'abcd'}).reply(200, { id: '1234' })
[res, body] = yield request.postAsync url, { json: { gplusID: '1234', gplusAccessToken: 'abcd' }}
expect(res.statusCode).toBe(404)
done()
describe 'POST /auth/spy', ->
beforeEach utils.wrap (done) ->

View file

@ -9,46 +9,6 @@ mockAuthEvent =
signedRequest: 'akjsdhfjkhea.3423nkfkdsejnfkd'
status: 'connected'
# Whatev, it's all public info anyway
mockMe =
id: '4301938'
email: 'scott@codecombat.com'
first_name: 'Scott'
gender: 'male'
last_name: 'Erickson'
link: 'https://www.facebook.com/scott.erickson.779'
locale: 'en_US'
name: 'Scott Erickson'
timezone: -7
updated_time: '2014-05-21T04:58:06+0000'
username: 'scott.erickson.779'
verified: true
work: [
{
employer:
id: '167559910060759'
name: 'CodeCombat'
location:
id: '114952118516947'
name: 'San Francisco, California'
start_date: '2013-02-28'
}
{
end_date: '2013-01-31'
employer:
id: '39198748555'
name: 'Skritter'
location:
id: '106109576086811'
name: 'Oberlin, Ohio'
start_date: '2008-06-01'
}
]
window.FB ?= {
api: ->
login: ->
@ -60,24 +20,7 @@ describe 'lib/FacebookHandler.coffee', ->
me.markToRevert()
me.set({_id: '12345'})
spyOn FB, 'api'
facebookHandler = new FacebookHandler()
facebookHandler.loginThroughFacebook()
Backbone.Mediator.publish 'auth:logged-in-with-facebook', mockAuthEvent
expect(FB.api).toHaveBeenCalled()
apiArgs = FB.api.calls.argsFor(0)
expect(apiArgs[0]).toBe('/me')
apiArgs[2](mockMe) # sending the 'response'
request = jasmine.Ajax.requests.mostRecent()
expect(request).toBeDefined()
params = JSON.parse request.params
expect(params.firstName).toBe(mockMe.first_name)
expect(params.lastName).toBe(mockMe.last_name)
expect(params.gender).toBe(mockMe.gender)
expect(params.email).toBe(mockMe.email)
expect(params.facebookID).toBe(mockMe.id)
#expect(request.method).toBe('PATCH') # PATCHes are PUTs until more proxy servers recognize PATCH
expect(request.method).toBe('PUT')
expect(_.string.startsWith(request.url, '/db/user/12345')).toBeTruthy()

View file

@ -2,11 +2,22 @@ AuthModal = require 'views/core/AuthModal'
RecoverModal = require 'views/core/RecoverModal'
describe 'AuthModal', ->
modal = null
beforeEach ->
modal = new AuthModal()
modal.render()
afterEach ->
modal.stopListening()
it 'opens the recover modal when you click the recover link', ->
m = new AuthModal({mode: 'login'})
m.render()
spyOn(m, 'openModalView')
m.$el.find('#link-to-recover').click()
expect(m.openModalView.calls.count()).toEqual(1)
args = m.openModalView.calls.argsFor(0)
spyOn(modal, 'openModalView')
modal.$el.find('#link-to-recover').click()
expect(modal.openModalView.calls.count()).toEqual(1)
args = modal.openModalView.calls.argsFor(0)
expect(args[0] instanceof RecoverModal).toBeTruthy()
it '(demo)', ->
jasmine.demoModal(modal)

View file

@ -1,5 +1,7 @@
CocoView = require 'views/core/CocoView'
User = require 'models/User'
CreateAccountModal = require 'views/core/CreateAccountModal'
AuthModal = require 'views/core/AuthModal'
BlandView = class BlandView extends CocoView
template: ->
@ -56,14 +58,18 @@ describe 'CocoView', ->
it 'shows a login button which opens the AuthModal', ->
button = view.$el.find('.login-btn')
expect(button.length).toBe(3) # including the two in the links section
spyOn(view, 'openModalView').and.callFake (modal) -> expect(modal.mode).toBe('login')
spyOn(view, 'openModalView').and.callFake (modal) ->
expect(modal instanceof AuthModal).toBe(true)
modal.stopListening()
button.click()
expect(view.openModalView).toHaveBeenCalled()
it 'shows a create account button which opens the AuthModal', ->
it 'shows a create account button which opens the CreateAccountModal', ->
button = view.$el.find('#create-account-btn')
expect(button.length).toBe(1)
spyOn(view, 'openModalView').and.callFake (modal) -> expect(modal.mode).toBe('signup')
spyOn(view, 'openModalView').and.callFake (modal) ->
expect(modal instanceof CreateAccountModal).toBe(true)
modal.stopListening()
button.click()
expect(view.openModalView).toHaveBeenCalled()

View file

@ -0,0 +1,210 @@
CreateAccountModal = require 'views/core/CreateAccountModal'
forms = require 'core/forms'
describe 'CreateAccountModal', ->
modal = null
beforeEach ->
modal = new CreateAccountModal()
modal.render()
modal.render = _.noop
jasmine.demoModal(modal)
window.gapi =
client:
load: _.noop
window.FB =
login: _.noop
api: _.noop
afterEach ->
modal.stopListening()
describe 'clicking the save button', ->
it 'fails if nothing is in the form, showing errors for email and password', ->
modal.$('form').each (i, el) -> el.reset()
modal.$('form:first').submit()
expect(jasmine.Ajax.requests.all().length).toBe(0)
expect(modal.$('.has-error').length).toBe(2)
it 'fails if email is missing', ->
modal.$('form').each (i, el) -> el.reset()
forms.objectToForm(modal.$el, { name: 'Name', password: 'xyzzy' })
modal.$('form:first').submit()
expect(jasmine.Ajax.requests.all().length).toBe(0)
expect(modal.$('.has-error').length).toBeTruthy()
it 'signs up if only email and password is provided', ->
modal.$('form').each (i, el) -> el.reset()
forms.objectToForm(modal.$el, { email: 'some@email.com', password: 'xyzzy' })
modal.$('form:first').submit()
requests = jasmine.Ajax.requests.all()
expect(requests.length).toBe(1)
expect(modal.$el.has('.has-warning').length).toBeFalsy()
expect(modal.$('#signup-button').is(':disabled')).toBe(true)
describe 'and a class code is entered', ->
beforeEach ->
modal.$('form').each (i, el) -> el.reset()
forms.objectToForm(modal.$el, { email: 'some@email.com', password: 'xyzzy', classCode: 'qwerty' })
modal.$('form:first').submit()
expect(jasmine.Ajax.requests.all().length).toBe(1)
it 'checks for Classroom existence if a class code was entered', ->
jasmine.demoModal(modal)
request = jasmine.Ajax.requests.mostRecent()
expect(request.url).toBe('/db/classroom?code=qwerty')
it 'has not hidden the close-modal button', ->
expect(modal.$('#close-modal').css('display')).toBe('block')
it 'continues with signup if the Classroom exists', ->
request = jasmine.Ajax.requests.mostRecent()
request.respondWith({status: 200, responseText: JSON.stringify({})})
request = jasmine.Ajax.requests.mostRecent()
expect(request.url).toBe('/db/user')
expect(request.method).toBe('POST')
it 'fails and shows an error if the Classroom does not exist', ->
request = jasmine.Ajax.requests.mostRecent()
request.respondWith({status: 404, responseText: JSON.stringify({})})
expect(jasmine.Ajax.requests.all().length).toBe(1)
expect(modal.$el.has('.has-error').length).toBeTruthy()
describe 'clicking the gplus button', ->
signupButton = null
beforeEach ->
application.gplusHandler.trigger 'render-login-buttons'
signupButton = modal.$('#gplus-signup-btn')
expect(signupButton.attr('disabled')).toBeFalsy()
signupButton.click()
application.gplusHandler.fakeGPlusLogin()
application.gplusHandler.trigger 'person-loaded', { firstName: 'Mr', lastName: 'Bean', gplusID: 'abcd', email: 'some@email.com' }
it 'checks to see if the user already exists in our system', ->
requests = jasmine.Ajax.requests.all()
expect(requests.length).toBe(1)
expect(signupButton.attr('disabled')).toBeTruthy()
describe 'and finding the given person is already a user', ->
beforeEach ->
expect(modal.$('#gplus-account-exists-row').hasClass('hide')).toBe(true)
request = jasmine.Ajax.requests.mostRecent()
request.respondWith({status: 200, responseText: JSON.stringify({_id: 'existinguser'})})
it 'shows a message saying you are connected with Google+, with a button for logging in', ->
expect(modal.$('#gplus-account-exists-row').hasClass('hide')).toBe(false)
loginBtn = modal.$('#gplus-login-btn')
expect(loginBtn.attr('disabled')).toBeFalsy()
loginBtn.click()
expect(loginBtn.attr('disabled')).toBeTruthy()
request = jasmine.Ajax.requests.mostRecent()
expect(request.method).toBe('POST')
expect(request.params).toBe('gplusID=abcd&gplusAccessToken=1234')
expect(request.url).toBe('/auth/login-gplus')
describe 'and the user finishes signup anyway with new info', ->
beforeEach ->
forms.objectToForm(modal.$el, { email: 'some@email.com', schoolName: 'Hogwarts' })
modal.$('form:first').submit()
it 'upserts the values to the new user', ->
request = jasmine.Ajax.requests.mostRecent()
expect(request.method).toBe('PUT')
expect(request.url).toBe('/db/user?gplusID=abcd&gplusAccessToken=1234')
expect(JSON.parse(request.params).schoolName).toBe('Hogwarts')
describe 'and finding the given person is not yet a user', ->
beforeEach ->
expect(modal.$('#gplus-logged-in-row').hasClass('hide')).toBe(true)
request = jasmine.Ajax.requests.mostRecent()
request.respondWith({status: 404})
it 'shows a message saying you are connected with Google+', ->
expect(modal.$('#gplus-logged-in-row').hasClass('hide')).toBe(false)
describe 'and the user finishes signup', ->
beforeEach ->
modal.$('form:first').submit()
it 'creates the user with the gplus attributes', ->
request = jasmine.Ajax.requests.mostRecent()
expect(request.method).toBe('PUT')
expect(request.url).toBe('/db/user?gplusID=abcd&gplusAccessToken=1234')
expect(_.string.startsWith(request.url, '/db/user')).toBe(true)
expect(modal.$('#signup-button').is(':disabled')).toBe(true)
describe 'clicking the facebook button', ->
signupButton = null
beforeEach ->
Backbone.Mediator.publish 'auth:facebook-api-loaded', {}
signupButton = modal.$('#facebook-signup-btn')
expect(signupButton.attr('disabled')).toBeFalsy()
signupButton.click()
application.facebookHandler.fakeFacebookLogin()
application.facebookHandler.trigger 'person-loaded', { firstName: 'Mr', lastName: 'Bean', facebookID: 'abcd', email: 'some@email.com' }
it 'checks to see if the user already exists in our system', ->
requests = jasmine.Ajax.requests.all()
expect(requests.length).toBe(1)
expect(signupButton.attr('disabled')).toBeTruthy()
describe 'and finding the given person is already a user', ->
beforeEach ->
expect(modal.$('#facebook-account-exists-row').hasClass('hide')).toBe(true)
request = jasmine.Ajax.requests.mostRecent()
request.respondWith({status: 200, responseText: JSON.stringify({_id: 'existinguser'})})
it 'shows a message saying you are connected with Facebook, with a button for logging in', ->
expect(modal.$('#facebook-account-exists-row').hasClass('hide')).toBe(false)
loginBtn = modal.$('#facebook-login-btn')
expect(loginBtn.attr('disabled')).toBeFalsy()
loginBtn.click()
expect(loginBtn.attr('disabled')).toBeTruthy()
request = jasmine.Ajax.requests.mostRecent()
expect(request.method).toBe('POST')
expect(request.params).toBe('facebookID=abcd&facebookAccessToken=1234')
expect(request.url).toBe('/auth/login-facebook')
describe 'and the user finishes signup anyway with new info', ->
beforeEach ->
forms.objectToForm(modal.$el, { email: 'some@email.com', schoolName: 'Hogwarts' })
modal.$('form:first').submit()
it 'upserts the values to the new user', ->
request = jasmine.Ajax.requests.mostRecent()
expect(request.method).toBe('PUT')
expect(request.url).toBe('/db/user?facebookID=abcd&facebookAccessToken=1234')
expect(JSON.parse(request.params).schoolName).toBe('Hogwarts')
describe 'and finding the given person is not yet a user', ->
beforeEach ->
expect(modal.$('#facebook-logged-in-row').hasClass('hide')).toBe(true)
request = jasmine.Ajax.requests.mostRecent()
request.respondWith({status: 404})
it 'shows a message saying you are connected with Facebook', ->
expect(modal.$('#facebook-logged-in-row').hasClass('hide')).toBe(false)
describe 'and the user finishes signup', ->
beforeEach ->
modal.$('form:first').submit()
it 'creates the user with the facebook attributes', ->
request = jasmine.Ajax.requests.mostRecent()
expect(request.method).toBe('PUT')
expect(_.string.startsWith(request.url, '/db/user')).toBe(true)
expect(modal.$('#signup-button').is(':disabled')).toBe(true)
expect(request.url).toBe('/db/user?facebookID=abcd&facebookAccessToken=1234')