diff --git a/src/coffee/constants.coffee b/src/coffee/constants.coffee index ff373267c..9b8fdb404 100644 --- a/src/coffee/constants.coffee +++ b/src/coffee/constants.coffee @@ -63,7 +63,11 @@ Key.Return = 13 exports.Login = Login = {} 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 = {} ModelState.unloaded = 'unloaded' @@ -87,10 +91,12 @@ Url.crafting = _.template "/craft/<%= inventoryText %>" Url.item = _.template "/browse/<%= modSlug %>/<%= itemSlug %>/" Url.itemData = _.template "/browse/<%= modSlug %>/<%= itemSlug %>/item.cg" Url.itemIcon = _.template "/browse/<%= modSlug %>/<%= itemSlug %>/icon.png" +Url.login = _.template "/login" Url.mod = _.template "/browse/<%= modSlug %>/" Url.modData = _.template "/data/<%= modSlug %>/mod.cg" Url.modIcon = _.template "/browse/<%= modSlug %>/icon.png" Url.modVersionData = _.template "/data/<%= modSlug %>/<%= modVersion %>/mod-version.cg" +Url.root = _.template "/" Url.tutorial = _.template "/browse/<%= modSlug %>/tutorials/<%= tutorialSlug %>/" Url.tutorialData = _.template "/data/<%= modSlug %>/tutorials/<%= tutorialSlug %>.cg" Url.tutorialIcon = _.template "/browse/<%= modSlug %>/tutorials/<%= tutorialSlug %>/icon.png" diff --git a/src/coffee/controllers/base_controller.coffee b/src/coffee/controllers/base_controller.coffee index e3a50ffb9..d23886f43 100644 --- a/src/coffee/controllers/base_controller.coffee +++ b/src/coffee/controllers/base_controller.coffee @@ -14,14 +14,12 @@ module.exports = class BaseController extends Backbone.View constructor: (options={})-> @tryRefresh = _.debounce @_tryRefresh, 100 - Object.defineProperty this, 'model', get:@getModel, set:@setModel @_model = null @_rendered = false @_parent = options.parent @_children = [] - Object.defineProperty this, 'rendered', get:-> return @_rendered @_loadTemplate options.templateName super options @@ -122,6 +120,21 @@ module.exports = class BaseController extends Backbone.View @_model = newModel @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 ###################################################################### events: -> diff --git a/src/coffee/controllers/header_controller.coffee b/src/coffee/controllers/header_controller.coffee index d48a50499..5d8db72f2 100644 --- a/src/coffee/controllers/header_controller.coffee +++ b/src/coffee/controllers/header_controller.coffee @@ -7,17 +7,36 @@ All rights reserved. BaseController = require './base_controller' {Duration} = require '../constants' +{Event} = require '../constants' {ProductionEnvs} = require '../constants' +{Url} = require '../constants' ######################################################################################################################## module.exports = class HeaderController extends BaseController constructor: (options={})-> + if not options.client? then throw new Error 'options.client is required' super options + @client = options.client + @_user = options.user + # 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: -> router.navigate '/', trigger:true return false @@ -28,6 +47,18 @@ module.exports = class HeaderController extends BaseController router.navigate $(event.currentTarget).attr('href'), trigger:true return false + # Property Methods ############################################################################# + + getUser: -> + return @_user + + setUser: (user)-> + @_user = user + @tryRefresh() + + Object.defineProperties @prototype, + user: {get:@prototype.getUser, set:@prototype.setUser} + # BaseController Overrides ##################################################################### render: -> @@ -36,6 +67,8 @@ module.exports = class HeaderController extends BaseController @_rendered = true @$navLinks = ($(el) for el in @$('.navBar a')) + @$loginName = @$('.login .name') + @$loginLink = @$('.login a') zIndex = @$navLinks.length + 100 for $navLink in @$navLinks @@ -46,6 +79,13 @@ module.exports = class HeaderController extends BaseController $('body').append "" refresh: -> + if @user? + @$loginName.html "Hello #{router.user.login}!" + @$loginLink.html "Logout" + else + @$loginName.html '' + @$loginLink.html 'Login' + for $navLink in @$navLinks linkPage = $navLink.data('page') if @model is linkPage @@ -64,4 +104,4 @@ module.exports = class HeaderController extends BaseController return _.extend super, 'click a.logo': 'onLogoClicked' 'click .navBar a': 'onNavItemClicked' - 'click .login a': 'routeLinkClick' + 'click .login a': 'onLoginLinkClicked' diff --git a/src/coffee/controllers/login_page_controller.coffee b/src/coffee/controllers/login_page_controller.coffee index 5e5e10bfa..a62f7daf1 100644 --- a/src/coffee/controllers/login_page_controller.coffee +++ b/src/coffee/controllers/login_page_controller.coffee @@ -5,24 +5,37 @@ Copyright (c) 2015 by Redwood Labs All rights reserved. ### -PageController = require './page_controller' -{Login} = require '../constants' +PageController = require './page_controller' +User = require '../models/user' +{CraftingGuideClient} = require 'crafting-guide-common' +{Event} = require '../constants' +{Login} = require '../constants' +{Url} = require '../constants' ######################################################################################################################## module.exports = class LoginPageController extends PageController - @ServerStatus: - Up: 'up' - Down: 'down' + @State: State = + ReadyToLogin: 'ready-to-login' + ServerDown: 'server-down' + InvalidCallback: 'invalid-callback' + FetchingToken: 'fetching-token' + LoggedIn: 'logged-in' constructor: (options={})-> options.templateName = 'login_page' - super options @client = options.client + @params = options.params @storage = options.storage + @_accessTokenLookupFailed = false + + @client.on Event.change + ':status', => @tryRefresh() + + super options + # Event Methods ################################################################################ onExpandReadMore: (event)-> @@ -42,13 +55,6 @@ module.exports = class LoginPageController extends PageController setLoginSecurityToken: (value)-> @storage.store 'loginSecurityToken', value - getServerStatus: -> - return @_serverStatus - - setServerStatus: (serverStatus)-> - @_serverStatus = serverStatus - @tryRefresh() - Object.defineProperties @prototype, loginSecurityToken: {get:@prototype.getLoginSecurityToken, set:@prototype.setLoginSecurityToken} serverStatus: {get:@prototype.getServerStatus, set:@prototype.setServerStatus} @@ -60,24 +66,28 @@ module.exports = class LoginPageController extends PageController # BaseController Methods ####################################################################### - onWillRender: -> - @_validateServerStatus() - super - onDidRender: -> - @$readMoreLink = @$('a.read-more') - @$readMoreContent = @$('.read-more-content') - @$serviceUpContent = @$('.service-up') - @$serviceDownContent = @$('.service-down') + @$readMoreLink = @$('a.read-more') + @$readMoreContent = @$('.read-more-content') + + 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 refresh: -> - if @serverStatus is LoginPageController.ServerStatus.Down - @$serviceUpContent.addClass 'hidden' - @$serviceDownContent.removeClass 'hidden' - else - @$serviceUpContent.removeClass 'hidden' - @$serviceDownContent.addClass 'hidden' + state = @_computeState() + @$('.two-thirds-content').addClass 'hidden' + @$(".two-thirds-content.#{state}").removeClass 'hidden' super @@ -90,21 +100,31 @@ module.exports = class LoginPageController extends PageController # 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: -> @loginSecurityToken = _.uuid() - baseUrl = Login.authorizeUrl state:@loginSecurityToken - window.location.href = baseUrl - - _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}" + clientId = Login.clientIds[global.env] + window.location.href = Login.authorizeUrl clientId:clientId, state:@loginSecurityToken diff --git a/src/coffee/crafting_guide_router.coffee b/src/coffee/crafting_guide_router.coffee index 549d54632..9c61c4821 100644 --- a/src/coffee/crafting_guide_router.coffee +++ b/src/coffee/crafting_guide_router.coffee @@ -20,6 +20,7 @@ ModPageController = require './controllers/mod_page_controller' TutorialPageController = require './controllers/tutorial_page_controller' Storage = require './models/storage' UrlParams = require './url_params' +User = require './models/user' {ProductionEnvs} = require './constants' {DefaultMods} = require './constants' {Duration} = require './constants' @@ -35,16 +36,20 @@ module.exports = class CraftingGuideRouter extends Backbone.Router @_page = null @_pageControllers = {} @_lastReported = null + @_user = null super options - @client = options.client - @imageLoader = new ImageLoader defaultUrl:'/images/unknown.png' - @modPack = new ModPack - @storage = new Storage storage:window.localStorage - @_defaultOptions = client:@client, imageLoader:@imageLoader, modPack:@modPack, storage:@storage + @client = options.client + @imageLoader = new ImageLoader defaultUrl:'/images/unknown.png' + @modPack = new ModPack + @storage = new Storage storage:window.localStorage - @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() + @on Event.change + ':user', => @headerController.user = @user # Public Methods ############################################################################### @@ -60,6 +65,38 @@ module.exports = class CraftingGuideRouter extends Backbone.Router @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 #################################################################### navigate: -> @@ -118,7 +155,8 @@ module.exports = class CraftingGuideRouter extends Backbone.Router @_setPage 'craft', controller 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 # Deprecated Route Methods ##################################################################### @@ -182,6 +220,7 @@ module.exports = class CraftingGuideRouter extends Backbone.Router window.scrollTo 0, 0 + controller.user = @user controller.onWillShow() controller.$el = $pageContent controller.render() diff --git a/src/coffee/main.coffee b/src/coffee/main.coffee index 2611b6f70..e306d950c 100644 --- a/src/coffee/main.coffee +++ b/src/coffee/main.coffee @@ -24,9 +24,9 @@ global.logger = new Logger switch window.location.hostname when 'local.crafting-guide.com' - global.env = 'development' + global.env = 'local' logger.level = Logger.DEBUG - apiBaseUrl = 'http://localhost:8001' + apiBaseUrl = 'http://local.crafting-guide.com:8001' when 'new.crafting-guide.com' global.env = 'staging' logger.level = Logger.VERBOSE @@ -38,7 +38,14 @@ switch window.location.hostname else 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.views = views global.markdown = global.markdown.markdown @@ -46,6 +53,7 @@ global.markdown = global.markdown.markdown global.feedbackController = new FeedbackController el:'.view__feedback' feedbackController.render() +global.router.loadCurrentUser() global.router.loadDefaultModPack() logger.info -> "CraftingGuide is ready" diff --git a/src/coffee/models/user.coffee b/src/coffee/models/user.coffee new file mode 100644 index 000000000..0d085bc2e --- /dev/null +++ b/src/coffee/models/user.coffee @@ -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})" diff --git a/src/jade/includes/_header.jade b/src/jade/includes/_header.jade index 9f9cae15b..1ac8ce3dc 100644 --- a/src/jade/includes/_header.jade +++ b/src/jade/includes/_header.jade @@ -12,7 +12,9 @@ .addthis_sharing_toolbox .divider.bottom - .login: a(href="/login") Login + .login + span.name + a(href="/login") Login .navBar a(data-page="home", href="/") diff --git a/src/jade/templates/login_page.jade b/src/jade/templates/login_page.jade index 65ffc180d..addd6bd1e 100644 --- a/src/jade/templates/login_page.jade +++ b/src/jade/templates/login_page.jade @@ -6,7 +6,7 @@ //- .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") h1 Login with GitHub p. @@ -29,10 +29,35 @@ .clear - .two-thirds-content.hideable.hidden.service-down + .two-thirds-content.hideable.hidden.server-down h1 Sorry! p. Our interactive editing service is down right now, but you can always submit a pull request over at the Crafting Guide GitHub Repository. We'll have 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 diff --git a/src/scss/views.scss b/src/scss/views.scss index b12c2b49d..4af1215e7 100644 --- a/src/scss/views.scss +++ b/src/scss/views.scss @@ -66,6 +66,13 @@ All rights reserved. font-size: $font-size-normal; font-weight: bold; } + + .name { + color: $color-text-light; + font-family: $font-family-normal; + font-size: $font-size-normal; + margin-right: 2em; + } } }