Trello tribute with Phoenix and React (pt.6)

User sign in front-end

Now that the back-end functionality is ready to handle sign in requests let's move on to the front-end and see how to build and send these requests and how to use the returned data to allow the user access to private routes.

The routes files

Before continuing let's take a look again at our React routes file:

// 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';
import CardsShowView               from '../views/cards/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 path="cards/:id" component={CardsShowView}/>
      </Route>
    </Route>
  </Route>
);

As we saw in part 4, the AuthenticatedContainer is going to prevent unauthenticated users from accessing the boards views unless the jwt token returned from the sign in process is present and valid.

The view component

Now we need to create the SessionsNew component where the sign in form will be rendered:

import React, {PropTypes}   from 'react';
import { connect }          from 'react-redux';
import { Link }             from 'react-router';

import { setDocumentTitle } from '../../utils';
import Actions              from '../../actions/sessions';

class SessionsNew extends React.Component {
  componentDidMount() {
    setDocumentTitle('Sign in');
  }

  _handleSubmit(e) {
    e.preventDefault();

    const { email, password } = this.refs;
    const { dispatch } = this.props;

    dispatch(Actions.signIn(email.value, password.value));
  }

  _renderError() {
    const { error } = this.props;

    if (!error) return false;

    return (
      <div className="error">
        {error}
      </div>
    );
  }

  render() {
    return (
      <div className='view-container sessions new'>
        <main>
          <header>
            <div className="logo" />
          </header>
          <form onSubmit={::this._handleSubmit}>
            {::this._renderError()}
            <div className="field">
              <input ref="email" type="Email" placeholder="Email" required="true" defaultValue="john@phoenix-trello.com"/>
            </div>
            <div className="field">
              <input ref="password" type="password" placeholder="Password" required="true" defaultValue="12345678"/>
            </div>
            <button type="submit">Sign in</button>
          </form>
          <Link to="/sign_up">Create new account</Link>
        </main>
      </div>
    );
  }
}

const mapStateToProps = (state) => (
  state.session
);

export default connect(mapStateToProps)(SessionsNew);

It basically renders the form and calls the signInaction creator when submitting it. It will also be connected to the store to get its props which will be updated through the session reducer, so we can display validation errors to the user.

The action creator

Following the user's interaction flow, let's create the sessions action creator:

// web/static/js/actions/sessions.js

import { routeActions }                   from 'redux-simple-router';
import Constants                          from '../constants';
import { Socket }                         from 'phoenix';
import { httpGet, httpPost, httpDelete }  from '../utils';

function setCurrentUser(dispatch, user) {
  dispatch({
    type: Constants.CURRENT_USER,
    currentUser: user,
  });

  // ...
};

const Actions = {
  signIn: (email, password) => {
    return dispatch => {
      const data = {
        session: {
          email: email,
          password: password,
        },
      };

      httpPost('/api/v1/sessions', data)
      .then((data) => {
        localStorage.setItem('phoenixAuthToken', data.jwt);
        setCurrentUser(dispatch, data.user);
        dispatch(routeActions.push('/'));
      })
      .catch((error) => {
        error.response.json()
        .then((errorJSON) => {
          dispatch({
            type: Constants.SESSIONS_ERROR,
            error: errorJSON.error,
          });
        });
      });
    };
  },

  // ...
};

export default Actions;

The signIn function will make a POST request sending as parameters the email and password previously provided by the user. If the authentication on the back-end is successful then it will store the returned jwt token in the localStorage and dispatch the currentUser JSON to the store. If, for any reason, there's a error authenticating the user, it will instead dispatch the errors so we can display them in the sign in form.

The reducer

Now let's create the session reducer:

// web/static/js/reducers/session.js

import Constants from '../constants';

const initialState = {
  currentUser: null,
  error: null,
};

export default function reducer(state = initialState, action = {}) {
  switch (action.type) {
    case Constants.CURRENT_USER:
      return { ...state, currentUser: action.currentUser, error: null };

    case Constants.SESSIONS_ERROR:
      return { ...state, error: action.error };

    default:
      return state;
  }
}

Not very much to say about it as it's quite self-explanatory so let's modify the authenticated container so it's aware of the new state:

The authenticated container

// web/static/js/containers/authenticated.js

import React            from 'react';
import { connect }      from 'react-redux';
import Actions          from '../actions/sessions';
import { routeActions } from 'redux-simple-router';
import Header           from '../layouts/header';

