diff --git a/app/Router.coffee b/app/Router.coffee index ca6af592b..5c873dd28 100644 --- a/app/Router.coffee +++ b/app/Router.coffee @@ -63,6 +63,8 @@ module.exports = class CocoRouter extends Backbone.Router 'file/*path': 'routeToServer' + 'github/*path': 'routeToServer' + 'legal': go('LegalView') 'multiplayer': go('MultiplayerView') diff --git a/app/application.coffee b/app/application.coffee index 4eb829c88..17cb27682 100644 --- a/app/application.coffee +++ b/app/application.coffee @@ -1,6 +1,7 @@ FacebookHandler = require 'lib/FacebookHandler' GPlusHandler = require 'lib/GPlusHandler' LinkedInHandler = require 'lib/LinkedInHandler' +GitHubHandler = require 'lib/GitHubHandler' locale = require 'locale/locale' {me} = require 'lib/auth' Tracker = require 'lib/Tracker' @@ -36,9 +37,11 @@ preload = (arrayOfImages) -> Application = initialize: -> Router = require('Router') + @isProduction = -> document.location.href.search('codecombat.com') isnt -1 @tracker = new Tracker() @facebookHandler = new FacebookHandler() @gplusHandler = new GPlusHandler() + @githubHandler = new GitHubHandler() $(document).bind 'keydown', preventBackspace $.notify.addStyle 'achievement', html: $(AchievementNotify()) @linkedinHandler = new LinkedInHandler() diff --git a/app/lib/GitHubHandler.coffee b/app/lib/GitHubHandler.coffee new file mode 100644 index 000000000..705fd4d8d --- /dev/null +++ b/app/lib/GitHubHandler.coffee @@ -0,0 +1,22 @@ +CocoClass = require 'lib/CocoClass' +{me} = require 'lib/auth' +storage = require 'lib/storage' + +module.exports = class GitHubHandler extends CocoClass + scopes: 'user:email' + + subscriptions: + 'github-login': 'commenceGitHubLogin' + + constructor: -> + super arguments... + @clientID = if application.isProduction() then '9b405bf5fb84590d1f02' else 'fd5c9d34eb171131bc87' + @redirect_uri = if application.isProduction() then 'http://codecombat.com/github/auth_callback' else 'http://localhost:3000/github/auth_callback' + + commenceGitHubLogin: -> + request = + scope: @scopes + client_id: @clientID + redirect_uri: @redirect_uri + + location.href = "https://github.com/login/oauth/authorize?" + $.param(request) diff --git a/app/lib/auth.coffee b/app/lib/auth.coffee index 6d218608d..8218c189c 100644 --- a/app/lib/auth.coffee +++ b/app/lib/auth.coffee @@ -19,7 +19,8 @@ module.exports.createUser = (userObject, failure=backboneFailure, nextURL=null) user.notyErrors = false user.save({}, { error: (model, jqxhr, options) -> - error = parseServerError(jqxhr.responseText) + error = parseServerError(jqxhr + .responseText) property = error.property if error.property if jqxhr.status is 409 and property is 'name' anonUserObject = _.omit(userObject, 'name') diff --git a/app/schemas/models/user.coffee b/app/schemas/models/user.coffee index 47a164b5c..10c148b5c 100644 --- a/app/schemas/models/user.coffee +++ b/app/schemas/models/user.coffee @@ -43,6 +43,7 @@ _.extend UserSchema.properties, photoURL: {type: 'string', format: 'image-file', title: 'Profile Picture', description: 'Upload a 256x256px or larger image to serve as your profile picture.'} facebookID: c.shortString({title: 'Facebook ID'}) + githubID: c.shortString({title: 'GitHub ID'}) gplusID: c.shortString({title: 'G+ ID'}) wizardColor1: c.pct({title: 'Wizard Clothes Color'}) diff --git a/app/templates/modal/auth.jade b/app/templates/modal/auth.jade index 0bb8f6b49..0fc5d514c 100644 --- a/app/templates/modal/auth.jade +++ b/app/templates/modal/auth.jade @@ -69,6 +69,8 @@ block modal-body-wait-content block modal-footer .modal-footer + div.network-login + btn.btn.github-login-button#github-login-button GitHub div.network-login .fb-login-button(data-show-faces="false", data-width="200", data-max-rows="1", data-scope="email") div.network-login diff --git a/app/views/modal/AuthModal.coffee b/app/views/modal/AuthModal.coffee index ea7e4c771..fd87dd988 100644 --- a/app/views/modal/AuthModal.coffee +++ b/app/views/modal/AuthModal.coffee @@ -14,6 +14,7 @@ module.exports = class AuthModal extends ModalView # login buttons 'click #switch-to-signup-button': 'onSignupInstead' 'click #signup-confirm-age': 'checkAge' + 'click #github-login-button': 'onGitHubLoginClicked' 'submit': 'onSubmitForm' # handles both submit buttons 'keyup #name': 'onNameChange' @@ -101,3 +102,6 @@ module.exports = class AuthModal extends ModalView else @suggestedName = newName forms.setErrorToProperty @$el, 'name', "That name is taken! How about #{newName}?", true + + onGitHubLoginClicked: -> + Backbone.Mediator.publish 'github-login' diff --git a/server/commons/mapping.coffee b/server/commons/mapping.coffee index 69f8abfc0..6ffa41e93 100644 --- a/server/commons/mapping.coffee +++ b/server/commons/mapping.coffee @@ -21,6 +21,7 @@ module.exports.routes = 'routes/db' 'routes/file' 'routes/folder' + 'routes/github' 'routes/languages' 'routes/mail' 'routes/sprites' diff --git a/server/routes/github.coffee b/server/routes/github.coffee new file mode 100644 index 000000000..dbae56bb6 --- /dev/null +++ b/server/routes/github.coffee @@ -0,0 +1,50 @@ +log = require 'winston' +errors = require '../commons/errors' +mongoose = require 'mongoose' +config = require('../../server_config') +request = require 'request' +User = require '../users/User' + +module.exports.setup = (app) -> + app.get '/github/auth_callback', (req, res) -> + return errors.forbidden res unless req.user # need identity + response = + code: req.query.code + client_id: config.github.client_id + client_secret: config.github.client_secret + headers = + Accept: 'application/json' + request.post {uri: 'https://github.com/login/oauth/access_token', json: response, headers: headers}, (err, r, response) -> + log.error err if err? + if response.error or err? # If anything goes wrong just 404 + res.send 404, response.error_description or err + else + {access_token, token_type, scope} = response + headers = + Accept: 'application/json' + Authorization: "token #{access_token}" + 'User-Agent': if config.isProduction then 'CodeCombat' else 'CodeCombatDev' + request.get {uri: 'https://api.github.com/user', headers: headers}, (err, r, response) -> + return log.error err if err? + githubUser = JSON.parse response + emailLower = githubUser.email.toLowerCase() + + # GitHub users can change emails + User.findOne {$or: [{emailLower: emailLower}, {githubID: githubUser.id}]}, (err, user) -> + return errors.serverError res, err if err? + wrapup = (err, user) -> + return errors.serverError res, err if err? + req.login (user), (err) -> + return errors.serverError res, err if err? + res.redirect '/' + unless user + req.user.set 'email', githubUser.email + req.user.set 'githubID', githubUser.id + req.user.save wrapup + else if user.get('githubID') isnt githubUser.id # Add or replace githubID to/with existing user + user.set 'githubID', githubUser.id + user.save wrapup + else if user.get('emailLower') isnt emailLower # Existing GitHub user with us changed email + user.update {email: githubUser.email}, (err) -> wrapup err, user + else # All good you've been here before + wrapup null, user diff --git a/server_config.coffee b/server_config.coffee index 336e8dd39..ff2a9cfcd 100644 --- a/server_config.coffee +++ b/server_config.coffee @@ -7,6 +7,10 @@ config.ssl_port = process.env.COCO_SSL_PORT or process.env.COCO_SSL_NODE_PORT or config.cloudflare = token: process.env.COCO_CLOUDFLARE_API_KEY or '' +config.github = + client_id: process.env.COCO_GITHUB_CLIENT_ID or 'fd5c9d34eb171131bc87' + client_secret: process.env.COCO_GITHUB_CLIENT_SECRET or '2555a86b83f850bc44a98c67c472adb2316a3f05' + config.mongo = port: process.env.COCO_MONGO_PORT or 27017 host: process.env.COCO_MONGO_HOST or 'localhost'