Discover Elixir & Phoenix

Back to All Courses

Lesson 12

# From website to web app

In the previous lesson, we made it possible for the user to sign up and log in to our website. In this chapter, we want to take advantage of this new functionality to make the UI of our app more dynamic!

This is sometimes referred to as the difference between a website (which has the same static content for everyone), and a web app.

Web apps typically offer a "logged-in" experience that lets the user read and write content.

## Extracting the header into a template

The first thing we want to do is to change the header depending on if the user is logged-in our logged-out. We can refer to our Sketch file to see how we intend to make it look:

Alright, let's implement that! Since we're going to add quite a bit of logic to our header now, it makes sense to extract it into its own template. We'll put that template in the layout-folder (that makes sense, because a header is part of the layout, right?):

# lib/messengyr_web/templates/layout/header.html.eex

<%= link "", class: "logo", to: page_path(@conn, :index) %>

</header>

Now we can remove the header-tag from our app.html.eex template, and replace it with a simple render function:

# lib/messengyr_web/templates/layout/app.html.eex

<!-- ... -->

<html lang="en">

<!-- ... -->

<body class=<%= @conn.path_info %>>
<div class="container">

<%= render MessengyrWeb.LayoutView, "header.html", assigns %>

<!-- ... -->

</div>
<!-- ... -->
</body>
</html>

Nothing fancy here! We just specify the view (LayoutView), the template ("header.html"), and then we use assigns in order to be able to use @conn in our template (which we will need in order to retrieve the logged-in user, as you saw in the previous lesson).

If you reload the page, it should still look exactly the same, which is a good sign!

Now comes the fun part! Remember how we learned that views "transform our data to make it easy to use when rendering a template"? Well, that's exactly what we're going to do now. We want to make the logic in our template as simple as possible, by defining good functions in our Layout View.

The first thing we want to know in our template is whether the user is logged-in or not. For that, we'll use an if-statement in our header template to render different things depending on the result of the logged_in?/1-function:

# lib/messengyr_web/templates/layout/header.html.eex

<%= link "", class: "logo", to: page_path(@conn, :index) %>

<%= if logged_in?(@conn) do %>
<p>Logged in!</p>
<% else %>
<% end %>

</header>

We obviously have to define this logged_in?/1-function now, or else we get a compilation error.

A tip when using libraries like Guardian: if you're too lazy to check the documentation (like I am sometimes), you can always use the special __info__ function to get information about Elixir modules. To get a list of all the functions that the Guardian.Plug module has for example, you simply use IO.inspect Guardian.Plug.__info__(:functions). You should then see authenticated? among the list of functions in your console, along with the number of parameters it takes. That's the function that we're going to use:

# lib/messengyr_web/views/layout_view.ex

defmodule MessengyrWeb.LayoutView do
use MessengyrWeb, :view

def logged_in?(conn) do
Guardian.Plug.authenticated?(conn, [])
end
end

Even though it's covered by the awesome logo, you can see that the "Logged in"-text renders as we expected!

## Showing a username and an avatar

Just showing "Logged in!" in the header is kind of lame, so the next step is to show the user's username and the avatar. First, we make some changes to the template and call the username/1 function inside the if-statement:

# lib/messengyr_web/templates/layout/header.html.eex

<!-- ... -->

<%= if logged_in?(@conn) do %>

<div class="profile-container">
</div>

<% else %>
<!-- ... -->

</header>

Now we need to define this username/1 function in our view. Here we can just use Guardian's current_resource function that we saw earlier, and then extract the username from that using pattern matching:

# lib/messengyr_web/views/layout_view.ex

defmodule MessengyrWeb.LayoutView do
# ...

# We get the user:
user = Guardian.Plug.current_resource(conn)

# We extract the username with pattern matching:

end

end

Next, we want to also add an avatar. For this, we don't want to make the user upload a profile picture or anything -- that's beyond the scope of this project. Instead we'll use Gravatar to fetch whatever profile picture (if they have one) that is linked to the user's email address.

If we check Gravatar's documentation for image requests, we learn that we need to hash the email address in the image URL. Let's do that in a new function!

# lib/messengyr_web/views/layout_view.ex

defmodule MessengyrWeb.LayoutView do

# ...

def avatar(conn) do
user = Guardian.Plug.current_resource(conn)

# We extract the email with pattern matching:
%{email: email} = user

# Hash the email address and make it URL-compliant:
hash_email = :crypto.hash(:md5, email) |> Base.encode16 |> String.downcase

# Return the image URL:
"http://www.gravatar.com/avatar/#{hash_email}"
end
end

Now we can add our image tag to the template, and call the avatar/1-function:

# lib/messengyr_web/templates/layout/header.html.eex

<!-- ... -->

<div class="profile-container">
<img src=<%= avatar(@conn) %> /> <!-- Add this line! -->
</div>

<!-- ... -->

</header>

We're almost done with the header. The last thing we want to add is a "log out"-button, next to the username. The button will just be a link to the URL /logout which will be connected to a function in our Page Controller.

You know the drill for new routes by now! We start by adding a line to our router file:

