Phoenix Todos - Transition to Redux

This post is written as a set of Literate Commits. The goal of this style is to show you how this program came together from beginning to end.

Each commit in the project is represented by a section of the article. Click each section's header to see the commit on Github, or check out the repository and follow along.

Written by Pete Corey on Sep 21, 2016.

Front-end Join

Now that our back-end is shored up, we can turn our attention to the front-end side of the sign-up functionality.

The first thing we need to do is add the "join" route to our router:


<Route path="join" component={AuthPageJoin}/>

We can copy the majority of the AuthPageJoin component from the Meteor Todos project.

One small change we need to make is to rename all references to confirm to password_confirm to match what our User.changeset expects.

We’ll also need to refactor how we create the user’s account. Instead of using Meteor’s Accounts system, we’ll need to manually make a POST request to "/api/users", passing the user provided email, password, and password_confirm fields:


fetch("/api/users", {
  method: "post",
  headers: {
    "Accept": "application/json",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    user: {
      email,
      password,
      password_confirm
    }
  })
})

If we receive any errors from the server, we can drop them directly into the errors field of our component’s state:


let errors = json.errors.reduce((errors, error) => {
  return Object.assign(errors, error);
});
this.setState({errors});

Otherwise, if everything went well we’ll recieve the newly signed up user’s JWT and user object in the json.jwt and json.user fields.

web/static/js/pages/AuthPage.jsx

+import React from 'react'; +import MobileMenu from '../components/MobileMenu.jsx'; + +// a common layout wrapper for auth pages +const AuthPage = ({ content, link }) => ( + <div className="page auth"> + <nav> + <MobileMenu/> + </nav> + <div className="content-scrollable"> + {content} + {link} + </div> + </div> +); + +AuthPage.propTypes = { + content: React.PropTypes.element, + link: React.PropTypes.element, +}; + +export default AuthPage;

web/static/js/pages/AuthPageJoin.jsx

