Skip to content
This repository has been archived by the owner on Mar 5, 2020. It is now read-only.

Commit

Permalink
Merge pull request #626 from toolness/oauth2-take-2
Browse files Browse the repository at this point in the history
Support OAuth2 (take 2)
  • Loading branch information
alicoding committed Apr 16, 2015
2 parents bf48f75 + 0fe33c1 commit 19fb89c
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 223 deletions.
142 changes: 92 additions & 50 deletions components/login.jsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,110 @@
var React = require('react');
var _ = require('underscore');
var React = require('react/addons');
var Router = require('react-router');
var Link = Router.Link;

var config = require('../lib/config');
var TeachAPIClientMixin = require('../mixins/teach-api-client');
var ga = require('react-ga');

var LogoutLink = React.createClass({
mixins: [TeachAPIClientMixin, Router.State, React.addons.PureRenderMixin],
propTypes: {
origin: React.PropTypes.string
},
getDefaultProps: function() {
return {
origin: config.ORIGIN
};
},
render: function() {
var callbackURL = this.props.origin + this.getPathname();
var loginBaseURL = this.getTeachAPI().baseURL;
var href = loginBaseURL + '/auth/oauth2/logout?callback=' +
encodeURIComponent(callbackURL);
var props = _.extend({}, this.props, {
href: href
});

return React.DOM.a(props, this.props.children);
}
});

var LoginLink = React.createClass({
mixins: [TeachAPIClientMixin, Router.State, React.addons.PureRenderMixin],
propTypes: {
origin: React.PropTypes.string,
callbackSearch: React.PropTypes.string,
action: React.PropTypes.string
},
getDefaultProps: function() {
return {
origin: config.ORIGIN,
callbackSearch: '',
action: 'signin'
};
},
render: function() {
var callbackPath = this.getPathname() + this.props.callbackSearch;
var callbackURL = this.props.origin + callbackPath;
var loginBaseURL = this.getTeachAPI().baseURL;
var action = this.props.action;
var href = loginBaseURL + '/auth/oauth2/authorize?callback=' +
encodeURIComponent(callbackURL) + '&action=' + action;
var props = _.extend({}, this.props, {
href: href
});

if (process.env.NODE_ENV !== 'production' &&
!/^(signin|signup)$/.test(action)) {
console.warn("unrecognized action: " + action);
}

return React.DOM.a(props, this.props.children);
}
});

