Deep dive into Phoenix

If you wish, you can download the project code from here. Sure enough, you can use the Phoenix that we have writing during our two last weeks. Anyway, if you decide to use code in the zip file provided above please remember that you have to unzip the project and then run the commands mix deps.get and npm install to download all the project dependencies (Phoenix/elixir and Javascript, respectively).

Plugs

Create a new file called web/plugs/authentication.ex and copy there the following snippet.

defmodule Takso.Authentication do
  def init(_opts), do: nil
  def call(conn, _) do
    IO.puts "HI THERE ..."
    conn
  end    
end

Any plug module implements two functions:1) the function init/1 that is used for passing references to infrastructure and data structures that the plug use to complete its task, and 2) the function call/2 that is called every time a new web request arrives to the application. In contrast to call/2 which is repeatedly called, the function init/1 is called only when the application is launched.

In the code above, the function call/2 receives conn, prints out a greeting and finally returns conn. The idea is that our implementation would have the opportunity to “decorate” the connection with additional information (e.g. user connected to the application for the duration of the session).

To connect our plug, we have to add it to the list of plugs configured for our application. Open the file router.ex and look for the block defining a “pipeline” associated with the HTML web page, as illustrated below. Add there the reference to our module.

  pipeline :browser do
    ...
    plug Takso.Authentication
  end

Note that such pipeline above is associated with the atom :browser. This pipeline is the chain of plugs that serve to implement the transformations and support to protocols that are typically used in HTML-based web applications, that is, the type of applications that are accessed via a Web browser.

Start your application and, in a web browser, go to the URL that points to the home page of our application. Check out the terminal where you run the application. You should see there the greeting from our plug.

Associating a default user to the session

To start with our work, we will associate to the application session a default user. Remember that we already have two users in the database (i.e. added via the script priv/repo/seeds.exs). What I propose you is to associate one of the two users to the session. To that end, our plug needs to get access to the application’s repository. So we need to pass it to the plug, via the function init/1.

Open once more the file web/router.ex and update the line that we added before as follows:

  pipeline :browser do
    ...
    plug Takso.Authentication, repo: Takso.Repo
  end

As you can see, we added a keywordlist [repo: Takso.Repo] to the initialization of the plug in the pipeline. Remember that we can just avoid the use of squared brackets to improve the readability of the code but, at the end of the day, there is indeed a keywordlist. That also means that we can pass more things to the plug at initialization time.

Now, let us change the implementation of the plug. Replace the current code with the following snippet:

defmodule Takso.Authentication do
  import Plug.Conn

  def init(opts) do
    opts[:repo]
  end

  def call(conn, repo) do
    IO.puts "HI THERE ..."
    user = repo.get(Takso.User, 1)
    assign(conn, :current_user, user)
  end
end

As you can see, init/1 receives the keywordlist and retrieves the reference to the application repository from it. In fact, this is the value, i.e. the repository, that this function will return. Moreover, this is the value that the function call/2 will receive as second parameter. With this parameter, we can query the database for the user with identifier 1 (remember that we added two users … normally, one of them should have the identifier 1). At the end of the function, we are adding the information about the user to conn using the function assign/3 that come from the library Plug.Conn. This is the reason why we had to also add the import to the module.

To check that our plug is working, let us also modify the web pages. Open the file web/templates/layout/app.html.eex and replace the entire block <header>...</header> with the following snippet:

      <header class="header">
        <ol class="breadcrumb pull-right">
<%= if @conn.assigns.current_user do %>
          <li>Hello <%= @conn.assigns.current_user.username %></li>
          <li><%= link "Log out", to: page_path(@conn, :index) %></li>
<% else %>
          <li><%= link "Log in", to: page_path(@conn, :index) %></li>
<% end %>
        </ol>
        <span class="logo"></span>
      </header>

Restart your application if required and then open the home page.

Adding a login page

Well, we have now a user associated with our session. But it will always be the same user! Let us now provide a way for a user to identify him/herself.

Note that we need several actions to handle the authentication process: one to retrieve the loging page, another one to authenticate (create the session) and a final one to terminate the user’s session. Clearly, we need another controller to this end. We will call this controller SessionController. Hence, let us repeat the process we have followed during the last two weeks to add this controller:

  • Add a route for this controller. Add the line resources "/sessions", SessionController, only: [:new, :create, :delete] to the file web/router.ex.

  • Create the file web/controllers/session_controller.ex and copy there the following code:

defmodule Takso.SessionController do
  use Takso.Web, :controller

  def new(conn, _params) do
    render conn, "new.html"
  end
end
  • Create the file web/controllers/session_view.ex and copy there the following code:
defmodule Takso.SessionView do
  use Takso.Web, :view
