Phoenix + React: love story. RePh 2.

Roman Chvanikov
9 min readMay 25, 2016

--

Want to start building RePh app without carrying about scaffolding? Good news: I just released RePh app generator, take a look!

In RePh 1 part of the article we’ve created a very basic app. Today we are going to improve it with authorization and different apps for guests and authorized users — it is a common task to have a landing page and application page which provides completely different layout, styles and JS. Another feature not covered in similar tutorials is authorization via websockets and storing auth status in session — that’s what you’ll learn as well.

Complete app code is available at Github, every step lives in it’s own commit: https://github.com/chvanikoff/reph2

The demo is available at https://reph2.herokuapp.com but is only available 18h/day due to Heroku free dyno limitation. If you see `Application Error` message — this is the case.

Primary tech stack used.

  • Elixir 1.2.5
  • Node 4.4.3
  • NPM 3.9.2
  • WebPack 1.13.0
  • Phoenix 1.1.4
  • React 15.0.2

Getting started.

We will use RePh 1 app as a base and to get started you should clone the repo. Again, I will use ~/phoenix as a root directory for the app but you can choose whichever you want.

cd ~/phoenix
git clone https://github.com/chvanikoff/reph.git
cd reph
mix deps.get && npm install

Hello landing.

Now let’s think about our app’s file structure — since we are going to have 2 bundles (landing and app), it doesnt’t make sense to keep all the files right in web/static/js and web/static/styles. Instead, we will use 2 subdirectories — “landing” and “app” — in both js and styles folder. Create the directories and move all files from priv/static/js to priv/static/js/landing and from priv/static/styles to priv/static/styles/landing:

