Complete the login feature

This commit is contained in:
Andrew Miner
2015-04-01 13:31:07 -07:00
parent 764f835698
commit ad897b80c0
10 changed files with 253 additions and 60 deletions
+7 -1
View File
@@ -63,7 +63,11 @@ Key.Return = 13
exports.Login = Login = {} exports.Login = Login = {}
Login.authorizeUrl = _.template "https://github.com/login/oauth/authorize" + Login.authorizeUrl = _.template "https://github.com/login/oauth/authorize" +
"?client_id=ea419abd2ab96c708815&scope=public_repo&state=<%= state %>" + "?client_id=<%= clientId %>&scope=public_repo&state=<%= state %>"
Login.clientIds =
'local': 'a2a1c5f1bb2d7bd14ebb'
'staging': '26e1e9a4a702cabe334f'
'production': 'ea419abd2ab96c708815'
exports.ModelState = ModelState = {} exports.ModelState = ModelState = {}
ModelState.unloaded = 'unloaded' ModelState.unloaded = 'unloaded'
@@ -87,10 +91,12 @@ Url.crafting = _.template "/craft/<%= inventoryText %>"
Url.item = _.template "/browse/<%= modSlug %>/<%= itemSlug %>/" Url.item = _.template "/browse/<%= modSlug %>/<%= itemSlug %>/"
Url.itemData = _.template "/browse/<%= modSlug %>/<%= itemSlug %>/item.cg" Url.itemData = _.template "/browse/<%= modSlug %>/<%= itemSlug %>/item.cg"
Url.itemIcon = _.template "/browse/<%= modSlug %>/<%= itemSlug %>/icon.png" Url.itemIcon = _.template "/browse/<%= modSlug %>/<%= itemSlug %>/icon.png"
Url.login = _.template "/login"
Url.mod = _.template "/browse/<%= modSlug %>/" Url.mod = _.template "/browse/<%= modSlug %>/"
Url.modData = _.template "/data/<%= modSlug %>/mod.cg" Url.modData = _.template "/data/<%= modSlug %>/mod.cg"
Url.modIcon = _.template "/browse/<%= modSlug %>/icon.png" Url.modIcon = _.template "/browse/<%= modSlug %>/icon.png"
Url.modVersionData = _.template "/data/<%= modSlug %>/<%= modVersion %>/mod-version.cg" Url.modVersionData = _.template "/data/<%= modSlug %>/<%= modVersion %>/mod-version.cg"
Url.root = _.template "/"
Url.tutorial = _.template "/browse/<%= modSlug %>/tutorials/<%= tutorialSlug %>/" Url.tutorial = _.template "/browse/<%= modSlug %>/tutorials/<%= tutorialSlug %>/"
Url.tutorialData = _.template "/data/<%= modSlug %>/tutorials/<%= tutorialSlug %>.cg" Url.tutorialData = _.template "/data/<%= modSlug %>/tutorials/<%= tutorialSlug %>.cg"
Url.tutorialIcon = _.template "/browse/<%= modSlug %>/tutorials/<%= tutorialSlug %>/icon.png" Url.tutorialIcon = _.template "/browse/<%= modSlug %>/tutorials/<%= tutorialSlug %>/icon.png"
+15 -2
View File
@@ -14,14 +14,12 @@ module.exports = class BaseController extends Backbone.View
constructor: (options={})-> constructor: (options={})->
@tryRefresh = _.debounce @_tryRefresh, 100 @tryRefresh = _.debounce @_tryRefresh, 100
Object.defineProperty this, 'model', get:@getModel, set:@setModel
@_model = null @_model = null
@_rendered = false @_rendered = false
@_parent = options.parent @_parent = options.parent
@_children = [] @_children = []
Object.defineProperty this, 'rendered', get:-> return @_rendered
@_loadTemplate options.templateName @_loadTemplate options.templateName
super options super options
@@ -122,6 +120,21 @@ module.exports = class BaseController extends Backbone.View
@_model = newModel @_model = newModel
@tryRefresh() @tryRefresh()
isRendered: ->
return @_rendered
getUser: ->
return @_user
setUser: (user)->
@_user = user
@tryRefresh()
Object.defineProperties @prototype,
model: {get:@prototype.getModel, set:@prototype.setModel}
rendered: {get:@prototype.isRendered }
user: {get:@prototype.user, set:@prototype.setUser}
# Backbone.View Overrides ###################################################################### # Backbone.View Overrides ######################################################################
events: -> events: ->
@@ -7,17 +7,36 @@ All rights reserved.
BaseController = require './base_controller' BaseController = require './base_controller'
{Duration} = require '../constants' {Duration} = require '../constants'
{Event} = require '../constants'
{ProductionEnvs} = require '../constants' {ProductionEnvs} = require '../constants'
{Url} = require '../constants'
######################################################################################################################## ########################################################################################################################
module.exports = class HeaderController extends BaseController module.exports = class HeaderController extends BaseController
constructor: (options={})-> constructor: (options={})->
if not options.client? then throw new Error 'options.client is required'
super options super options
@client = options.client
@_user = options.user
# Event Methods ################################################################################ # Event Methods ################################################################################
onLoginLinkClicked: (event)->
event.preventDefault()
if @user?
@client.logout()
.then ->
router.user = null
.catch (error)->
logger.error -> "Failed to log out: #{error}"
.done()
else
router.navigate Url.login(), trigger:true
onLogoClicked: -> onLogoClicked: ->
router.navigate '/', trigger:true router.navigate '/', trigger:true
return false return false
@@ -28,6 +47,18 @@ module.exports = class HeaderController extends BaseController
router.navigate $(event.currentTarget).attr('href'), trigger:true router.navigate $(event.currentTarget).attr('href'), trigger:true
return false return false
# Property Methods #############################################################################
getUser: ->
return @_user
setUser: (user)->
@_user = user
@tryRefresh()
Object.defineProperties @prototype,
user: {get:@prototype.getUser, set:@prototype.setUser}
# BaseController Overrides ##################################################################### # BaseController Overrides #####################################################################
render: -> render: ->
@@ -36,6 +67,8 @@ module.exports = class HeaderController extends BaseController
@_rendered = true @_rendered = true
@$navLinks = ($(el) for el in @$('.navBar a')) @$navLinks = ($(el) for el in @$('.navBar a'))
@$loginName = @$('.login .name')
@$loginLink = @$('.login a')
zIndex = @$navLinks.length + 100 zIndex = @$navLinks.length + 100
for $navLink in @$navLinks for $navLink in @$navLinks
@@ -46,6 +79,13 @@ module.exports = class HeaderController extends BaseController
$('body').append "<script async src=\"#{addThisUrl}\"></script>" $('body').append "<script async src=\"#{addThisUrl}\"></script>"
refresh: -> refresh: ->
if @user?
@$loginName.html "Hello #{router.user.login}!"
@$loginLink.html "Logout"
else
@$loginName.html ''
@$loginLink.html 'Login'
for $navLink in @$navLinks for $navLink in @$navLinks
linkPage = $navLink.data('page') linkPage = $navLink.data('page')
if @model is linkPage if @model is linkPage
@@ -64,4 +104,4 @@ module.exports = class HeaderController extends BaseController
return _.extend super, return _.extend super,
'click a.logo': 'onLogoClicked' 'click a.logo': 'onLogoClicked'
'click .navBar a': 'onNavItemClicked' 'click .navBar a': 'onNavItemClicked'
'click .login a': 'routeLinkClick' 'click .login a': 'onLoginLinkClicked'
@@ -5,24 +5,37 @@ Copyright (c) 2015 by Redwood Labs
All rights reserved. All rights reserved.
### ###
PageController = require './page_controller' PageController = require './page_controller'
{Login} = require '../constants' User = require '../models/user'
{CraftingGuideClient} = require 'crafting-guide-common'
{Event} = require '../constants'
{Login} = require '../constants'
{Url} = require '../constants'
######################################################################################################################## ########################################################################################################################
module.exports = class LoginPageController extends PageController module.exports = class LoginPageController extends PageController
@ServerStatus: @State: State =
Up: 'up' ReadyToLogin: 'ready-to-login'
Down: 'down' ServerDown: 'server-down'
InvalidCallback: 'invalid-callback'
FetchingToken: 'fetching-token'
LoggedIn: 'logged-in'
constructor: (options={})-> constructor: (options={})->
options.templateName = 'login_page' options.templateName = 'login_page'
super options
@client = options.client @client = options.client
@params = options.params
@storage = options.storage @storage = options.storage
@_accessTokenLookupFailed = false
@client.on Event.change + ':status', => @tryRefresh()
super options
# Event Methods ################################################################################ # Event Methods ################################################################################
onExpandReadMore: (event)-> onExpandReadMore: (event)->
@@ -42,13 +55,6 @@ module.exports = class LoginPageController extends PageController
setLoginSecurityToken: (value)-> setLoginSecurityToken: (value)->
@storage.store 'loginSecurityToken', value @storage.store 'loginSecurityToken', value
getServerStatus: ->
return @_serverStatus
setServerStatus: (serverStatus)->
@_serverStatus = serverStatus
@tryRefresh()
Object.defineProperties @prototype, Object.defineProperties @prototype,
loginSecurityToken: {get:@prototype.getLoginSecurityToken, set:@prototype.setLoginSecurityToken} loginSecurityToken: {get:@prototype.getLoginSecurityToken, set:@prototype.setLoginSecurityToken}
serverStatus: {get:@prototype.getServerStatus, set:@prototype.setServerStatus} serverStatus: {get:@prototype.getServerStatus, set:@prototype.setServerStatus}
@@ -60,24 +66,28 @@ module.exports = class LoginPageController extends PageController
# BaseController Methods ####################################################################### # BaseController Methods #######################################################################
onWillRender: ->
@_validateServerStatus()
super
onDidRender: -> onDidRender: ->
@$readMoreLink = @$('a.read-more') @$readMoreLink = @$('a.read-more')
@$readMoreContent = @$('.read-more-content') @$readMoreContent = @$('.read-more-content')
@$serviceUpContent = @$('.service-up')
@$serviceDownContent = @$('.service-down') if (not @user?) and (@params?.state is @loginSecurityToken)
@client.completeGitHubLogin code:@params.code
.then (response)->
attributes = response.json.data.user
if attributes?
router.user = new User attributes
router.navigate Url.root(), trigger:true
.catch (error)->
logger.error "Failed to get access token: #{error}"
@_accessTokenLookupFailed = true
@refresh()
.done()
super super
refresh: -> refresh: ->
if @serverStatus is LoginPageController.ServerStatus.Down state = @_computeState()
@$serviceUpContent.addClass 'hidden' @$('.two-thirds-content').addClass 'hidden'
@$serviceDownContent.removeClass 'hidden' @$(".two-thirds-content.#{state}").removeClass 'hidden'
else
@$serviceUpContent.removeClass 'hidden'
@$serviceDownContent.addClass 'hidden'
super super
@@ -90,21 +100,31 @@ module.exports = class LoginPageController extends PageController
# Private Methods ############################################################################## # Private Methods ##############################################################################
_computeState: ->
if @user? then return State.LoggedIn
if @client.status is CraftingGuideClient.Status.Down then return State.ServerDown
if @_accessTokenLookupFailed then return State.InvalidCallback
if @params?.state?
if @params.state is @loginSecurityToken
return State.FetchingToken
else
return State.InvalidCallback
return State.ReadyToLogin
_compeleteLogin: (code)->
@client.completeGitHubLogin code:code
.then (response)=>
router.user = new User response.json.user
.catch (error)->
@_accessTokenLookupFailed = true
@refresh
.done()
_redirectToGitHub: -> _redirectToGitHub: ->
@loginSecurityToken = _.uuid() @loginSecurityToken = _.uuid()
baseUrl = Login.authorizeUrl state:@loginSecurityToken clientId = Login.clientIds[global.env]
window.location.href = baseUrl window.location.href = Login.authorizeUrl clientId:clientId, state:@loginSecurityToken
_validateServerStatus: ->
message = _.uuid()
logger.info "Checking Crafting Guide Server at #{@client.baseUrl}..."
@client.ping message:message
.then (response)=>
if response.json.message is message
@serverStatus = LoginPageController.ServerStatus.Up
logger.info "Crafting Guide Server is up"
else
throw new Error "Invalid server ping response: #{response.json.message} isnt #{message}"
.catch (error)=>
@serverStatus = LoginPageController.ServerStatus.Down
logger.error "Crafting Guide server is down: #{error}"
+46 -7
View File
@@ -20,6 +20,7 @@ ModPageController = require './controllers/mod_page_controller'
TutorialPageController = require './controllers/tutorial_page_controller' TutorialPageController = require './controllers/tutorial_page_controller'
Storage = require './models/storage' Storage = require './models/storage'
UrlParams = require './url_params' UrlParams = require './url_params'
User = require './models/user'
{ProductionEnvs} = require './constants' {ProductionEnvs} = require './constants'
{DefaultMods} = require './constants' {DefaultMods} = require './constants'
{Duration} = require './constants' {Duration} = require './constants'
@@ -35,16 +36,20 @@ module.exports = class CraftingGuideRouter extends Backbone.Router
@_page = null @_page = null
@_pageControllers = {} @_pageControllers = {}
@_lastReported = null @_lastReported = null
@_user = null
super options super options
@client = options.client @client = options.client
@imageLoader = new ImageLoader defaultUrl:'/images/unknown.png' @imageLoader = new ImageLoader defaultUrl:'/images/unknown.png'
@modPack = new ModPack @modPack = new ModPack
@storage = new Storage storage:window.localStorage @storage = new Storage storage:window.localStorage
@_defaultOptions = client:@client, imageLoader:@imageLoader, modPack:@modPack, storage:@storage
@headerController = new HeaderController el:'.view__header' @_defaultOptions = client:@client, imageLoader:@imageLoader, modPack:@modPack, storage:@storage, user:@_user
@on Event.change + ':user', => @_defaultOptions.user = @_user
@headerController = new HeaderController _.extend {el:'.view__header'}, @_defaultOptions
@headerController.render() @headerController.render()
@on Event.change + ':user', => @headerController.user = @user
# Public Methods ############################################################################### # Public Methods ###############################################################################
@@ -60,6 +65,38 @@ module.exports = class CraftingGuideRouter extends Backbone.Router
@modPack.addMod mod @modPack.addMod mod
loadCurrentUser: ->
@client.fetchCurrentUser()
.then (response)=>
userData = response.json?.data?.user
@user = new User userData if userData?
.catch (error)=>
if error.response.statusCode isnt 401
logger.error "Failed to get current user: #{error}"
else
logger.info "User is not logged in"
.done()
# Property Methods #############################################################################
getUser: ->
return @_user
setUser: (newUser)->
oldUser = @_user
return if newUser is oldUser
@_user = newUser
logger.info -> "User changed to: #{newUser}"
@trigger Event.change + ':user', this, oldUser, newUser
@trigger Event.change, this
if @_controller? then @_controller.user = newUser
Object.defineProperties @prototype,
user: {get:@prototype.getUser, set:@prototype.setUser}
# Backbone.Router Overrides #################################################################### # Backbone.Router Overrides ####################################################################
navigate: -> navigate: ->
@@ -118,7 +155,8 @@ module.exports = class CraftingGuideRouter extends Backbone.Router
@_setPage 'craft', controller @_setPage 'craft', controller
route__login: -> route__login: ->
controller = new LoginPageController _.extend {}, @_defaultOptions params = new UrlParams code:{type:'string'}, state:{type:'string'}
controller = new LoginPageController _.extend {params:params}, @_defaultOptions
@_setPage 'login', controller @_setPage 'login', controller
# Deprecated Route Methods ##################################################################### # Deprecated Route Methods #####################################################################
@@ -182,6 +220,7 @@ module.exports = class CraftingGuideRouter extends Backbone.Router
window.scrollTo 0, 0 window.scrollTo 0, 0
controller.user = @user
controller.onWillShow() controller.onWillShow()
controller.$el = $pageContent controller.$el = $pageContent
controller.render() controller.render()
+11 -3
View File
@@ -24,9 +24,9 @@ global.logger = new Logger
switch window.location.hostname switch window.location.hostname
when 'local.crafting-guide.com' when 'local.crafting-guide.com'
global.env = 'development' global.env = 'local'
logger.level = Logger.DEBUG logger.level = Logger.DEBUG
apiBaseUrl = 'http://localhost:8001' apiBaseUrl = 'http://local.crafting-guide.com:8001'
when 'new.crafting-guide.com' when 'new.crafting-guide.com'
global.env = 'staging' global.env = 'staging'
logger.level = Logger.VERBOSE logger.level = Logger.VERBOSE
@@ -38,7 +38,14 @@ switch window.location.hostname
else else
throw new Error "cannot determine the environment of: #{window.location.hostname}" throw new Error "cannot determine the environment of: #{window.location.hostname}"
global.router = new CraftingGuideRouter client:new CraftingGuideClient baseUrl:apiBaseUrl client = _.extend new CraftingGuideClient(baseUrl:apiBaseUrl), Backbone.Events
client.onStatusChanged = (c, oldStatus, newStatus)->
logger.info "Crafting Guide server status changed from #{oldStatus} to #{newStatus}"
client.trigger 'change:status', client, oldStatus, newStatus
client.trigger 'change', client
client.checkStatus()
global.router = new CraftingGuideRouter client:client
global.util = require 'util' global.util = require 'util'
global.views = views global.views = views
global.markdown = global.markdown.markdown global.markdown = global.markdown.markdown
@@ -46,6 +53,7 @@ global.markdown = global.markdown.markdown
global.feedbackController = new FeedbackController el:'.view__feedback' global.feedbackController = new FeedbackController el:'.view__feedback'
feedbackController.render() feedbackController.render()
global.router.loadCurrentUser()
global.router.loadDefaultModPack() global.router.loadDefaultModPack()
logger.info -> "CraftingGuide is ready" logger.info -> "CraftingGuide is ready"
+33
View File
@@ -0,0 +1,33 @@
###
Crafting Guide - user.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
{Url} = require '../constants'
########################################################################################################################
module.exports = class GitHubUser extends BaseModel
constructor: (attributes={}, options={})->
attributes.avatarUrl ?= null
attributes.email ?= null
attributes.login ?= null
attributes.name ?= null
super attributes, options
# Property Methods #############################################################################
isAuthenticated: ->
return @login?
Object.defineProperties @prototype,
authenticated: {get:@prototype.isAuthenticated}
# Object Overrides #############################################################################
toString: ->
return "#{@name} (#{@login})"
+3 -1
View File
@@ -12,7 +12,9 @@
.addthis_sharing_toolbox .addthis_sharing_toolbox
.divider.bottom .divider.bottom
.login: a(href="/login") Login .login
span.name
a(href="/login") Login
.navBar .navBar
a(data-page="home", href="/") a(data-page="home", href="/")
+27 -2
View File
@@ -6,7 +6,7 @@
//- //-
.view__login_page .view__login_page
.two-thirds-content.hideable.service-up .two-thirds-content.hideable.ready-to-login
img.float-right(src="/images/github-login-large.png") img.float-right(src="/images/github-login-large.png")
h1 Login with GitHub h1 Login with GitHub
p. p.
@@ -29,10 +29,35 @@
.clear .clear
.two-thirds-content.hideable.hidden.service-down .two-thirds-content.hideable.hidden.server-down
h1 Sorry! h1 Sorry!
p. p.
Our interactive editing service is down right now, but you can always submit a pull request over at Our interactive editing service is down right now, but you can always submit a pull request over at
the <a href="https://github.com/andrewminer/crafting-guide">Crafting Guide GitHub Repository</a>. We'll have the <a href="https://github.com/andrewminer/crafting-guide">Crafting Guide GitHub Repository</a>. We'll have
the editing service back up soon! the editing service back up soon!
.two-thirds-content.hideable.hidden.invalid-callback
img.float-right(src="/images/github-login-large.png")
h1 Oops!
p.
Sorry about that, but it looks like something went wrong with your GitHub login. Click the button below to
try again, or use the Feedback button on the left to let us know, and we'll try to help.
.actions
button.huge Login with GitHub
.clear
.two-thirds-content.hideable.hidden.fetching-token
img.float-right(src="/images/github-login-large.png")
h1 Completing log in...
p.
Hang on just a second while we finish logging you in...
.clear
.two-thirds-content.hideable.hidden.logged-in
img.float-right(src="/images/github-login-large.png")
h1 All set!
.clear
+7
View File
@@ -66,6 +66,13 @@ All rights reserved.
font-size: $font-size-normal; font-size: $font-size-normal;
font-weight: bold; font-weight: bold;
} }
.name {
color: $color-text-light;
font-family: $font-family-normal;
font-size: $font-size-normal;
margin-right: 2em;
}
} }
} }