class AuthenticatedContainer extends React.Component {
  componentDidMount() {
    const { dispatch, currentUser } = this.props;
    const phoenixAuthToken = localStorage.getItem('phoenixAuthToken');

    if (phoenixAuthToken && !currentUser) {
      dispatch(Actions.currentUser());
    } else if (!phoenixAuthToken) {
      dispatch(routeActions.push('/sign_in'));
    }
  }

  render() {
    const { currentUser, dispatch } = this.props;

    if (!currentUser) return false;

    return (
      <div className="application-container">
        <Header
          currentUser={currentUser}
          dispatch={dispatch}/>

        <div className="main-container">
          {this.props.children}
        </div>
      </div>
    );
  }
}

const mapStateToProps = (state) => ({
  currentUser: state.session.currentUser,
});

export default connect(mapStateToProps)(AuthenticatedContainer);

When this component gets mounted, if there is an authentication token but not a currentUser in the store, it will call the currentUser action creator to retrieve the user's data from the back-end. Let's add it:

// web/static/js/actions/sessions.js
// ...

const Actions = {
  // ...

  currentUser: () => {
    return dispatch => {
      httpGet('/api/v1/current_user')
      .then(function(data) {
        setCurrentUser(dispatch, data);
      })
      .catch(function(error) {
        console.log(error);
        dispatch(routeActions.push('/sign_in'));
      });
    };
  },

  // ...
}

// ...

This gets us covered if the user reloads the browser or he just visits the root url again without signing out before. Following our previous steps, after signing the user and setting the currentUser in the state, the component will render normally displaying the header component and its nested children routes.

The header component

This component will render the user's gravatar and name along with the link to the boards url and the sign out button.

// web/static/js/layouts/header.js

import React          from 'react';
import { Link }       from 'react-router';
import Actions        from '../actions/sessions';
import ReactGravatar  from 'react-gravatar';

export default class Header extends React.Component {
  constructor() {
    super();
  }

  _renderCurrentUser() {
    const { currentUser } = this.props;

    if (!currentUser) {
      return false;
    }

    const fullName = [currentUser.first_name, currentUser.last_name].join(' ');

    return (
      <a className="current-user">
        <ReactGravatar email={currentUser.email} https /> {fullName}
      </a>
    );
  }

  _renderSignOutLink() {
    if (!this.props.currentUser) {
      return false;
    }

    return (
      <a href="#" onClick={::this._handleSignOutClick}><i className="fa fa-sign-out"/> Sign out</a>
    );
  }

  _handleSignOutClick(e) {
    e.preventDefault();

    this.props.dispatch(Actions.signOut());
  }

  render() {
    return (
      <header className="main-header">
        <nav>
          <ul>
            <li>
              <Link to="/"><i className="fa fa-columns"/> Boards</Link>
            </li>
          </ul>
        </nav>
        <Link to='/'>
          <span className='logo'/>
        </Link>
        <nav className="right">
          <ul>
            <li>
              {this._renderCurrentUser()}
            </li>
            <li>
              {this._renderSignOutLink()}
            </li>
          </ul>
        </nav>
      </header>
    );
  }
}

When the user clicks the sign out button it calls the signOut method of the session action creator. Let's add it then:

// web/static/js/actions/sessions.js
// ...

const Actions = {
  // ...

  signOut: () => {
    return dispatch => {
      httpDelete('/api/v1/sessions')
      .then((data) => {
        localStorage.removeItem('phoenixAuthToken');

        dispatch({
          type: Constants.USER_SIGNED_OUT,
        });

        dispatch(routeActions.push('/sign_in'));
      })
      .catch(function(error) {
        console.log(error);
      });
    };
  },

  // ...
}

// ...

It will send a DELETE request against the back-end and, when successful, it will remove the phoenixAuthToken from the localStorage and dispatch the USER_SIGNED_OUT action reseting the currentUser from the state using the previously defined session reducer:

// web/static/js/reducers/session.js

import Constants from '../constants';

const initialState = {
  currentUser: null,
  error: null,
};

export default function reducer(state = initialState, action = {}) {
  switch (action.type) {
    // ...

    case Constants.USER_SIGNED_OUT:
      return initialState;

    // ...
  }
}

One more thing

Although we are done with the user sign in process, there is a crucial functionality we haven't implemented yet, which is going to be the core of all the future features we will code: the user socket and its channels. It's so important that I'd rather prefer leaving it for the next post where we will see how the UserSocket looks like and how to connect to it so we can have bidirectional channels between our front-end and the back-end, displaying changes to the user in realtime. Meanwhile, don't forget to check out the live demo and final source code:

Happy coding!

comments powered by Disqus