Merge branch 'andrewminer-github-login'

This commit is contained in:
Andrew Miner
2015-04-02 11:04:59 -07:00
19 changed files with 462 additions and 23 deletions
Binary file not shown.
+2 -1
View File
@@ -31,6 +31,7 @@ serveIndex = (request, response)->
app.get '*/$', serveIndex
app.get '/craft/*', serveIndex
app.get '/login', serveIndex
app.get '*', (request, response)->
extension = path.extname(request.path)
@@ -49,5 +50,5 @@ app.get '*', (request, response)->
response.set 'Content-Type', contentType
response.sendFile request.path, root:ROOT
server = app.listen 8000, ->
server = app.listen 8000, '127.0.0.1', ->
console.log "listening at: #{server.address().address}:#{server.address().port}"
+16 -6
View File
@@ -61,6 +61,20 @@ Event.transitionEnd = (->
exports.Key = Key = {}
Key.Return = 13
exports.Login = Login = {}
Login.authorizeUrl = _.template "https://github.com/login/oauth/authorize" +
"?client_id=<%= clientId %>&scope=public_repo&state=<%= state %>"
Login.clientIds =
'local': 'a2a1c5f1bb2d7bd14ebb'
'staging': '26e1e9a4a702cabe334f'
'production': 'ea419abd2ab96c708815'
exports.ModelState = ModelState = {}
ModelState.unloaded = 'unloaded'
ModelState.loading = 'loading'
ModelState.loaded = 'loaded'
ModelState.failed = 'failed'
exports.Opacity = Opacity = {}
Opacity.hidden = 1e-6
Opacity.shown = 1
@@ -69,12 +83,6 @@ exports.ProductionEnvs = [ 'staging', 'production' ]
exports.RequiredMods = [ 'minecraft' ]
exports.ModelState = ModelState = {}
ModelState.unloaded = 'unloaded'
ModelState.loading = 'loading'
ModelState.loaded = 'loaded'
ModelState.failed = 'failed'
exports.Text = Text = {}
Text.title = 'The Ultimate Minecraft Crafting Guide'
@@ -83,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"
+15 -2
View File
@@ -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: ->
@@ -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 "<script async src=\"#{addThisUrl}\"></script>"
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,3 +104,4 @@ module.exports = class HeaderController extends BaseController
return _.extend super,
'click a.logo': 'onLogoClicked'
'click .navBar a': 'onNavItemClicked'
'click .login a': 'onLoginLinkClicked'
@@ -0,0 +1,130 @@
###
Crafting Guide - login_page_controller.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
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
@State: State =
ReadyToLogin: 'ready-to-login'
ServerDown: 'server-down'
InvalidCallback: 'invalid-callback'
FetchingToken: 'fetching-token'
LoggedIn: 'logged-in'
constructor: (options={})->
options.templateName = 'login_page'
@client = options.client
@params = options.params
@storage = options.storage
@_accessTokenLookupFailed = false
@client.on Event.change + ':status', => @tryRefresh()
super options
# Event Methods ################################################################################
onExpandReadMore: (event)->
event.preventDefault()
@$readMoreLink.addClass 'hidden'
@$readMoreContent.removeClass 'closed'
onLoginButtonClicked: (event)->
event.preventDefault()
@_redirectToGitHub()
# Property Methods #############################################################################
getLoginSecurityToken: ->
@storage.load 'loginSecurityToken'
setLoginSecurityToken: (value)->
@storage.store 'loginSecurityToken', value
Object.defineProperties @prototype,
loginSecurityToken: {get:@prototype.getLoginSecurityToken, set:@prototype.setLoginSecurityToken}
serverStatus: {get:@prototype.getServerStatus, set:@prototype.setServerStatus}
# PageController Overrides #####################################################################
getTitle: ->
return "Login"
# BaseController Methods #######################################################################
onDidRender: ->
@$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: ->
state = @_computeState()
@$('.two-thirds-content').addClass 'hidden'
@$(".two-thirds-content.#{state}").removeClass 'hidden'
super
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'click a.read-more': 'onExpandReadMore'
'click button': 'onLoginButtonClicked'
# 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()
clientId = Login.clientIds[global.env]
window.location.href = Login.authorizeUrl clientId:clientId, state:@loginSecurityToken
+48 -2
View File
@@ -13,12 +13,14 @@ HomePageController = require './controllers/home_page_controller'
ImageLoader = require './controllers/image_loader'
ItemPageController = require './controllers/item_page_controller'
ItemSlug = require './models/item_slug'
LoginPageController = require './controllers/login_page_controller'
Mod = require './models/mod'
ModPack = require './models/mod_pack'
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'
@@ -34,15 +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 = 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()
@on Event.change + ':user', => @headerController.user = @user
# Public Methods ###############################################################################
@@ -58,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: ->
@@ -73,6 +112,7 @@ module.exports = class CraftingGuideRouter extends Backbone.Router
'configure(/)': 'route__configure'
'craft(/)': 'route__craft'
'craft/:text': 'route__craft'
'login(/)': 'route__login'
'item/:itemSlug': 'deprecated__item'
'crafting/(:text)': 'deprecated__crafting'
@@ -114,6 +154,11 @@ module.exports = class CraftingGuideRouter extends Backbone.Router
controller.model.params = inventoryText:text
@_setPage 'craft', controller
route__login: ->
params = new UrlParams code:{type:'string'}, state:{type:'string'}
controller = new LoginPageController _.extend {params:params}, @_defaultOptions
@_setPage 'login', controller
# Deprecated Route Methods #####################################################################
deprecated__crafting: (text)->
@@ -175,6 +220,7 @@ module.exports = class CraftingGuideRouter extends Backbone.Router
window.scrollTo 0, 0
controller.user = @user
controller.onWillShow()
controller.$el = $pageContent
controller.render()
+17 -3
View File
@@ -11,6 +11,7 @@ require './polyfill'
CraftingGuideRouter = require './crafting_guide_router'
FeedbackController = require './controllers/feedback_controller'
views = require './views'
{CraftingGuideClient} = require 'crafting-guide-common'
{Logger} = require 'crafting-guide-common'
########################################################################################################################
@@ -22,17 +23,29 @@ if typeof(global) is 'undefined'
global.logger = new Logger
switch window.location.hostname
when 'localhost'
global.env = 'development'
when 'local.crafting-guide.com'
global.env = 'local'
logger.level = Logger.DEBUG
apiBaseUrl = 'http://local.crafting-guide.com:8001'
when 'new.crafting-guide.com'
global.env = 'staging'
logger.level = Logger.VERBOSE
apiBaseUrl = 'https://crafting-guide-production.herokuapp.com'
when 'crafting-guide.com'
global.env = 'production'
logger.level = Logger.INFO
apiBaseUrl = 'https://crafting-guide-production.herokuapp.com'
else
throw new Error "cannot determine the environment of: #{window.location.hostname}"
global.router = new CraftingGuideRouter
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
@@ -40,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"
+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})"
+2
View File
@@ -31,3 +31,5 @@ _.mixin
parts = [ null, parts[0] ]
return parts
_.mixin require('crafting-guide-common').stringMixins
+3
View File
@@ -12,6 +12,9 @@
.addthis_sharing_toolbox
.divider.bottom
.login
span.name
a(href="/login") Login
.navBar
a(data-page="home", href="/")
+63
View File
@@ -0,0 +1,63 @@
//-
//- Crafting Guide - login_page.jade
//-
//- Copyright (c) 2015 by Redwood Labs
//- All rights reserved.
//-
.view__login_page
.two-thirds-content.hideable.ready-to-login
img.float-right(src="/images/github-login-large.png")
h1 Login with GitHub
p.
Use your GitHub credentials to login and make changes on Crafting Guide. No GitHub account? Don't worry, you
can get one for free. <a class="read-more" href="#">read more...</a>
.read-more-content.slideable.closed
p.
All changes you make on the site get submitted directly to our GitHub repo, and then published through
the same automated deployment process as changes made by anyone else. That way, anyone can contribute
easily: whether developer or end user.
p.
To log in, we redirect you to GitHub's site, and once you're signed in, they'll send you back here. We
never see your credentials, and only get the minimum set of permissions necessary to submit changes to
the Crafting Guide repo on your behalf.
p.
GitHub offers free accounts, so even if you don't already have one, you've got nothing to lose!
.actions
button.huge Login with GitHub
.clear
.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 <a href="https://github.com/andrewminer/crafting-guide">Crafting Guide GitHub Repository</a>. 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
+24
View File
@@ -19,6 +19,10 @@ All rights reserved.
text-align: center;
}
.clear {
clear: both;
}
.content {
background: $color-content;
box-shadow: 10px 10px 20px #666;
@@ -32,6 +36,11 @@ All rights reserved.
.error-new { background: $color-error-new !important; }
.float-right {
float: right;
margin-left: 3em;
}
.hideable {
opacity: 1;
transition: opacity $animate-normal;
@@ -111,6 +120,21 @@ All rights reserved.
}
}
.slideable {
max-height: 500px;
overflow: hidden;
transition: max-height $animate-normal;
&.closed {
max-height: 0;
}
}
.two-thirds-content {
position: relative; width: 66%;
margin: 5em 16.5%;
}
.videos {
.panel {
text-align: center;
+1
View File
@@ -77,6 +77,7 @@ $color-footer-text: #FFFFFF;
$color-link: #0000FF;
$color-link-subtle: #545A99;
$color-text: #000000;
$color-text-light: #FFFFFF;
// Z-Index Layers //////////////////////////////////////////////////////////////////////////////////
+5
View File
@@ -29,6 +29,11 @@ button {
margin: 2em 0;
padding: 0.5em 0.5em;
}
&.huge {
font-size: $font-size-large;
padding: 0.5em 3em;
}
}
h1 {
+1
View File
@@ -17,6 +17,7 @@ All rights reserved.
@import 'inventory_table';
@import 'item';
@import 'item_page';
@import 'login_page';
@import 'minimal_recipe';
@import 'mod';
@import 'mod_pack';
+33
View File
@@ -0,0 +1,33 @@
/*
Crafting Guide - login_page.scss
Copyright (C) 2015 by Redwood Labs
All rights reserved.
*/
.view__login_page {
button {
outline: none;
}
h1 {
font-family: $font-family-header;
font-size: $font-size-x-large;
padding-top: 2em;
}
p {
margin-bottom: 1em;
}
.actions {
margin-top: 4em;
width: 100%;
text-align: center;
}
.read-more {
font-size: 75%;
}
}
+19
View File
@@ -55,6 +55,25 @@ All rights reserved.
.divider {
background: $color-brick-dark url('/images/stone_brick.png');
position: absolute; bottom: 5em; height: 3.2em; width: 100%;
.login {
position: absolute; top: 50%; right: 2em;
@include transform(translateY(-50%));
a {
color: $color-text-light;
font-family: $font-family-normal;
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;
}
}
}
.navBar {
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB