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:
The logged-out header VS the logged-in header.
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
<header>
<%= link "", class: "logo", to: page_path(@conn, :index) %>
<%= link "Log in", to: page_path(@conn, :login) %>
</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">
<!-- Add this line: -->
<%= render MessengyrWeb.LayoutView, "header.html", assigns %>
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<!-- ... -->
</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
<header>
<%= link "", class: "logo", to: page_path(@conn, :index) %>
<%= if logged_in?(@conn) do %>
<p>Logged in!</p>
<% else %>
<%= link "Log in", to: page_path(@conn, :login) %>
<% 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
# Add this function:
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
<header>
<!-- ... -->
<%= if logged_in?(@conn) do %>
<!-- Add this element -->
<div class="profile-container">
<p><%= username(@conn) %></p>
</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
# ...
# Add this function:
def username(conn) do
# We get the user:
user = Guardian.Plug.current_resource(conn)
# We extract the username with pattern matching:
%{username: username} = user
# Return the username:
username
end
end
Tada! And the CSS that we added earlier already has it styled for us.
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
# ...
# Add this function:
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
<header>
<!-- ... -->
<div class="profile-container">
<img src=<%= avatar(@conn) %> /> <!-- Add this line! -->
<p><%= username(@conn) %></p>
</div>
<!-- ... -->
</header>
If you have a profile picture linked to your email address on Gravatar, you should now see your picture in the Phoenix app!
Adding a "log out"-button
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
# ...
post "/login", PageController, :login_user
# Add this line:
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
# ...
# Add this function:
def logout(conn, _params) do
conn
|> Guardian.Plug.sign_out
|> put_flash(:info, "Signed out successfully!")
|> redirect(to: "/")
end
end
And finally we add the link to /logout in our header template, before the avatar and username:
<!-- lib/messengyr_web/templates/layout/header.html.eex -->
<header>
<!-- ... -->
<%= if logged_in?(@conn) do %>
<!-- Add this line: -->
<%= 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">
<div class="card login">
<%= form_for @conn, page_path(@conn, :login_user), [as: :credentials], fn f -> %>
<%= text_input f, :username, placeholder: "Username" %>
<%= text_input f, :password, placeholder: "Password", type: "password" %>
<%= submit "Log in", class: "login" %>
<% end %>
</div>
<div class="card signup">
<h5>
<strong>
New to Messengyr?
</strong>
Sign up!
</h5>
<%= form_for @user_changeset, page_path(@conn, :create_user), fn f -> %>
<%= text_input f, :email, placeholder: "Email" %>
<%= text_input f, :username, placeholder: "Username" %>
<%= text_input f, :password, placeholder: "Password", type: "password" %>
<%= submit "Sign up", class: "signup" %>
<% end %>
</div>
</div>
That's it! Now you can log in and sign up straight from the landing page!
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
# Add this line:
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
# ...
# Add this function:
def auth_error(conn, {_type, _reason}, _opts) do
conn
|> put_flash(:error, "You need to log in to view your messages.")
|> redirect(to: "/")
end
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.
A final note:
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!