This post belongs to the Trello tribute with Phoenix Framework and React series.
- Intro and selected stack
- Phoenix Framework project setup
- The User model and JWT auth
- Front-end for sign up with React and Redux
- Database seeding and sign in controller
- Front-end authentication with React and Redux
- Sockets and channels
- Listing and creating boards
- Adding new board members
- Tracking connected board members
- Adding lists and cards
- Deploying our application on Heroku
User sign up
In the last post we created the User
model with its validations
and the necessary changeset transformation to generate the encrypted password,
we also updated our router file and created the RegistrationController
where a
new user request is handled and returns the user in JSON and its jwt token for
authenticating future requests. Now let's move on to the front-end side.
Preparing the React router
The main goal is to have two public routes, /sign_in
and /sign_up
, which
any visitor is going to be able to visit in order to log into the application
or register a new user account.
On the other hand we are going to need a /
as the root route to display all the boards belonging to the user and finally a /boards/:id
route to display the selected board by the user. To access these last two routes, the
user must be authenticated, otherwise we'll redirect him to the registration screen.
So let's update the react-router routes
file to represent this:
// web/static/js/routes/index.js
import { IndexRoute, Route } from 'react-router';
import React from 'react';
import MainLayout from '../layouts/main';
import AuthenticatedContainer from '../containers/authenticated';
import HomeIndexView from '../views/home';
import RegistrationsNew from '../views/registrations/new';
import SessionsNew from '../views/sessions/new';
import BoardsShowView from '../views/boards/show';
export default (
<Route component={MainLayout}>
<Route path="/sign_up" component={RegistrationsNew} />
<Route path="/sign_in" component={SessionsNew} />
<Route path="/" component={AuthenticatedContainer}>
<IndexRoute component={HomeIndexView} />
<Route path="/boards/:id" component={BoardsShowView} />
</Route>
</Route>
);
The tricky part is the AuthenticatedContainer
, let's take a look at it:
// web/static/js/containers/authenticated.js
import React from 'react';
import { connect } from 'react-redux';
import { routeActions } from 'redux-simple-router';
class AuthenticatedContainer extends React.Component {
componentDidMount() {
const { dispatch, currentUser } = this.props;
if (localStorage.getItem('phoenixAuthToken')) {
dispatch(Actions.currentUser());
} else {
dispatch(routeActions.push('/sign_up'));
}
}
render() {
// ...
}
}
const mapStateToProps = (state) => ({
currentUser: state.session.currentUser,
});
export default connect(mapStateToProps)(AuthenticatedContainer);
What we basically do here is, when the component mounts, check if the jwt token is present in the browser's local storage. Later on we will see how to set it, but for now let's just imagine that it doesn't exist, so thanks to the redux-simple-router library we will redirect the user to the sign up page.
The sign up view component
This is what we will render to the user once we detect that he is not authenticated:
// web/static/js/views/registrations/new.js
import React, {PropTypes} from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import { setDocumentTitle, renderErrorsFor } from '../../utils';
import Actions from '../../actions/registrations';
class RegistrationsNew extends React.Component {
componentDidMount() {
setDocumentTitle('Sign up');
}
_handleSubmit(e) {
e.preventDefault();
const { dispatch } = this.props;
const data = {
first_name: this.refs.firstName.value,
last_name: this.refs.lastName.value,
email: this.refs.email.value,
password: this.refs.password.value,
password_confirmation: this.refs.passwordConfirmation.value,
};
dispatch(Actions.signUp(data));
}
render() {
const { errors } = this.props;
return (
<div className="view-container registrations new">
<main>
<header>
<div className="logo" />
</header>
<form onSubmit={::this._handleSubmit}>
<div className="field">
<input ref="firstName" type="text" placeholder="First name" required={true} />
{renderErrorsFor(errors, 'first_name')}
</div>
<div className="field">
<input ref="lastName" type="text" placeholder="Last name" required={true} />
{renderErrorsFor(errors, 'last_name')}
</div>
<div className="field">
<input ref="email" type="email" placeholder="Email" required={true} />
{renderErrorsFor(errors, 'email')}
</div>
<div className="field">
<input ref="password" type="password" placeholder="Password" required={true} />
{renderErrorsFor(errors, 'password')}
</div>
<div className="field">
<input ref="passwordConfirmation" type="password" placeholder="Confirm password" required={true} />
{renderErrorsFor(errors, 'password_confirmation')}
</div>
<button type="submit">Sign up</button>
</form>
<Link to="/sign_in">Sign in</Link>
</main>
</div>
);
}
}
const mapStateToProps = (state) => ({
errors: state.registration.errors,
});
export default connect(mapStateToProps)(RegistrationsNew);
Not very much to say about this component... it changes the document's
title when it mounts, it also renders the sign up form and dispatches the result
of the signUp
registration action creator.
The action creator
When the previous form is submitted we want to send the data to the server where it will be processed:
// web/static/js/actions/registrations.js
import { pushPath } from 'redux-simple-router';
import Constants from '../constants';
import { httpPost } from '../utils';
const Actions = {};
Actions.signUp = (data) => {
return dispatch => {
httpPost('/api/v1/registrations', {user: data})
.then((data) => {
localStorage.setItem('phoenixAuthToken', data.jwt);
dispatch({
type: Constants.CURRENT_USER,
currentUser: data.user,
});
dispatch(pushPath('/'));
})
.catch((error) => {
error.response.json()
.then((errorJSON) => {
dispatch({
type: Constants.REGISTRATIONS_ERROR,
errors: errorJSON.errors,
});
});
});
};
};
export default Actions;
When the RegistrationsNew
component calls this action creator passing the form data,
a new POST request is sent to the server. The request is filtered by Phoenix's router and processed by the RegistrationController
we previously created in the previous blog post. If the result is successful
then the returned jwt
token is stored into the localStorage
, the
created user data is dispatched in the CURRENT_USER
action and it finally
redirects the user to the root path. On the contrary, if there is any error related
to the registration data, it will dispatch REGISTRATIONS_ERROR
action with the errors
so we can show them in the form to the user.
For these http requests we are going to lean on the isomorphic-fetch package used from our utility file which includes some helpers for this purpose:
// web/static/js/utils/index.js
import React from 'react';
import fetch from 'isomorphic-fetch';
import { polyfill } from 'es6-promise';
export function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
} else {
var error = new Error(response.statusText);
error.response = response;
throw error;
}
}
export function parseJSON(response) {
return response.json();
}
export function httpPost(url, data) {
const headers = {
Authorization: localStorage.getItem('phoenixAuthToken'),
Accept: 'application/json',
'Content-Type': 'application/json',
}
const body = JSON.stringify(data);
return fetch(url, {
method: 'post',
headers: headers,
body: body,
})
.then(checkStatus)
.then(parseJSON);
}
// ...
The reducers
The final step is to handle these actions results with the reducers so that we can create the
new state tree the application needs. First let's take a look to the session
reducer where the currentUser
is going to be set:
// web/static/js/reducers/session.js
import Constants from '../constants';
const initialState = {
currentUser: null,
};
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case Constants.CURRENT_USER:
return { ...state, currentUser: action.currentUser };
default:
return state;
}
}
In case there is any kind of registration errors we also need to update the new
state with them so they can be displayed to the user. Let's add them in the registration
reducer:
// web/static/js/reducers/registration.js
import Constants from '../constants';
const initialState = {
errors: null,
};
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case Constants.REGISTRATIONS_ERROR:
return {...state, errors: action.errors};
default:
return state;
}
}
Note that for displaying the errors we are calling the renderErrorsFor
function from the utils file:
// web/static/js/utils/index.js
// ...
export function renderErrorsFor(errors, ref) {
if (!errors) return false;
return errors.map((error, i) => {
if (error[ref]) {
return (
<div key={i} className="error">
{error[ref]}
</div>
);
}
});
}
And that's all for the registration process. In the next post we will see how existing users can authenticate into the application and have access to their private stuff. Meanwhile, don't forget to check out the live demo and final source code:
Happy coding!