end
  • Add the form for the login page. To that end, create the file web/templates/session/new.html.eex and copy there the following EEX template:
<%= form_for @conn, session_path(@conn, :create), [as: :session], fn session -> %>
  <div class="form-group">
    <%= label session, :username, class: "control-label" %>
    <%= text_input session, :username, class: "form-control" %>
  </div>
  <div class="form-group">
    <%= label session, :password, class: "control-label" %>
    <%= text_input session, :password, class: "form-control" %>
  </div>
  <div class="form-group">
    <%= submit "Submit", class: "btn btn-primary" %>
  </div>
<% end %>

Well, all the above changes are just to provide the login page. We still need to handle the actual creation of the session. We are going to do it incrementally. Copy the following snippet to the file web/controllers/session_controller.ex.

  def create(conn, %{"session" => %{"username" => username, "password" => password}}) do
    conn
    |> put_flash(:info, "Welcome #{username}")
    |> redirect(to: page_path(conn, :index))
  end

The code above is more like a facade, as it welcomes the incoming user without any actual authentication. However, as you can check on the application’s home page, the session is still associated with the default user.

Well, we have now the credentials; it is time then to verify them. Even if we could check the credentials in the controller, it is not the right place. In fact, we will put the verification code in the plug. Moreover, we will modify the code of the controller to handle two cases: one when the user credentials are valid and the other one when they are invalid. As is customary in Elixir, we will not use exceptions but :ok and :error tuples. Replace the code of the function create/2 with the following one:

  def create(conn, %{"session" => %{"username" => username, "password" => password}}) do
    case Takso.Authentication.check_credentials(conn, username, password, repo: Takso.Repo) do
      {:ok, conn} ->
        conn
        |> put_flash(:info, "Welcome #{username}")
        |> redirect(to: page_path(conn, :index))
      {:error, _reason, conn} ->
        conn
        |> put_flash(:error, "Bad credentials")
        |> render("new.html")
    end    
  end

As you can see above, we also need to modify the file web/plugs/authentication.ex: we have to add the function check_credentials/4. The code is the following:

  def check_credentials(conn, username, password, [repo: repo]) do
    user = repo.get_by(Takso.User, username: username)
    if user.password == password do
      {:ok, assign(conn, :current_user, user) |> put_session(:user_id, user.id)}
    else
      {:error, :unauthorized, conn}
    end    
  end

Fortunately, the code above is simple. Using the value of username, it retrieves the information about the user. Then, we compare the password stored in the database with the one provided by the user via the web form. If the value match the function will return a tuple with :ok and a new conn, on which we have added the pair :current_user, user to the conn’s assigns (this is the name used in the documentation) and on which we have added the pair :user_id, user.id to the underlying session. Note that the value of current user lasts only for the duration of a single interaction, where as the value of the user_id stored in the session will be sent back and forth, as long as the user keeps the session “alive”.

If you try again the application, you will notice that the application is greeting the user that logged in. However, somehow the user is reset when we reach the index page. The reason why this happens is that the application is redirected to the index page after completing the creation of the session, which will call again the function call/2 in the authentication plug. Remember that, in this moment, that function sets the value of current user with the information of the user with identifier 1. However, we have copied the identifier of the logged in user into the session. Henceforth, we can fix the problem just by using such information. Before changing the code, I would like to mention that the code to storing the information of the current user to conn is similar in functions call/2 and check_credentials/4. For this reason, I will propose you to refactor such code into a separate function (i.e. login/3). Replace the implementation of the aforementioned functions in file web/plugs/authentication.ex with the following code:

  def call(conn, repo) do
    user_id = get_session(conn, :user_id)
    user = user_id && repo.get(Takso.User, user_id)
    login(conn, user_id, user)
  end

  def login(conn, user_id, user) do
    assign(conn, :current_user, user)
    |> put_session(:user_id, user_id)
  end

  def check_credentials(conn, username, password, [repo: repo]) do
    user = repo.get_by(Takso.User, username: username)
    if user.password == password do
      {:ok, login(conn, user.id, user) }
    else
      {:error, :unauthorized, conn}
    end    
  end

Adding the logout action

As you can see, almost from the beginning, I added a link to launch the logout action. However, the link is wrongly directed. It turns out that we added the link by the time there was not yet the SessionController.

To correct this problem we will need to update the EEX template web/templates/layout/app.html.eex. Replace again the block <header>...</header> with the following snippet:

      <header class="header">
        <ol class="breadcrumb pull-right">
<%= if @conn.assigns.current_user do %>
          <li>Hello <%= @conn.assigns.current_user.username %></li>
          <li><%= link "Log out", to: session_path(@conn, :delete, @conn.assigns.current_user), method: "delete" %></li>
<% else %>
          <li><%= link "Log in", to: session_path(@conn, :new) %></li>
