Skip to content

Commit

Permalink
Implement frontend login
Browse files Browse the repository at this point in the history
  • Loading branch information
jonganc committed Oct 10, 2017
1 parent 2ff0cb0 commit a976310
Show file tree
Hide file tree
Showing 10 changed files with 564 additions and 52 deletions.
77 changes: 62 additions & 15 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { BrowserRouter as Router, Route, Switch, Link }
from 'react-router-dom';
import 'whatwg-fetch'; // fetch polyfill, as needed

import Login from './Login.jsx';
import LogOut from './LogOut.jsx';
import Header from './Header.jsx';
import SomeClass from './SomeClass.jsx';
import UpdateAccessToken from './UpdateAccessToken.jsx';
Expand All @@ -19,25 +21,70 @@ const Home = () => (
</div>
);

const App = () => (
class AppMain extends React.Component {
constructor(props) {
super(props);
this.state = {
// if null, the user is not logged in
// when logged in, the user is an object
// { states, firstName, lastName, isAdmin }
user: null,
};
this.setUser = this.setUser.bind(this);
}

// set the user's login state
setUser(user) {
this.setState({ user });
}

render() {
if (!this.state.user) {
return <Login setUser={this.setUser} />;
}

return (
<Router>
<div>
<Header user={this.state.user} />
<div className="container-fluid">
<Switch>
<Route exact path="/" component={Home} />
<Route
exact
path="/logout"
render={props => (
<LogOut
setUser={this.setUser}
history={props.history}
/>)}
/>{
}<Route path="/some-path" component={SomeClass} />
<Route
path="/update-access-token"
component={UpdateAccessToken}
/>
<Route path="*" component={NoMatch} />
</Switch>
<hr />
<h5><small>Some footer text.</small></h5>
</div>
</div>
</Router>);
}
}

// wrapper that includes logout functionality
const AppWrapper = props => (
<Router>
<div>
<Header />
<div className="container-fluid">
<Switch>
<Route exact path="/" component={Home} />
<Route path="/some-path" component={SomeClass} />
<Route path="/update-access-token" component={UpdateAccessToken} />
<Route path="*" component={NoMatch} />
</Switch>
<hr />
<h5><small>Some footer text.</small></h5>
</div>
</div>
<Switch>
<Route exact path="/logout" component={LogOut} />
<Route component={AppMain} />
</Switch>
</Router>
);

ReactDOM.render(<App />, contentNode);
ReactDOM.render(<AppWrapper />, contentNode);

if (module.hot) {
module.hot.accept();
Expand Down
79 changes: 79 additions & 0 deletions src/GoogleLoginButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// we abstract the login button component in case we need to change login
// providers

import React from 'react';
import PropTypes from 'prop-types';

import { injectScript } from './common-client';

// LoginButton takes three properties: appId, onSuccess onFailure.
// The latter two are callbacks used in the appropriate instances.
//
// ONSUCCESS is called with the appropriate ID token.
// ONFAILURE is called with an error
class GoogleLoginButton extends React.Component {
constructor(props) {
super(props);
this.state = {
// have we begun styling the button?
isStyled: false,
};
}

// style the login button
async styleButton() {
if (!window.gapi) {
await injectScript('https://apis.google.com/js/api.js');
}
if (!window.gapi.signin2) {
await new Promise(
(resolveInner, rejectInner) => window.gapi.load('signin2', {
callback: resolveInner,
onerror: () =>
rejectInner(new Error('gapi.signin2 failed to load')),
timeout: 5000,
ontimeout: () =>
rejectInner(new Error('gapi.signin2 timed-out on load ')),
}));
}

window.gapi.signin2.render('g-signin2', {
id: 'g-signin2',
onsuccess: (googleUser) => {
this.props.onSuccess(googleUser.getAuthResponse().id_token);
},
onfailure: this.props.onFailure,
scope: 'email',
});
}

render(props) {
if (!this.state.isStyled) {
this.state.isStyled = true;

this.styleButton();

// add a header with client id if not already present
if (!document.querySelector('meta[name="google-signin-client_id"]')) {
const meta = document.createElement('meta');
meta.name = 'google-signin-client_id';
meta.content = this.props.appId;
document.head.appendChild(meta);
}
}
return <div id="g-signin2" />;
}
}

GoogleLoginButton.propTypes = {
appId: PropTypes.string.isRequired,
onSuccess: PropTypes.func,
onFailure: PropTypes.func,
};

GoogleLoginButton.defaultProps = {
onSuccess: () => {},
onFailure: () => {},
};

export default GoogleLoginButton;
37 changes: 16 additions & 21 deletions src/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import React from 'react';
import {
Navbar, Nav, NavItem, NavDropdown, MenuItem, Glyphicon,
} from 'react-bootstrap';
import { Navbar, Nav, NavItem, NavDropdown, MenuItem } from 'react-bootstrap';
import propTypes from 'prop-types';
import { LinkContainer } from 'react-router-bootstrap';
import GoogleLogin from 'react-google-login';
import ClientCredentials from './credentials-client';

// Dummy callback for login
const responseGoogle = (response) => {
console.log(response);
};

const Header = () => (
const Header = props => (
<Navbar fluid>
<Navbar.Header>
<Navbar.Brand>ATI Broadcast App</Navbar.Brand>
Expand All @@ -20,25 +12,28 @@ const Header = () => (
<LinkContainer to="/some-path">
<NavItem>Go somewhere</NavItem>
</LinkContainer>
<NavItem>
<GoogleLogin
clientId={ClientCredentials.googleClientId}
buttonText="Login"
onSuccess={responseGoogle}
onFailure={responseGoogle}
/>
</NavItem>
</Nav>
<Nav pullRight>
<NavDropdown
id="user-dropdown"
title={<Glyphicon glyph="option-horizontal" />}
title={`${props.user.firstName} ${props.user.lastName}`}
noCaret
>
<MenuItem>Logout</MenuItem>
<LinkContainer to="/logout">
<MenuItem>Logout</MenuItem>
</LinkContainer>
</NavDropdown>
</Nav>
</Navbar>
);

Header.propTypes = {
user: propTypes.shape({
states: propTypes.arrayOf(propTypes.string),
firstName: propTypes.string,
lastName: propTypes.string,
isAdmin: propTypes.bool,
}).isRequired,
};

export default Header;
87 changes: 87 additions & 0 deletions src/LogOut.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react';
import PropTypes from 'prop-types';

import { deleteParseJson, printBackendError } from './common-client';
import googleLogOut from './googleLogOut';

class LogOutMain extends React.Component {
constructor(props) {
super(props);
this._isMounted = true;
this.state = {
// an error message. null if we are waiting
message: null,
};

// note that we take a little care not to call setState after
// component has unmounted
deleteParseJson('/api/login')
.then(
async (response) => {
if (!this._isMounted) return;
if (response.error) {
this.setState(
{ message: printBackendError(response, true) });
} else {
let isError = false;
try {
await googleLogOut();
if (!this._isMounted) return;
} catch (e) {
if (!this._isMounted) return;
isError = true;
this.setState(
{ message: `Error logging out of Google:\n${e.message}` });
}
if (!isError) window.location.href = '/';
}
},
(e) => {
if (!this._isMounted) return;
this.setState({ message: e.message });
});
}

componentWillUnmount() {
this._isMounted = false;
}

render() {
if (this.state.message) {
return (
<h4 style={{ color: 'red' }}>{this.state.message}</h4>
);
} else {
return (
<h1>Waiting on server response</h1>
);
}
}
}

// this wrapper just takes care of recreating LogOutMain when component is
// reloaded
// eslint-disable-next-line react/no-multi-comp
class LogOut extends React.Component {
constructor(props) {
super(props);
this.state = { key: 0 };
this.props.history.listen((location, action) => {
this.setState({ key: this.state.key + 1 });
});
}

render() {
return (
<LogOutMain
key={this.state.key.toString()}
/>);
}
}

LogOut.propTypes = {
// eslint-disable-next-line react/forbid-prop-types
history: PropTypes.object.isRequired,
};

export default LogOut;
Loading

0 comments on commit a976310

Please sign in to comment.