+import React from 'react'; +import AuthPage from './AuthPage.jsx'; +import { Link } from 'react-router'; + +export default class JoinPage extends React.Component { + constructor(props) { + super(props); + this.state = { errors: {} }; + this.onSubmit = this.onSubmit.bind(this); + } + + onSubmit(event) { + event.preventDefault(); + const email = this.refs.email.value; + const password = this.refs.password.value; + const password_confirm = this.refs.password_confirm.value; + const errors = {}; + + if (!email) { + errors.email = 'Email required'; + } + if (!password) { + errors.password = 'Password required'; + } + if (password_confirm !== password) { + errors.password_confirm = 'Please confirm your password'; + } + + this.setState({ errors }); + if (Object.keys(errors).length) { + return; + } + + fetch("/api/users", { + method: "post", + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + user: { + email, + password, + password_confirm + } + }) + }) + .then((res) => { + res + .json() + .then((json) => { + if (json.errors) { + let errors = json.errors.reduce((errors, error) => { + return Object.assign(errors, error); + }); + this.setState({errors}); + } + else { + // TODO: Save `json.user`{:.language-javascript} and `json.jwt`{:.language-javascript} to state + this.context.router.push('/'); + } + }); + }); + } + + render() { + const { errors } = this.state; + const errorMessages = Object.keys(errors).map(key => errors[key]); + const errorClass = key => errors[key] && 'error'; + + const content = ( + <div className="wrapper-auth"> + <h1 className="title-auth">Join.</h1> + <p className="subtitle-auth" >Joining allows you to make private lists</p> + <form onSubmit={this.onSubmit}> + <div className="list-errors"> + {errorMessages.map(msg => ( + <div className="list-item" key={msg}>{msg}</div> + ))} + </div> + <div className={`input-symbol ${errorClass('email')}`{:.language-javascript}}> + <input type="email" name="email" ref="email" placeholder="Your Email"/> + <span className="icon-email" title="Your Email"></span> + </div> + <div className={`input-symbol ${errorClass('password')}`{:.language-javascript}}> + <input type="password" name="password" ref="password" placeholder="Password"/> + <span className="icon-lock" title="Password"></span> + </div> + <div className={`input-symbol ${errorClass('password_confirm')}`{:.language-javascript}}> + <input type="password" name="password_confirm" ref="password_confirm" placeholder="Confirm Password"/> + <span className="icon-lock" title="Confirm Password"></span> + </div> + <button type="submit" className="btn-primary">Join Now</button> + </form> + </div> + ); + + const link = <Link to="/signin" className="link-auth-alt">Have an account? Sign in</Link>; + + return <AuthPage content={content} link={link}/>; + } +} + +JoinPage.contextTypes = { + router: React.PropTypes.object, +};

web/static/js/routes.jsx

... import AppContainer from './containers/AppContainer.jsx'; +import AuthPageJoin from './pages/AuthPageJoin.jsx'; import NotFoundPage from './pages/NotFoundPage.jsx'; ... <Route path="/" component={AppContainer}> + <Route path="join" component={AuthPageJoin}/> <Route path="*" component={NotFoundPage}/>

Enter Redux

Now that we’re receiving the newly signed up user and JWT from the server, we’ll need some way of updating our client-side application state.

Previously, we were doing this reactively with Meteor’s createContainer function. Since we’re not using Meteor, this is no longer an option.

Instead, we’ll switch to Redux for all of our state management needs.

The first thing we need to do to get started with Redux is to install our NPM dependencies:


npm install --save redux react-redux

Now we need to think about the “shape” of our application’s state. Thankfully, we don’t have to think too hard. The shape has been decided for us in the old AppContainer component. We’ll define this “state shape” in a new file, /web/static/js/reducers.js:


const initialState = {
  user: undefined,
  jwt: undefined,
  loading: false,
  connected: true,
  menuOpen: false,
  lists: []
};

Notice that we slipped in a new field: jwt. We’ll use this field to hold the JWT we get as a response from our server when signing in or signing up.

Now we need to define our reducer:


export default (state = initialState, action) => {
  switch(action.type) {
    default:
      return state;
  }
}

Since we don’t have any actions defined yet, our reducer is as simple as it gets.

Now that we have our reducer defined, we can create our store in app.js:


const store = createStore(reducers);

We’ll pass our store into our renderRoutes function so we can wrap our router in a <Provider> component:


<Provider store={store}>
  <Router ...>
    ...
  </Router>
</Provider>

Lastly, we’ll use subscribe to trigger a re-render any time our store changes.

Now that our Redux store is all wired up, we can pull the state initialization out of our AppContainer component and connect it to our Redux store:


const AppContainer = connect(state => state)(App);

Now our application state is passed from our store, into our AppContainer, and down into the App component.

package.json

"react-dom": "^15.3.1", + "react-redux": "^4.4.5", "react-router": "^2.7.0", + "redux": "^3.6.0", "uglify-js-brunch": ">= 1.0 < 1.8"

web/static/js/app.js

-import { render } from "react-dom"; +import ReactDOM from "react-dom"; +import reducers from "./reducers"; +import { createStore } from "redux"; import { renderRoutes } from "./routes.jsx"; -render(renderRoutes(), document.getElementById("app")); +const store = createStore(reducers); +const el = document.getElementById("app"); + +function render() { + ReactDOM.render(renderRoutes(store), el); +} + +render(); +store.subscribe(render);

web/static/js/containers/AppContainer.jsx

import App from '../layouts/App.jsx'; -import React from 'react'; +import { connect } from "react-redux"; -export default class AppContainer extends React.Component { - constructor(props) { - super(props); - this.state = { - user: undefined, - loading: false, - connected: true, - menuOpen: false, - lists: [] - }; - } +const AppContainer = connect(state => state)(App); - render() { - return (<App {...this.state} {...this.props}/>); - } -}; +export default AppContainer;

web/static/js/reducers/index.js

+const initialState = { + user: undefined, + jwt: undefined, + loading: false, + connected: true, + menuOpen: false, + lists: [] +}; + +export default (state = initialState, action) => { + switch (action.type) { + default: + return state; + } +}

web/static/js/routes.jsx

... import { Router, Route, browserHistory } from 'react-router'; +import { Provider } from "react-redux"; ... -export const renderRoutes = () => ( - <Router history={browserHistory}> - <Route path="/" component={AppContainer}> - <Route path="join" component={AuthPageJoin}/> - <Route path="*" component={NotFoundPage}/> - </Route> - </Router> +export const renderRoutes = (store) => ( + <Provider store={store}> + <Router history={browserHistory}> + <Route path="/" component={AppContainer}> + <Route path="join" component={AuthPageJoin}/> + <Route path="*" component={NotFoundPage}/> + </Route> + </Router> + </Provider> );

Sign Up Actions

Now that we’re using Redux, we’ll want to create actions that describe the sign-up process. Because sign-up is an asynchronous process, we’ll need three actions: SIGN_UP_REQUEST, SIGN_UP_SUCCESS, and SIGN_UP_FAILURE:


export function signUpRequest() {
  return { type: SIGN_UP_REQUEST };
}

export function signUpSuccess(user, jwt) {
  return { type: SIGN_UP_SUCCESS, user, jwt };
}

export function signUpFailure(errors) {
  return { type: SIGN_UP_FAILURE, errors };
}

We’ll also pull the fetch call that we’re using to hit our /api/users endpoint into a helper method called signUp. signUp will dispatch a SIGN_UP_REQUEST action, followed by either a SIGN_UP_SUCCESS or SIGN_UP_FAILURE, depending on the result of our fetch.

Because we’re using asynchronous actions, we’ll need to pull in the Redux Thunk middleware:


npm install --save redux-thunk

And then wire it into our store:


const store = createStore(
  reducers,
  applyMiddleware(thunkMiddleware)
);

Now that our actions are defined, we’ll need to create a matching set of reducers. The SIGN_UP_REQUEST reducer does nothing:


case SIGN_UP_REQUEST:
  return state;

The SIGN_UP_SUCCESS reducer stores the returned user and jwt object in our application state:


case SIGN_UP_SUCCESS:
  return Object.assign({}, state, {
    user: action.user,
    jwt: action.jwt
  });

And the SIGN_UP_FAILURE reducer stores the returned errors (you’ll also notice that we added an errors field to our initialState):


case SIGN_UP_FAILURE:
  return Object.assign({}, state, {
    errors: action.errors
  });

Great. Now we can wrap our JoinPage component in a Redux connect wrapper and pull in errors from our application state:


(state) => {
  return {
    errors: state.errors
  }
}

And create a helper that dispatches our signUp asynchronous action:


(dispatch) => {
  return {
    signUp: (email, password, password_confirm) => {
      return dispatch(signUp(email, password, password_confirm));
    }
  };
}

Now that JoinPage is subscribed to changes to our store, we’ll need to move the logic that transforms our errors into a usable form from its constructor into the render function, and replace the old fetch logic with a call to signUp.

After trying to sign up with these changes, we’ll see a runtime error in our UserMenu component. It’s expecting the newly signed-in user’s email address to be in user.emails[0].address. Changing this component to pull the address from user.email fixes the errors.

The sign-up functionality is complete!

package.json

"redux": "^3.6.0", + "redux-thunk": "^2.1.0", "uglify-js-brunch": ">= 1.0 < 1.8"

web/static/js/actions/index.js

+export const SIGN_UP_REQUEST = "SIGN_UP_REQUEST"; +export const SIGN_UP_SUCCESS = "SIGN_UP_SUCCESS"; +export const SIGN_UP_FAILURE = "SIGN_UP_FAILURE"; + +export function signUpRequest() { + return { type: SIGN_UP_REQUEST }; +} + +export function signUpSuccess(user, jwt) { + return { type: SIGN_UP_SUCCESS, user, jwt }; +} + +export function signUpFailure(errors) { + return { type: SIGN_UP_FAILURE, errors }; +} + +export function signUp(email, password, password_confirm) { + return (dispatch) => { + dispatch(signUpRequest()); + return fetch("/api/users", { + method: "post", + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + user: { + email, + password, + password_confirm + } + }) + }) + .then((res) => res.json()) + .then((res) => { + if (res.errors) { + dispatch(signUpFailure(res.errors)); + return false; + } + else { + dispatch(signUpSuccess(res.user, res.jwt)); + return true; + } + }); + } +}

web/static/js/app.js

... import reducers from "./reducers"; -import { createStore } from "redux"; +import { createStore, applyMiddleware } from "redux"; import { renderRoutes } from "./routes.jsx"; +import thunkMiddleware from "redux-thunk"; -const store = createStore(reducers); +const store = createStore( + reducers, + applyMiddleware(thunkMiddleware) +); const el = document.getElementById("app");

web/static/js/components/UserMenu.jsx

... const { user, logout } = this.props; - const email = user.emails[0].address; + const email = user.email; const emailLocalPart = email.substring(0, email.indexOf('@'));

web/static/js/pages/AuthPageJoin.jsx

... import { Link } from 'react-router'; +import { connect } from "react-redux"; +import { signUp } from "../actions"; -export default class JoinPage extends React.Component { +class JoinPage extends React.Component { constructor(props) { super(props); - this.state = { errors: {} }; + this.state = { + signUp: props.signUp + }; this.onSubmit = this.onSubmit.bind(this); ... - fetch("/api/users", { - method: "post", - headers: { - "Accept": "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - user: { - email, - password, - password_confirm + this.state.signUp(email, password, password_confirm) + .then((success) => { + if (success) { + this.context.router.push('/'); } - }) - }) - .then((res) => { - res - .json() - .then((json) => { - if (json.errors) { - let errors = json.errors.reduce((errors, error) => { - return Object.assign(errors, error); - }); - this.setState({errors}); - } - else { - // TODO: Save `json.user`{:.language-javascript} and `json.jwt`{:.language-javascript} to state - this.context.router.push('/'); - } - }); }); ... render() { - const { errors } = this.state; + const errors = (this.props.errors || []).reduce((errors, error) => { + return Object.assign(errors, error); + }, {}); const errorMessages = Object.keys(errors).map(key => errors[key]); ... }; + +export default connect( + (state) => { + return { + errors: state.errors + } + }, + (dispatch) => { + return { + signUp: (email, password, password_confirm) => { + return dispatch(signUp(email, password, password_confirm)); + } + }; + } +)(JoinPage);

web/static/js/reducers/index.js

+import { + SIGN_UP_REQUEST, + SIGN_UP_SUCCESS, + SIGN_UP_FAILURE, +} from "../actions"; + const initialState = { - user: undefined, - jwt: undefined, - loading: false, - connected: true, - menuOpen: false, - lists: [] + user: undefined, + jwt: undefined, + loading: false, + connected: true, + menuOpen: false, + lists: [], + errors: [] }; ... export default (state = initialState, action) => { - switch (action.type) { - default: - return state; - } + switch (action.type) { + case SIGN_UP_REQUEST: + return state; + case SIGN_UP_SUCCESS: + return Object.assign({}, state, { + user: action.user, + jwt: action.jwt + }); + case SIGN_UP_FAILURE: + return Object.assign({}, state, { + errors: action.errors + }); + default: + return state; + } }

Final Thoughts

Transitioning away from the react-meteor-data package’s createContainer-based container components to a more generalized stat management system like Redux can be a lot of work.

It’s easy to take for granted how simple Meteor makes things.

However, it’s arguable that transitioning to a more predictable state management system is worth the up-front effort. Spending time designing your application’s state, actions, and reducers will leave you with a much more maintainable and predictable system down the road.

Next week we’ll (finally) finish the authentication portion of this project by wiring up our sign-in and sign-out back-end functionality to our Redux-powered front-end.

Clone Meteor Collection References

Written by Pete Corey on Sep 19, 2016.

We recently ran into an interesting situation in a Meteor application we were building for a client.

The application had several types of users. We wanted each type of users to have a distinct set of helpers (defined with the Collection Helpers package).

Unfortunately, Meteor’s heavy use of global variables and the inability to define multiple collection references for a single MongoDB collection made this a more complicated task than we hoped.

Buyers and Sellers

To get a better idea of what we’re talking about, imagine we have “buyers” and “sellers”. Both of these are normal users, so they’ll reference the Meteor.users collection:


Buyers = Meteor.users;
Sellers = Meteor.users;

Now let’s define a few helpers:


Buyers.helpers({
  buy() { ... },
  history() { ... }
});

Sellers.helpers({
  sell() { ... },
  history() { ... }
});

Let’s imagine that buy on Buyers carries out a purchase, and history returns a list of all purchases that buyer has made. Similarly, sell on Sellers carries out a sale, and history returns a list of sales that seller has made.

A Buyer’s Seller History

We can call sell on a Seller, as expected:


let seller = Sellers.findOne({ ... });
seller.sell();

Similarly, we can call buy on a Buyer:


let buyer = Buyers.findOne({ ... });
buyer.buy();

We can also call history on both buyer and seller. However, when we call history on our seller, we don’t get a list of their sales. Instead, we get a list of their purchases.

If we dig a little more, we’ll also notice that we can call sell on our buyer, and buy on our seller.

This is definitely not what we want. These two distinct types of users should have totally separate sets of helpers.

Supersets of Helpers

These issues are happening because we’re defining two sets of helpers on the same Meteor.users collection. After the second call to helpers, Meteor.users has a buy helper, a sell helper, and the seller’s version of the history helper (the buyer’s history was overridden).

Even though we’re using different variables to point to our “different” collections, both variables are pointing to the same collection reference.

Our Meteor.users collection now has a superset of helper functions made up of the union of the Buyers and Sellers helpers.

Cloned Collection References

After considering a few more architecturally complicated solutions to this problem, we realized that an easy solution was sitting right under our noses.

Instead of having Buyers and Sellers reference the Meteor.users collection directly, we could have Buyers and Sellers reference shallow clones of the Meteor.users collection:


Buyers = _.clone(Meteor.users);
Sellers = _.clone(Meteor.users);

This way, each clone would have it’s own internal _helpers function which is used to transform the database document into an object usable by our Meteor application.

Calling Buyers.helpers will define helper functions on the Buyers collection reference, not the Sellers or Meteor.users collection references. Similarly, Sellers.helpers will set up a set of helper functions unique to the Sellers collection reference.

Now calling buyer.history() returns a list of purchases, and seller.history() returns a list of sales. The sell helper doesn’t exist on our buyer user, and buy doesn’t exist on our seller.

Perfect!

Final Thoughts

While this solution worked great for our application, it might not be the best solution to your problem.

Cloning collection references is a delicate thing that may not play nicely with all collection functionality, or all collection-centric Meteor packages.

Also note that deep cloning of collection references does not work at all. While we haven’t looked under the hood to find out what’s going on, we assume that it has to do with breaking callback references or something along those lines.

If you’re facing a problem like this, try to work out a solution that operates within the design principles of Meteor before hacking your way around them. But if all else fails, remember that you have options.

Phoenix Todos - Back-end Authentication

This post is written as a set of Literate Commits. The goal of this style is to show you how this program came together from beginning to end.

Each commit in the project is represented by a section of the article. Click each section's header to see the commit on Github, or check out the repository and follow along.

Written by Pete Corey on Sep 14, 2016.

Enter Guardian

Now we’re getting to the meat of our authentication system. We have our User model set up, but we need to associate users with active sessions.

This is where Guardian comes in. Guardian is an authentication framework that leverages JSON Web Tokens (JWT) and plays nicely with Phoenix Channels.

To use Guardian, we’ll first add it as a depenency to our application:


{:guardian, "~> 0.12.0"}

Next, we need to do some configuring:


config :guardian, Guardian,
  allowed_algos: ["HS512"], # optional
  verify_module: Guardian.JWT,  # optional
  issuer: "PhoenixTodos",
  ttl: { 30, :days },
  verify_issuer: true, # optional
  secret_key: %{"kty" => "oct", "k" => System.get_env("GUARDIAN_SECRET_KEY")},
  serializer: PhoenixTodos.GuardianSerializer

You’ll notice that I’m pulling my secret_key from my system’s environment variables. It’s a bad idea to keep secrets in version control.

I also specified a serializer module. This is Guardian’s bridge into your system. It acts as a translation layer between Guardian’s JWT and your User model.

Because it’s unique to our system, we’ll need to build the PhoenixTodos.GuardianSerializer ourselves.

Our serializer will need two fuctions. The first, for_token translates a User model into a token string. An invalid User should return an :error:


test "generates token for valid user", %{user: user} do
  assert {:ok, _} = GuardianSerializer.for_token(user)
end

test "generates error for invalid user", %{} do
  assert {:error, "Invalid user"} = GuardianSerializer.for_token(%{})
end

Thanks to Elixir’s pattern matching, for_token is a very simple function:


def for_token(%User{id: id}), do: {:ok, "User:#{id}"}
def for_token(_), do: {:error, "Invalid user"}

Similarly, we need to define a from_token function, which takes a token string and returns the corresponding User model:


test "finds user from valid token", %{user: user} do
  {:ok, token} = GuardianSerializer.for_token(user)
  assert {:ok, _} = GuardianSerializer.from_token(token)
end

test "doesn't find user from invalid token", %{} do
  assert {:error, "Invalid user"} = GuardianSerializer.from_token("bad")
end

To implement this, we’ll pull the User id out of the token string, and look it up in the database:


def from_token("User:" <> id), do: {:ok, Repo.get(User, String.to_integer(id))}
def from_token(_), do: {:error, "Invalid user"}

Now that we’ve finished our serializer, we’re in a position to wire up the rest of our authentication system!

config/config.exs

... binary_id: false + +config :guardian, Guardian, + allowed_algos: ["HS512"], # optional + verify_module: Guardian.JWT, # optional + issuer: "PhoenixTodos", + ttl: { 30, :days }, + verify_issuer: true, # optional + secret_key: %{"kty" => "oct", "k" => System.get_env("GUARDIAN_SECRET_KEY")}, + serializer: PhoenixTodos.GuardianSerializer

lib/phoenix_todos/guardian_serializer.ex

+defmodule PhoenixTodos.GuardianSerializer do + @behavior Guardian.Serializer + + alias PhoenixTodos.{User, Repo} + + def for_token(%User{id: id}), do: {:ok, "User:#{id}"} + def for_token(_), do: {:error, "Invalid user"} + + def from_token("User:" <> id), do: {:ok, Repo.get(User, String.to_integer(id))} + def from_token(_), do: {:error, "Invalid user"} +end

mix.exs

... {:mix_test_watch, "~> 0.2", only: :dev}, - {:comeonin, "~> 2.0"}] + {:comeonin, "~> 2.0"}, + {:guardian, "~> 0.12.0"}] end

mix.lock

-%{"comeonin": {:hex, :comeonin, "2.5.2"}, +%{"base64url": {:hex, :base64url, "0.0.1"}, + "comeonin": {:hex, :comeonin, "2.5.2"}, "connection": {:hex, :connection, "1.0.4"}, "gettext": {:hex, :gettext, "0.11.0"}, + "guardian": {:hex, :guardian, "0.12.0"}, + "jose": {:hex, :jose, "1.8.0"}, "mime": {:hex, :mime, "1.0.1"}, "postgrex": {:hex, :postgrex, "0.11.2"}, - "ranch": {:hex, :ranch, "1.2.1"}} + "ranch": {:hex, :ranch, "1.2.1"}, + "uuid": {:hex, :uuid, "1.1.4"}}

test/lib/guardian_serializer_test.exs

+defmodule PhoenixTodos.GuardianSerializerTest do + use ExUnit.Case, async: true + + alias PhoenixTodos.{User, Repo, GuardianSerializer} + + setup_all do + user = User.changeset(%User{}, %{ + email: "email@example.com", + password: "password" + }) + |> Repo.insert! + + {:ok, user: user} + end + + test "generates token for valid user", %{user: user} do + assert {:ok, _} = GuardianSerializer.for_token(user) + end + + test "generates error for invalid user", %{} do + assert {:error, "Invalid user"} = GuardianSerializer.for_token(%{}) + end + + test "finds user from valid token", %{user: user} do + {:ok, token} = GuardianSerializer.for_token(user) + assert {:ok, _} = GuardianSerializer.from_token(token) + end + + test "doesn't find user from invalid token", %{} do + assert {:error, "Invalid user"} = GuardianSerializer.from_token("bad") + end +end

Sign-Up Route and Controller

The first step to implementing authentication in our application is creating a back-end sign-up route that creates a new user in our system.

To do this, we’ll create an "/api/users" route that sends POST requests to the UserController.create function:


post "/users", UserController, :create

We expect the user’s email and password to be sent as parameters to this endpoint. UserController.create takes those params, passes them into our User.changeset, and then attempts to insert the resulting User into the database:


User.changeset(%User{}, params)
|> Repo.insert

If the insert fails, we return the changeset errors to the client:


conn
|> put_status(:unprocessable_entity)
|> render(PhoenixTodos.ApiView, "error.json", error: changeset)

Otherwise, we’ll use Guardian to sign the new user’s JWT and return the jwt and user objects down to the client:


{:ok, jwt, _full_claims} = Guardian.encode_and_sign(user, :token)
conn
|> put_status(:created)
|> render(PhoenixTodos.ApiView, "data.json", data: %{jwt: jwt, user: user})

Now all a user needs to do to sign up with our Todos application is send a POST request to /api/users with their email and password. In turn, they’ll receive their JWT which they can send along with any subsequent requests to verify their identity.

test/controllers/user_controller_test.exs

+defmodule PhoenixTodos.UserControllerTest do + use PhoenixTodos.ConnCase + + test "creates a user", %{conn: conn} do + conn = post conn, "/api/users", user: %{ + email: "email@example.com", + password: "password" + } + %{ + "jwt" => _, + "user" => %{ + "id" => _, + "email" => "email@example.com" + } + } = json_response(conn, 201) + end + + test "fails user validation", %{conn: conn} do + conn = post conn, "/api/users", user: %{ + email: "email@example.com", + password: "pass" + } + %{ + "errors" => [ + %{ + "password" => "should be at least 5 character(s)" + } + ] + } = json_response(conn, 422) + end +end

web/controllers/user_controller.ex

+defmodule PhoenixTodos.UserController do + use PhoenixTodos.Web, :controller + + alias PhoenixTodos.{User, Repo} + + def create(conn, %{"user" => params}) do + User.changeset(%User{}, params) + |> Repo.insert + |> handle_insert(conn) + end + + defp handle_insert({:ok, user}, conn) do + {:ok, jwt, _full_claims} = Guardian.encode_and_sign(user, :token) + conn + |> put_status(:created) + |> render(PhoenixTodos.ApiView, "data.json", data: %{jwt: jwt, user: user}) + end + defp handle_insert({:error, changeset}, conn) do + conn + |> put_status(:unprocessable_entity) + |> render(PhoenixTodos.ApiView, "error.json", error: changeset) + end +end

web/models/user.ex

... use PhoenixTodos.Web, :model + @derive {Poison.Encoder, only: [:id, :email]}

web/router.ex

... + scope "/api", PhoenixTodos do + pipe_through :api + + post "/users", UserController, :create + end + scope "/", PhoenixTodos do ... - # Other scopes may use custom stacks. - # scope "/api", PhoenixTodos do - # pipe_through :api - # end end

web/views/api_view.ex

+defmodule PhoenixTodos.ApiView do + use PhoenixTodos.Web, :view + + def render("data.json", %{data: data}) do + data + end + + def render("error.json", %{error: changeset = %Ecto.Changeset{}}) do + errors = Enum.map(changeset.errors, fn {field, detail} -> + %{} |> Map.put(field, render_detail(detail)) + end) + + %{ errors: errors } + end + + def render("error.json", %{error: error}), do: %{error: error} + + def render("error.json", %{}), do: %{} + + defp render_detail({message, values}) do + Enum.reduce(values, message, fn {k, v}, acc -> String.replace(acc, "%{#{k}}", to_string(v)) end) + end + + defp render_detail(message) do + message + end + +end

Sign-In Route and Controller

Now that users have the ability to join our application, how will they sign into their accounts?

We’ll start implementing sign-in functionality by adding a new route to our Phoenix application:


post "/sessions", SessionController, :create

When a user sends a POST request to /sessions, we’ll route them to the create function in our SessionController module. This function will attempt to sign the user in with the credentials they provide.

At a high level, the create function will be fairly straight-forward. We want to look up the user based on the email they gave, check if the password they supplied matches what we have on file:


def create(conn, %{"email" => email, "password" => password}) do
  user = get_user(email)
  user
  |> check_password(password)
  |> handle_check_password(conn, user)
end

If get_user returns nil, we couldn’t find the user based on the email address they provided. In that case, we’ll return false from check_password:


defp check_password(nil, _password), do: false

Otherwise, we’ll use Comeonin to compare the hashed password we have saved in encrypted_password with the hash of the password the user provided:


defp check_password(user, password) do
  Comeonin.Bcrypt.checkpw(password, user.encrypted_password)
end

If all goes well, we’ll return a jwt and the user object for the now-authenticated user:


render(PhoenixTodos.ApiView, "data.json", data: %{jwt: jwt, user: user})

We can test this sign-in route/controller combination just like we’ve tested our sign-up functionality.

test/controllers/session_controller_test.exs

+defmodule PhoenixTodos.SessionControllerTest do + use PhoenixTodos.ConnCase + + alias PhoenixTodos.{User, Repo} + + test "creates a session", %{conn: conn} do + %User{} + |> User.changeset(%{ + email: "email@example.com", + password: "password" + }) + |> Repo.insert! + + conn = post conn, "/api/sessions", email: "email@example.com", password: "password" + %{ + "jwt" => _jwt, + "user" => %{ + "id" => _id, + "email" => "email@example.com" + } + } = json_response(conn, 201) + end + + test "fails authorization", %{conn: conn} do + conn = post conn, "/api/sessions", email: "email@example.com", password: "wrong" + %{ + "error" => "Unable to authenticate" + } = json_response(conn, 422) + end +end

web/controllers/session_controller.ex

+defmodule PhoenixTodos.SessionController do + use PhoenixTodos.Web, :controller + + alias PhoenixTodos.{User, Repo} + + def create(conn, %{"email" => email, "password" => password}) do + user = get_user(email) + user + |> check_password(password) + |> handle_check_password(conn, user) + end + + defp get_user(email) do + Repo.get_by(User, email: String.downcase(email)) + end + + defp check_password(nil, _password), do: false + defp check_password(user, password) do + Comeonin.Bcrypt.checkpw(password, user.encrypted_password) + end + + defp handle_check_password(true, conn, user) do + {:ok, jwt, _full_claims} = Guardian.encode_and_sign(user, :token) + conn + |> put_status(:created) + |> render(PhoenixTodos.ApiView, "data.json", data: %{jwt: jwt, user: user}) + end + defp handle_check_password(false, conn, _user) do + conn + |> put_status(:unprocessable_entity) + |> render(PhoenixTodos.ApiView, "error.json", error: "Unable to authenticate") + end + +end

web/router.ex

... plug :accepts, ["json"] + plug Guardian.Plug.VerifyHeader + plug Guardian.Plug.LoadResource end ... post "/users", UserController, :create + + post "/sessions", SessionController, :create end

Sign-Out Route and Controller

The final piece of our authorization trifecta is the ability for users to sign out once they’ve successfully joined or signed into the application.

To implement sign-out functionality, we’ll want to create a route that destroys a user’s session when its called by an authenticated user:


delete "/sessions", SessionController, :delete

This new route points to SessionController.delete. This function doesn’t exist yet, so let’s create it:


def delete(conn, _) do
  conn
  |> revoke_claims
  |> render(PhoenixTodos.ApiView, "data.json", data: %{})
end

revoke_claims will be a private function that simply looks up the current user’s token and claims, and then revokes them:


{:ok, claims} = Guardian.Plug.claims(conn)
Guardian.Plug.current_token(conn)
|> Guardian.revoke!(claims)

In implementing this feature, we cleaned up our SessionControllerTest module a bit. We added a create_user function, which creates a user with a given email address and password, and a create_session function that logs that user in.

Using those functions we can create a user’s session, and then construct a DELETE request with the user’s JWT (session_response["jwt"]) in the "authorization" header. If this request is successful, we’ve successfully deleted the user’s session.

test/controllers/session_controller_test.exs

... - test "creates a session", %{conn: conn} do + defp create_user(email, password) do %User{} |> User.changeset(%{ - email: "email@example.com", - password: "password" - }) + email: email, + password: password + }) |> Repo.insert! + end - conn = post conn, "/api/sessions", email: "email@example.com", password: "password" - %{ - "jwt" => _jwt, - "user" => %{ - "id" => _id, - "email" => "email@example.com" - } - } = json_response(conn, 201) + defp create_session(conn, email, password) do + post(conn, "/api/sessions", email: email, password: password) + |> json_response(201) + end + + test "creates a session", %{conn: conn} do + create_user("email@example.com", "password") + + response = create_session(conn, "email@example.com", "password") + + assert response["jwt"] + assert response["user"]["id"] + assert response["user"]["email"] end ... end + + test "deletes a session", %{conn: conn} do + create_user("email@example.com", "password") + session_response = create_session(conn, "email@example.com", "password") + + conn + |> put_req_header("authorization", session_response["jwt"]) + |> delete("/api/sessions") + |> json_response(200) + end + end

web/controllers/session_controller.ex

... + def delete(conn, _) do + conn + |> revoke_claims + |> render(PhoenixTodos.ApiView, "data.json", data: %{}) + end + + defp revoke_claims(conn) do + {:ok, claims} = Guardian.Plug.claims(conn) + Guardian.Plug.current_token(conn) + |> Guardian.revoke!(claims) + conn + end + def create(conn, %{"email" => email, "password" => password}) do

web/router.ex

... post "/sessions", SessionController, :create + delete "/sessions", SessionController, :delete end

Final Thoughts

As a Meteor developer, it seems like we’re spending an huge amount of time implementing authorization in our Phoenix Todos application. This functionality comes out of the box with Meteor!

The truth is that authentication is a massive, nuanced problem. Meteor’s Accounts system is a shining example of what Meteor does right. It abstracts away an incredibly tedious, but extremely important aspect of building web applications into an easy to use package.

On the other hand, Phoenix’s approach of forcing us to implement our own authentication system has its own set of benefits. By implementing authentication ourselves, we always know exactly what’s going on in every step of the process. There is no magic here. Complete control can be liberating.

Check back next week when we turn our attention back to the front-end, and wire up our sign-up and sign-in React templates!