<% end %>
        </ol>
        <span class="logo"></span>
      </header>

In addition to the links, we also need the action on the controller. Henceforth, we need to copy the following snippet into SessionController:

  def delete(conn, _params) do
    conn
    |> Takso.Authentication.logout()
    |> redirect(to: page_path(conn, :index))
  end

As you can see, we have delegated the handling of the housekeeping tasks to Takso.Authentication. Assuming that all is done, the controller just need to redirect to a page that is left open to unauthenticated users. In this case, I am assuming that such page is the applications home page. Another possibility could be to redirect to the login page.

Finally, we need to implement the function logout/0 within Takso.Authentication. The corresponding code is shown below:

  def logout(conn) do
    configure_session(conn, drop: true)
  end

As you can see, we just need to close the session. Well, I want to clarify that the authentication plug is still very primitive (e.g. the password is not encrypted, etc.). I decided to keep the implementation simple for this lecture and we will complete it next week.

Connecting Bookings and Users

In the previous sessions, we added support for handling users and also booking requests. However, a booking has been left dangling, without an owner. Now that we have a basic authentication support, we will connect the information.

To start with this process, we need to modify the definition of booking. So far, the table “bookings” stores only the pickup and drop off addresses, in addition to the timestamps and underlying identifier. As we have been mentioning, every change made to the database schema needs to be done via a database migration. Indeed, we have added a migration for creating the table, now we need a new migration where we are going to specify the changes to be made to the database schema.

To create the migration file, we will use the command mix ecto.gen.migration add_user_and_status_to_booking. The last part of the command corresponds with the name of the migration. The final name of the migration will have a timestamp as a prefix. Remember that such timestamp is used by ecto to decide the order on which the migrations are executed. It is also customary to use a self-descriptive name for the migration.

defmodule Takso.Repo.Migrations.AddUserAndStatusToBooking do
  use Ecto.Migration

  def change do
    alter table(:bookings) do
      add :status, :string, default: "open"
      add :user_id, references(:users)
    end
  end
end

Note that the migration alters the table :bookings and does not create a new one. The migration add a status and a user identifier. In the case of the user identifier, the datatype is a special one: it is a reference to another table (i.e. it is a foreign key, if we use the vocabulary of relational databases). Also, it is worth noting that in this migration we have specified that the default value for status is set to “open”

We now have to modify the booking model. To that end, replace the content of file web/models/booking.ex with the following code:

defmodule Takso.Booking do
  use Takso.Web, :model

  schema "bookings" do
    field :pickup_address, :string
    field :dropoff_address, :string
    field :status, :string, default: "open"
    belongs_to :user, Takso.User
    timestamps()
  end

  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:pickup_address, :dropoff_address, :status])
    |> validate_required([:pickup_address, :dropoff_address])
  end
end

As you can see, we added the field :status in the schema definition and also in the references made with function changeset/2. This should be easy to understand by this time. The new concept here is the use of belongs_to :user, Takso.User in the schema. The latter, serves to specify that there exists an association from a user and his/her bookings: a booking belongs to a user and, conversely, a user may have many bookings.

Well, you should already have noticed that we need to implement the inverse relationship. Open the file web/models/user.ex and add the following line to the corresponding schema:

  schema "users" do
    ...
    has_many :bookings, Takso.Booking
    timestamps()
  end

The final change is on the booking controller: we have to connect the user with the booking. Remember that now, the information of the currently logged in user is available in conn. Armed with this, we can complete the code as follows (replace the implementation of this function on the file web/controllers/booking_controller.ex):

def create(conn, %{"booking" => booking_params}) do
    user = conn.assigns.current_user

    booking_struct = build_assoc(user, :bookings, Enum.map(booking_params, fn({key, value}) -> {String.to_atom(key), value} end))
    Repo.insert(booking_struct)

    query = from t in Taxi, where: t.status == "available", select: t
    available_taxis = Repo.all(query)
    if length(available_taxis) > 0 do
      conn
      |> put_flash(:info, "Your taxi will arrive in 5 minutes")
      |> redirect(to: booking_path(conn, :index))
    else
      conn
      |> put_flash(:error, "We apologize, we cannot serve your request in this moment")
      |> redirect(to: booking_path(conn, :index))
    end
  end

In the first line, we retrieve the information about the current user from conn. Then we initialize a struct for the booking, with the parameters received as part of the call and, additionally, we use the function build_assoc to connect the user and the bookings. Note that this is the approach proposed by the documentation to connect values from two different tables via an association. What is interesting is that, in spite of being building the struct for a “booking”, we have to connect the values from the user perspective. Personally, I find this approach a bit counter-intuitive. For this reason, I will present an alternative approach during later (e.g. during the lab session).

Well, it is now the time to give a try to the new code.