var Login = React.createClass({
mixins: [TeachAPIClientMixin],
statics: {
LoginLink: LoginLink,
LogoutLink: LogoutLink,
teachAPIEvents: {
'login:start': 'handleApiLoginStart',
'login:error': 'handleApiLoginError',
'login:cancel': 'handleApiLoginCancel',
'login:success': 'handleApiLoginSuccess',
'logout': 'handleApiLogout'
}
},
getDefaultProps: function() {
return {
alert: defaultAlert
};
},
componentDidMount: function() {
this.setState({username: this.getTeachAPI().getUsername()});
var teachAPI = this.getTeachAPI();

teachAPI.checkLoginStatus();
this.setState({username: teachAPI.getUsername()});
},
getInitialState: function() {
return {
username: null,
loggingIn: false
loggingIn: false,
loginError: false
};
},
handleLoginClick: function(e) {
e.preventDefault();
this.setState({loggingIn: true});
this.getTeachAPI().startLogin();
ga.event({ category: 'Login', action: 'Start Login' });
},
handleLogoutClick: function(e) {
e.preventDefault();
this.getTeachAPI().logout();
ga.event({ category: 'Login', action: 'Clicked Logout' });
},
handleApiLoginError: function(err) {
this.setState({loggingIn: false});

if (!config.IN_TEST_SUITE) {
console.log("Teach API error", err);
ga.event({ category: 'Login', action: 'Teach API Error',
nonInteraction:true});
}

if (err.hasNoWebmakerAccount) {
this.props.alert(
"An error occurred when logging in. Are you sure you " +
"have a Webmaker account associated with the email " +
"address you used?"
);
ga.event({ category: 'Login', action: 'Error: Has no Webmaker Account',
nonInteraction:true});
} else {
this.props.alert("An error occurred! Please try again later.");
ga.event({ category: 'Login', action: 'Error Occurred',
nonInteraction:true});
}
this.setState({
loggingIn: false,
loginError: true
});
ga.event({ category: 'Login', action: 'Error Occurred',
nonInteraction:true});
},
handleApiLoginCancel: function() {
this.setState({loggingIn: false});
ga.event({ category: 'Login', action: 'Cancelled Login' });
handleApiLoginStart: function() {
this.setState({loggingIn: true});
},
handleApiLoginSuccess: function(info) {
this.setState({username: this.getTeachAPI().getUsername(),
Expand All @@ -80,22 +118,34 @@ var Login = React.createClass({
render: function() {
var content;

if (this.state.loggingIn) {
if (this.state.loginError) {
content = (
<span><small>
<span className="glyphicon glyphicon-flash"/>&nbsp;
Unable to contact login server.
<br/>
<span className="glyphicon glyphicon-flash" style={{
opacity: '0'
}}/>&nbsp;
Refresh the page to try again.
</small></span>
);
} else if (this.state.loggingIn) {
content = (
<span>
Logging in&hellip;
Loading&hellip;
</span>
);
} else if (this.state.username) {
content = (
<span>
Logged in as {this.state.username} | <a href="" onClick={this.handleLogoutClick}>Logout</a>
Logged in as {this.state.username} | <LogoutLink>Logout</LogoutLink>
</span>
);
} else {
content = (
<span>
<Link to="join">Create an account</Link> | <a href="" onClick={this.handleLoginClick}>Log in</a>
<LoginLink action="signup">Create an account</LoginLink> | <LoginLink>Log in</LoginLink>
</span>
);
}
Expand All @@ -108,12 +158,4 @@ var Login = React.createClass({
}
});

function defaultAlert(message) {
if (process.browser) {
window.alert(message);
} else {
console.log("User alert: " + message);
}
}

module.exports = Login;
3 changes: 3 additions & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ exports.IN_STATIC_SITE = IN_STATIC_SITE;
exports.GENERATING_STATIC_SITE = GENERATING_STATIC_SITE;
exports.ENABLE_PUSHSTATE = ENABLE_PUSHSTATE;
exports.IN_TEST_SUITE = (typeof describe == 'function');
exports.ORIGIN = IN_STATIC_SITE ? (window.location.protocol + '//' +
window.location.host)
: (process.env.ORIGIN || '');
1 change: 0 additions & 1 deletion lib/index-static.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ function generateWithPageHTML(url, options, pageHTML) {
<script src="/commons.bundle.js"></script>
<script src="/app.bundle.js"></script>
<script src="https://mozorg.cdn.mozilla.net/tabzilla/tabzilla.js"></script>
<script src="https://login.persona.org/include.js" async></script>
</body>
</html>
);
Expand Down
40 changes: 15 additions & 25 deletions lib/teach-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,36 +53,26 @@ _.extend(TeachAPI.prototype, {
var info = this.getLoginInfo();
return info && info.username;
},
startLogin: function() {
if (!(process.browser && window.navigator.id)) {
return this.emit('login:error',
new Error('navigator.id does not exist'));
}
window.navigator.id.get(function(assertion) {
if (!assertion) {
return this.emit('login:cancel');
}

request
.post(this.baseURL + '/auth/persona')
.type('form')
.send({ assertion: assertion })
.end(function(err, res) {
if (err) {
err.hasNoWebmakerAccount = (
err.response && err.response.forbidden &&
err.response.text == 'invalid assertion or email'
);
return this.emit('login:error', err);
}

checkLoginStatus: function() {
this.emit('login:start');
request.get(this.baseURL + '/auth/status')
.withCredentials()
.accept('json')
.end(function(err, res) {
if (err) {
this.emit('login:error', err);
return;
}
if (res.body.username) {
// TODO: Handle a thrown exception here.
this.storage[STORAGE_KEY] = JSON.stringify(res.body);

this.emit('username:change', res.body.username);
this.emit('login:success', res.body);
}.bind(this));
}.bind(this));
} else {
this.logout();
}
}.bind(this));
},
request: function(method, path) {
var info = this.getLoginInfo();
Expand Down
11 changes: 3 additions & 8 deletions pages/clubs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var PageEndCTA = require('../components/page-end-cta.jsx');
var Modal = require('../components/modal.jsx');
var ModalManagerMixin = require('../mixins/modal-manager');
var TeachAPIClientMixin = require('../mixins/teach-api-client');
var LoginLink = require('../components/login.jsx').LoginLink;
var ga = require('react-ga');

var Illustration = require('../components/illustration.jsx');
Expand Down Expand Up @@ -332,10 +333,6 @@ var ModalAddOrChangeYourClub = React.createClass({
this.hideModal();
this.props.onSuccess(this.state.result);
},
handleJoinClick: function() {
this.hideModal();
this.transitionTo('join');
},
renderValidationErrors: function() {
if (this.state.validationErrors.length) {
return (
Expand All @@ -361,10 +358,8 @@ var ModalAddOrChangeYourClub = React.createClass({
content = (
<div>
<p>Before you can {action} your club, you need to log in.</p>
<button className="btn btn-primary btn-block"
onClick={this.getTeachAPI().startLogin}>Log In</button>
<button className="btn btn-default btn-block"
onClick={this.handleJoinClick}>Create an account</button>
<LoginLink callbackSearch="?modal=add" className="btn btn-primary btn-block">Log In</LoginLink>
<LoginLink callbackSearch="?modal=add" action="signup" className="btn btn-default btn-block">Create an account</LoginLink>
</div>
);
} else if (this.state.step == this.STEP_FORM ||
Expand Down
22 changes: 0 additions & 22 deletions test/browser/clubs-page.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,28 +203,6 @@ describe("ClubsPage.ModalAddOrChangeYourClub", function() {
modal.state.step.should.equal(modal.STEP_AUTH);
});

it("closes modal when user clicks join btn", function() {
teachAPI.emit('username:change', null);
var joinBtn = TestUtils.findRenderedDOMComponentWithClass(
modal,
'btn-default'
);
modal.context.hideModal.callCount.should.eql(0);
TestUtils.Simulate.click(joinBtn);
modal.context.hideModal.callCount.should.eql(1);
});

it("starts login when user clicks login on auth step", function() {
teachAPI.emit('username:change', null);
var loginBtn = TestUtils.findRenderedDOMComponentWithClass(
modal,
'btn-primary'
);
teachAPI.startLogin.callCount.should.eql(0);
TestUtils.Simulate.click(loginBtn);
teachAPI.startLogin.callCount.should.eql(1);
});

it("shows form when user is logged in", function() {
teachAPI.emit('username:change', 'foo');
modal.state.step.should.equal(modal.STEP_FORM);
Expand Down
Loading

0 comments on commit 19fb89c

Please sign in to comment.