zooppa-auth
A set of utilities and authentication components designed for the Zooppa platform, meant to connect to an API developed using the devise_token_auth gem.
Strongly inspired by the redux-auth library, whose code has been partially adapted for the oAuth actions.
The examples assume
react-router v3. This is
because we will utilize
redux-auth-wrapper, which
does not support the newest react-router
v4 at the time of writing. The
examples will be updated as soon as react-router v4
is supported by
redux-auth-wrapper
.
This package requires redux
and the redux-thunk
middleware.
Work In Progress Warning
The public interface of this project is being actively developed. It is, therefore, still evolving and subject to sudden change. Wait until version 1.0.0 if you want to enjoy a stable API.
Installing
npm install @zooppa/zooppa-auth lodash@^4.17.4 qs@^6.5.0
or
yarn add @zooppa/zooppa-auth lodash@^4.17.4 qs@^6.5.0
Using
Setting up the root reducer (importing the auth reducer):
// reducers/root.js
import { combineReducers } from 'redux';
import { authReducer as auth } from '@zooppa/zooppa-auth';
export default combineReducers({
auth
});
Setting up the redux store (incuding the thunk
middleware):
// store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/root';
const configureStore = () =>
createStore(
rootReducer,
/* preloadedState, */
applyMiddleware(thunk)
);
Bootstrapping your application correctly:
// index.js
import { actions } from '@zooppa/zooppa-auth';
const { configure:configureAuth, logout, validateToken } = actions;
const refreshCurrentUserByToken = store => (nextState, replace) => {
store.dispatch(validateToken());
};
const doLogout = store => (nextState, replace) => {
store.dispatch(logout());
replace('/');
};
const store = configureStore();
// call this BEFORE rendering
store.dispatch(configureAuth({
apiRoot: BASE_API_URL,
}));
ReactDOM.render(
<Provider store={store}>
<Router history={browserHistory}>
<Route path="/" onEnter={refreshCurrentUserByToken(store)} component={App} >
<IndexRoute component={Home} />
<Route path="login" component={LoginPage} />
<Route path="logout" onEnter={doLogout(store)} />
<Route path="*" component={Page404} />
</Route>
</Router>
</Provider>,
document.getElementById('root'),
);
Creating your components leveraging the zooppa-auth
actions:
// MyComponent.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actions } from '@zooppa/zooppa-auth';
const { logout } = actions;
class MyComponent extends Component {
render() {
const { isAuthenticated, logout } = this.props;
const logoutButton = isAuthenticated ?
<button onClick={logout} /> :
null;
return (
<div>
User is { isAuthenticated ? 'logged in' : 'logged out' }
{ logoutButton }
</div>
);
}
}
const mapStateToProps = ({ auth }) => ({
isAuthenticated: !!auth.session.currentUser // << current user info
});
export default connect(mapStateToProps, { logout })(MyComponent);
Authenticating with oAuth2:
// FacebookButton.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { actions } from '@zooppa/zooppa-auth';
import { BASE_FRONTEND_URL } from './config';
const { authenticateWithOAuth } = actions;
class FacebookButton extends Component {
static propTypes = {
authenticateWithOauth: PropTypes.function.isRequired
}
constructor(props) {
super(props);
this.authenticateWithFacebook = this.authenticateWithFacebook.bind(this);
}
authenticateWithFacebook() {
this.props
.authenticateWithOAuth({
provider: 'facebook',
popupLandingUrl: `${BASE_FRONTEND_URL}/oauth_landing`,
})
.then(() => this.context.router.replace('/'));
}
render() {
return (
<button className="facebook" onClick={this.authenticateWithFacebook}>
Sign in via Facebook
</button>
);
}
}
FacebookButton.contextTypes = {
router: PropTypes.object.isRequired,
};
export default connect(null, { authenticateWithOAuth })(FacebookButton);
Signing up via email:
// SignupPage.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { get } from 'lodash';
class SignupPage extends Component {
static propTypes = {
signup: PropTypes.function.isRequired
}
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
password_confirmation: '',
}
this.handleInput = this.handleInput.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleInput(field) {
return e => this.setState({ [field]: e.target.value });
}
handleSubmit(e) {
e.preventDefault();
this.props.signup(Object.assign(
{ confirm_success_url: 'http://example.net' },
this.state,
))
.then(() => { this.context.router.redirect('/signup_confirm') }) // mount a signup confirmation page here
.catch(console.log);
}
render () {
const { signup } = this.props;
const errors = this.props.errors.map(error => <li>{error}</li>);
return (
<form onSubmit={this.handleSubmit}>
Email: <input type="text" value={this.state.email} />
Password: <input type="password" value={this.state.password} />
Password confirmation: <input type="password_confirmation" value={this.state.password} />
<button type="submit">Sign Up</button>
</form>
{errors}
);
}
}
const mapStateToProps = ({ auth }) => ({
errors: get(auth.signup.errors, 'full_messages', []);
});
export default connect(mapStateToProps, { signup })(SignupPage);
// App.js, assuming it's mounted @ the confirm_success_url path
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { writeToLocalStorage } from '@zooppa/zooppa-auth';
class App extends Component {
static propTypes = {
location: PropTypes.shape({
query: PropTypes.shape({
account_confirmation_success: PropTypes.string,
uid: PropTypes.string,
client_id: PropTypes.string,
token: PropTypes.string,
}),
}),
};
// or connect() it!
static contextTypes = {
store: PropTypes.object.isRequired,
};
componentWillMount() {
// the following params will be set by the API redirect after clicking the
// email link
const {
account_confirmation_success,
uid,
client_id,
token,
} = this.props.location.query;
if (account_confirmation_success) {
writeToLocalStorage({
'access-token': token,
client: client_id,
uid,
});
// or connect() it!
this.context.store.dispatch(validateToken());
}
}
render() {
return (
<h1>Hello world</h1>
);
}
}
Leveraging the supplied BaseApi class to create your own API-consuming classes:
// util/UsersApi.js
import { BaseApi } from '@zooppa/zooppa-auth';
class UsersApi extends BaseApi {
index() {
return this.apiClient.get('/users');
}
show(id) {
return this.apiClient.get(`/users/${id}`);
}
}
// actions/users.js
export const fetchUsers = () => (dispatch, getState) => {
dispatch(requestUsers());
return new UsersApi({ apiRoot: getState().auth.configuration.apiRoot })
.index({ role, page })
.then(res => res.json())
.then(users => dispatch(receiveUsers(users)));
}
Testing
-
npm run test:single
-- single test run with coverage check -
npm run test
-- keep running tests on modified files
Linting
This project is meant to be used in a create-react-app application, therefore we are relying on the same linter rules.