# First, let's move all JS files to new path
cd web/static/js
mkdir landing
# Following will throw an error but it will do what we need - to
# move all the files we have to "landing" directory. If you know a
# better way of doing this, let me know.
mv ./* landing
mkdir app
# And now exactly the same for styles
cd ../styles
mkdir landing
mv ./* landing
mkdir app
# Let's get back to our root before next steps
cd ../../../

We will have 2 different applications, so let’s split images into landing and app as well — just create 2 directories in web/static/assets/images and move phoenix.png to images/landing:

cd web/static/assets/images
mkdir landing app
mv phoenix.png landing
# Let's get back to our root before next steps
cd -

Now when we know about our app file structure, we can update Webpack config files. There are a few changes to do:

  • in webpack.config.js change all references of web/static/js and web/static/styles with web/static/js/landing and web/static/styles/landing accordingly
  • update images and fonts loaders to put files into images/landing instead of just images
  • update output filename to be js/landing.js

Also, I’ve decided to cleanup url-loaders declaration a bit and created a helper function URLLoader to replace current images/fonts loaders with a cleaner representation. Here is webpack.config.js after all changes done:

Once done, update webpack.server.config.js as well. We will name our file and library to be generated “landing”:

Now we are done with Webpack configs and the rest of our app should be updated to work with new paths.

Since we moved our images to images/landing, logo path in styles must be changed. Update web/static/styles/landing/index.less:

We also changed server bundle name to landing.js so change reph.js to it in web/controllers/page_controller.ex

Next, let’s create a separate layout for landing: web/templates/layout/landing.html.eex. It will be the same as app.html.eex but js and css would be different. Copy app.html.eex layout

cd web/templates/layout
cp ./app.html.eex ./landing.html.eex

and update paths to static files. Finally, the landing.html.eex will look like this:

To use this layout instead of default app.html.eex, we have to update page_controller.ex with a call to put_layout/2 function before rendering:

Next, let’s remove visitors from the landing — it will be kind of secret information which is only visible for authorized users. Open web/static/js/landing/components/Main/index.js and remove everything related to visitors info displaying as well as connect function call — there’s no need in state anymore. Here is the result:

At this point our setup is done, you can launch the app with mix phoenix.server and check that everything is correctly bundled in new paths and looks exactly same as before.

Hello auth.

Next thing to do in our app is authorization. We will use Comeonin 2.4, Guardian 0.11 and GuardianDB 0.5 (since latest 0.6 requires Ecto 2.0 which is still having RC status) so add all of these into deps and applications lists of mix.exs and then run “mix deps.get”:

def application do
[mod: {Reph, []},
applications: [:phoenix, :phoenix_html, :cowboy,
:logger, :gettext, :phoenix_ecto, :postgrex,
:std_json_io, :guardian, :comeonin]]
end
# ...defp deps do
[{:phoenix, "~> 1.1.4"},
{:postgrex, ">= 0.0.0"},
{:phoenix_ecto, "~> 2.0"},
{:phoenix_html, "~> 2.4"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:gettext, "~> 0.9"},
{:cowboy, "~> 1.0"},
{:std_json_io, "~> 0.1"},
{:comeonin, "~> 2.4"},
{:guardian, "~> 0.11"},
{:guardian_db, "~> 0.5"}]
end

Guardian requires configuration so open config/config.exs and add configs for guardian and guardian_db:

config :guardian, Guardian,
issuer: "reph2",
ttl: {30, :days},
secret_key: "a big secured secret key",
serializer: Reph.Guardian.Serializer,
hooks: GuardianDb
config :guardian_db, GuardianDb,
repo: Reph.Repo,
schema_name: "tokens"

Tip: you can generate secret key with “mix phoenix.gen.secret” but you need to save the config first because running tasks requires project compilation which will fail without the config in place.

As you can see, we will use GuardianDB to persist tokens which means we need a migration to create “tokens” table. Create migration with

mix ecto.gen.migration create_tokens

and update priv/repo/migrations/%DATETIME%_create_tokens.exs:

Now let’s start with a simple User model which will only have id, email and password (hash). Also we will need 2 virtual fields for validation purpose — password_plain and terms_confirmed. We’ll generate the model with Phoenix generator:

mix phoenix.gen.model User users email password

Since email should be unique, add such a key to the column in priv/repo/migrations/%DATETIME%_create_user.exs:

Now you can run “mix ecto.setup” to create database and run 2 migrations — for users and guardian tokens. To add virtual fields to User model, open web/models/user.ex and update schema:

schema "users" do
field :email, :string
field :password, :string
field :password_plain, :string, virtual: true
field :terms_confirmed, :boolean, virtual: true
timestamps
end

Also I suggest to add alias for Reph.Repo — just add this line to the beginning of User model file:

alias Reph.Repo

List of required fields will contain data sent from client and optional fields will only have password, where we will put hash of password_plain:

@required_fields ~w(email password_plain terms_confirmed)
@optional_fields ~w(password)

In changeset/2 function we will perform all validations and set password field. Setting password field means hashing of virtual password_plain field and setting the hash to password field. To do this, let’s create a function cs_encrypt_password (“cs” stands for changeset) which will set the field when all validations passed and just return the same changeset if not:

defp cs_encrypt_password(%Ecto.Changeset{valid?: true, changes: %{password_plain: pwd}} = cs) do
put_change(cs, :password, Comeonin.Bcrypt.hashpwsalt(pwd))
end
defp cs_encrypt_password(cs), do: cs

And here is what changeset/2 function will look like:

def changeset(model, params \\ :empty) do
model
|> cast(params, @required_fields, @optional_fields)
|> validate_format(:email, ~r/@/, message: "Email format is not valid")
|> validate_length(:password_plain, min: 5, message: "Password should be 5 or more characters long")
|> validate_confirmation(:password_plain, message: "Password confirmation doesn’t match")
|> unique_constraint(:email, message: "This email is already taken")
|> validate_change(:terms_confirmed, fn
_, true -> []
_, _ -> [terms_confirmed: "Please confirm terms and conditions"]
end)
|> cs_encrypt_password()
end

In the User model we will also process users signin/signup actions. Signup will be very simple since all related logic is already defined in changeset/2:

def signup(params) do
%__MODULE__{}
|> changeset(params)
|> Repo.insert()
end

Signin will require a function to check password based on user model loaded and password provided by user. If user was not loaded (not found by email), we will perform a dummy password check. The reason for this check is in order to make user enumeration by timing responses more difficult. We might want this check only on real prod so let’s also add an if-condition to not do this unless we are running with MIX_ENV=prod:

defp check_password(%__MODULE__{password: hash} = user, password) do
case Comeonin.Bcrypt.checkpw(password, hash) do
true -> {:ok, user}
false -> {:error, "Invalid email or password"}
end
end
defp check_password(nil, _password) do
if Mix.env == :prod do
Comeonin.Bcrypt.dummy_checkpw()
end
{:error, "Invalid email or password"}
end

Now when we can check passwords, let’s write signin function:

def signin(params) do
email = Map.get(params, "email", "")
password = Map.get(params, "password", "")
__MODULE__
|> Repo.get_by(email: String.downcase(email))
|> check_password(password)
end

And here is complete code for web/model/user.ex:

Once you have User model in place, create Guardian serializer which will handle it’s serialization. Open file lib/reph/guardian/serializer.ex and put the following:

To login and register, we will use websocket connection as a transport. Once user successfully registered, login action will be immediately fired to authorize him. Since we need user auth status on server side (stored in session) to allow or disallow access to our app, in response to login we’ll send user a short-lived token. This token must be used in very limited timeframe at a proxy verification page. If token successfully validated, we will authorize user with Guardian and redirect him to the app.

Let’s start with an Auth channel — web/channels/auth_channel.ex and add signin/signup handlers where we will send short-lived auth token to client in case of success:

And add it to web/channels/user_socket.ex channels section:

Once client received {:ok, jwt} response, it should redirect user to the proxy page I’ve mentioned above. URL will be “/?token=%JWT%” and handler will locate in PageConroller. Update web/controllers/page_controller.ex with the handler (remember to put it before “index(conn, _params)” function because later matches everything):

def index(conn, %{"token" => token}) do
case Guardian.decode_and_verify(token) do
{ :ok, claims } ->
Guardian.revoke!(token)
{:ok, user} = Guardian.serializer.from_token(claims["sub"])
conn
|> Guardian.Plug.sign_in(user)
|> redirect(to: "/app")
_ ->
redirect(conn, to: "/")
end
end

Now when we have all the server code in place, let’s move to client-side.

To connect to auth channel, dispatch another channel_join action at web/static/js/landing/store/index.js:

Now, we need an auth reducer where we will keep login failure status and registration errors:

Don’t forget to add it to list of reducers in web/static/js/landing/reducers/index.js:

To process forms inputs, we need a new set of actions, create web/static/js/landing/actions/auth.js and add register/login actions:

And finally, we’ll create registration component. It will be a form with onSubmit handler calling “register” auth action and list of errors if any. Create web/static/js/landing/components/Registration/index.js:

Now the last thing to do is login form. Create web/static/js/landing/components/Login/index.js:

The forms are using some css-classes I’ve got styles for, open web/static/styles/landing/index.less and insert the following to the bottom of the file:

Now update web/static/js/landing/routes/index.js with routes to the auth components:

and web/static/js/landing/containers/App/index.js with links to login/register:

Authorization is done! Launch the app with “mix phoenix.server” — registration and login should work as expected: show errors if any and redirect to /app (which is not working so far) if everything is ok.

Hello app.

The last thing in our app will be the app itself. After login, user is redirected to “/app” but currently there’s nothing but error, let’s fix this. First, we need 2 more webpack configs: webpack.app.config.js and webpack.app.server.config.js. I’ve tried to avoid this and put both landing and app into 1 config but it didn’t work (most probably because my webpack-fu isn’t strong enough), let me know if you are aware of how to do this.

The configs will be almost same as for landing but will bundle “app” instead of “landing” and will not include copying of assets directory because we already do this in our landing webpack config. Here is the webpack.app.config.js:

and here is webpack.app.server.config.js:

I also suggest renaming webpack.config.js to webpack.landing.config.js and webpack.server.config.js to webpack.landing.server.config.js. Too long, I know, but that’s the cost of explicitness.

Now when we have 4 watchers, it can be a good idea to add a function to generate similar watchers based on config name instead of copy same code again and again. Here is final config/dev.exs:

Next, create web/controllers/app_controller.ex, there’s nothing new in it except we will use Guardian plug to only allow authenticated users to access the controller. Also, logout action will be handled by the controller:

and let’s update Phoenix router: add new scope “/app” and use 2 Guardian plugs — VerifySession and LoadResource — for it via new “app” pipeline:

Since template will be the same as for PageController, just copy it to the app directory:

cp -r web/templates/page web/templates/app

App layout already have correct css/js paths in place so the only thing left is to create an empty view file for app controller — web/views/app_view.ex:

And that’s all we need on server side. The rest is the app client code.

Let’s prepare our app static files:

cd web/static# most of code will remain the same
cp -r js/landing/* js/app/
cp -r styles/landing/* styles/app/
# these files are not needed for app
cd js/app
rm -rf actions/auth.js components/Login components/Registration reducers/auth.js
# Let's get back to our root before next steps
cd ../../../../

Next, update web/static/js/app/routes/index.js:

We removed auth-related stuff so let’s clean up our code accordingly. Remove auth reducer from web/static/js/app/reducers/index.js:

And remove connection to auth channel from web/static/js/app/store/index.js:

I’m going to use the Dashboard template example from Bootstrap for the app so let’s update app styles first. We will remove everything but Bootstrap import from web/static/styles/app/index.less and insert styles provided with the template. Finally, the styles file will look like following (I’ve converted CSS to less automatically):

Now, update web/static/js/app/containers/App/index.js:

and web/static/js/app/components/Main/index.js:

Now we have fully functional app. Run “mix phoenix.server” and play around with authorization and two different apps. That’s a good start point for something great!

Stay tuned!

In next part we will either write more useful application (users management, for example, or will start developing yet another blog), or prepare the current one for real production with refactoring and caching server-rendered JS. Comments are yours to vote for the “RePh 3” topic!

--

--