# lib/messengyr_web/router.ex

defmodule MessengyrWeb.Router do

# ...

scope "/", MessengyrWeb do
# ...

get "/logout", PageController, :logout
end

# ...

end

Next we add the logout/2-function to our PageController. In it, we'll just use Guardian's sign_out function, set a flash message, and redirect to the landing page:

# lib/messengyr_web/controllers/page_controller.ex

defmodule MessengyrWeb.PageController do
# ...

def logout(conn, _params) do
conn
|> Guardian.Plug.sign_out
|> put_flash(:info, "Signed out successfully!")
|> redirect(to: "/")
end

end

<!-- lib/messengyr_web/templates/layout/header.html.eex -->

<!-- ... -->

<%= if logged_in?(@conn) do %>

<%= link "Log out", to: page_path(@conn, :logout) %>

<div class="profile-container">
<!-- ... -->

</header>

Once that's done, you should be able to log in and log out as much as you want!

## Fixing the landing page forms

There's a little flaw in our landing page that we've overlooked until now -- the login and signup boxes don't work! Right now, we can only log in by going to the /login route, and we can only sign up by going to /signup.

These boxes are currently useless!

Thankfully, we already have all the logic ready, so we just need to connect them to the relevant actions in our Page controller.

First of all, we need a **User changeset **on the landing page for the signup form to work, so we need to set it in our existing index/2-function in the controller:

# lib/messengyr_web/controllers/page_controller.ex

defmodule MessengyrWeb.PageController do

# ...

def index(conn, _params) do
changeset = Accounts.register_changeset()

render conn, user_changeset: changeset
end

# ...

end

Next, we simply **replace the form in the login box **with the dynamic form from login.html.eex, and the form in the signup box with the one from signup.html.eex. The final index.html.eex template should look like this:

<!-- lib/messengyr_web/templates/page/index.html.eex -->

<div class="tagline">
<h1>Welcome to Messengyr!</h1>
<h2>A messenger clone built in Elixir + Phoenix</h2>
</div>

<div class="action-boxes">

<%= form_for @conn, page_path(@conn, :login_user), [as: :credentials], fn f -> %>

<% end %>
</div>

<div class="card signup">
<h5>
<strong>
New to Messengyr?
</strong>

</h5>

<%= form_for @user_changeset, page_path(@conn, :create_user), fn f -> %>
<%= text_input f, :email, placeholder: "Email" %>

<% end %>

</div>

</div>

## Authenticated routes

Our final task in this lesson is to create a new route (/messages), that's only available for logged-in users. First we create the route:

# lib/messengyr_web/router.ex

defmodule MessengyrWeb.Router do

# ...

scope "/", MessengyrWeb do
# ...
get "/logout", PageController, :logout

get "/messages", ChatController, :index # Add this line!
end

# ...

end

Since this route is for logged-in users only, it's going to work in a lightly different way than our previous routes. Therefore, we use a new controller for it -- the ChatController. We need to create a file for it, and add its index/2-function:

# lib/messengyr_web/controllers/chat_controller.ex

defmodule MessengyrWeb.ChatController do
use MessengyrWeb, :controller

def index(conn, _params) do
render conn
end

end

As you know, we also need a view for this controller if we want the template to render, so let's create ChatView:

# lib/messengyr_web/views/chat_view.ex

defmodule MessengyrWeb.ChatView do
use MessengyrWeb, :view
end

Finally we need a template. By following Phoenix's conventions, we know that this needs to be created at messengyr_web/templates/chat/index.html.eex. The file can be completely empty for now.

If you followed the instructions, you should be able to see an empty page when you go to /messages.

Now we want this route to only be available to users who are logged-in. For this, we can use Guardian's EnsureAuthenticated plug in the ChatController:

# lib/messengyr_web/controllers/chat_controller.ex

defmodule MessengyrWeb.ChatController do
use MessengyrWeb, :controller

plug Guardian.Plug.EnsureAuthenticated, handler: __MODULE__

# ...

The plug's handler should point to a module that contains an auth_error/3 function. In that function, we specify what happens if the user is not logged-in, yet tries to hit a route connected to this controller.

Since we currently only have a single controller that requires authentication (ChatController), we can simply set this handler to __MODULE__ to tell Guardian to look for the auth_error/3-function in the current module.

Let's create this auth_error/3-function now to handle unauthenticated users:

# lib/messengyr/web_controllers/chat_controller.ex

defmodule MessengyrWeb.ChatController do
# ...

def auth_error(conn, {_type, _reason}, _opts) do
conn
end
That's all! Now try to go to /messages without being logged in and you should be redirected an see a flash message. If you are logged in however, you should see the empty chat/index-template, like before.
Guardian also has an EnsureNotAuthenticated plug that we could use to make sure that certain routes can only be accessed when the user is not logged in. This could be used for the login page for example (because why should you be able to access the login page when you're already logged in?). For now though, we'll keep things simple and let these pages be available even for authenticated users.
Congratulations! We've now gone from having a mostly static website to something that resembles a web app! In the next couple of lessons, we're going stay on the /messages route and build the interactive messaging interface!