To be in the same page, please download the reference implementation of the project here.

Guardian … a refresher.

Two weeks ago, we started setting up Guardian but run out of time, such that we stopped halfway through. To resume with this process, we are going to review the main steps in the setup so far.

  • We added the dependency {:guardian, "~> 0.14"} to file mix.exs (of course, we run mix deps.get to install the dependency).
  • We added the following snippet within config/config.exs (do not forget to generate a secret key with mix phx.gen.secret and replace that key in the configuration file).

    config :guardian, Guardian,
      issuer: "Takso",
      ttl: {30, :days},
      allowed_drift: 2000,
      secret_key: "copy the secret key here",
      serializer: Takso.GuardianSerializer
    
  • We added the file web/guardian_serializer.ex to the project with the following content:

    defmodule Takso.GuardianSerializer do
      @behaviour Guardian.Serializer
      def for_token(%Takso.User{} = user), do: {:ok, "User:#{user.id}"}
      def for_token(_), do: {:error, "Unknown Resource"}
      def from_token("User:"<>user_id), do: {:ok, Takso.Repo.get(Takso.User, user_id)}
      def from_token(_), do: {:error, "Unknown Resource"}
    end
    

Note that at this moment, the authentication of our application is being still provided by the plug that we incrementally implemented. In fact, from the functional point of view, our plug is already a good approximation to what our application requires. However, in practice, we would need to use a more sophisticated authentication mechanism. That is the reason why, we want to replace our plug. Interestingly, Guardian provides an good implementation of JSW, a widely used authentication mechanism. Moreover, Guardian is ready to be integrated not only to EEX-based applications (server-side HTML templating), but also for REST API’s and Phoenix channels. All that is exactly what I want to illustrate with our application.

Guarding … completing its setup.

As I mentioned, before, we need to replace the house-made authentication plug. Let us first remove the file web/plugs/authentication.ex. Well, in fact, we will need to keep some lines of such file. Well, it would be easier if you create a new file called web/helpers/authentication.ex and copy there the following snippet:

defmodule Takso.Authentication do
  def check_credentials(conn, user, password) do
    if user && Comeonin.Pbkdf2.check_pass(user, password) do
      {:ok, conn}
    else
      {:error, conn}
    end
  end
end

Note that we just kept there the verification of the password and that we are not longer copying whatsoever into the connection. That will be done via Guardian and will be different depending on whether we are working with EEX-based interaction or with a REST API.

Now, open the file web/router.ex and update its content, according to the following snippet:

pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
  plug :fetch_flash
  plug :protect_from_forgery
  plug :put_secure_browser_headers
  # plug Takso.Authentication, repo: Takso.Repo << Bye to our nice plug !
end

pipeline :browser_auth do  
  plug Guardian.Plug.VerifySession
  plug Guardian.Plug.LoadResource
end

pipeline :require_login do
  plug Guardian.Plug.EnsureAuthenticated, handler: Takso.SessionController    
  plug :guardian_current_user
end

As you can see, we have commented the line that injected our authentication plug. Then, we defined two additional pipelines that mostly rely on Gardian. The first one, i.e. pipeline :browser_auth, implements the authentication behavior similar to our plug. The second pipeline, i.e. :require_login, will be used for specifying that some modules (i.e. controller actions/functions) cannot be reached unless the user has been properly authenticated. Whenever, we reach a point of such sort, Guarding would redirect the control to the unauthorized handler that we would add to the session controller. The handler implementation is shown in the following snippet, please copy that code to file web/controllers/session_controller.ex.

def unauthorized(conn) do
  conn
  |> put_flash(:error, "Nothing to see there")
  |> redirect(to: page_path(conn, :index))
  |> halt
end

Unfortunately, Guardian and Canary are not compatible off-the-shelf: Canary that the connection hold a reference to current_user (as we did with our plug), whereas Guardian refers to the same information as current_resource. To resolve this discrepancy, we added a function plug in the last line of the pipeline. The implementation of such function plug, that can be in-lined in the file web/router.ex, is the following:

def guardian_current_user(conn, _) do
  Plug.Conn.assign(conn, :current_user, Guardian.Plug.current_resource(conn))
end

Armed with the above, we can now refine our routes to meet the authentication requirements as follows:

scope "/", Takso do
  pipe_through :browser
  resources "/sessions", SessionController, only: [:new, :create, :delete]
end

scope "/", Takso do
  pipe_through [:browser, :browser_auth]
  get "/", PageController, :index
end

scope "/", Takso do
  pipe_through [:browser, :browser_auth, :require_login]
  resources "/users", UserController
  get "/bookings/summary", BookingController, :summary
  resources "/bookings", BookingController
end

Note that the path /sessions can be accessed without any authentication. That means, for instance, that w e do not need to be logged in to the system to access the login page, which seems reasonable. On the other hand, we have added the pipeline :browser_auth to the path /. This implies that, once authenticated, we are going to preserve the user authentication information while we are in the path /. Finally, in order to reach the paths /users, /bookings and /bookings/summary, the user must be properly authenticated (i.e. the login procedure has been successful).

Well, Guardian is now set up. However, we still need to update some components. First, let us update the template web/templates/layout/app.html.eex by replacing the following block (the previous code should still refer to current_user):

<%= if Guardian.Plug.current_resource(@conn) do %>
          <li>Hello <%= Guardian.Plug.current_resource(@conn).username %></li>
          <li><%= link "Log out", to: session_path(@conn, :delete, Guardian.Plug.current_resource(@conn)), method: "delete" %></li>
<% else %>
          <li><%= link "Log in", to: session_path(@conn, :new) %></li>
<% end %>

The references to conn.assigns.current_user made within web/controllers/booking_controller.ex can be left as they are, because all the functions on this module cannot be accessed unless the user is authenticated.

The last file that we need to update is web/controllers/session_controller.ex. Use the following snippet to that end:

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

def delete(conn, _) do
  conn
  |> Guardian.Plug.sign_out
  |> redirect(to: page_path(conn, :index))
end

Note that we added the calls to Guardian helpers to sign in and to sign out a user. Within those calls, Guardian adds/removes the information of the user to the connection.


VueJS components

In last week’s session, we completed already a very nice frontend for our customer, including some graphical elements. However, as our application grows, we would need a way to organize the code to keep the things clean and understandable. To that end, in this session we will learn about VueJS components.

You will notice that the code we used to start with in this session does not contain the Javascript that we added last week. I decided to do so, to reduce a little bit the distractors (e.g. Google maps related code). I am confident that you will be able to add such code later without any problem.

Let us start by adding some javascript dependencies to our project. In a terminal window, run the following commands:

npm install --save vue vue-router
npm install --save-dev vue-brunch babel-plugin-transform-runtime

You should remember that the two first packages (i.e. vue and vue-router) are runtime libraries. The other two libraries are development libraries (they are used to compile the code we add into plain javascript that will be later deployed).

For completeness, I will mention that we need to change brunch’s configuration (we did this last time also). Open the file brunch-config.js and change it as shown below.

exports.config = {
  ...
  npm: {
    enabled: true,
    aliases: { vue: "vue/dist/vue.common.js" }
  }
}

We are ready. Let me start by saying that a VueJS provides syntax and tools to organize the code in units that are referred to as components. A component is composed of an HTML component, a script and local CSS style specifications, all of them in a single file. For instance, in our sample taxi application, we can clearly see that we could use one VueJS component for the interacting with the customers and a second one for interacting with the drivers. This is, in fact, the approach that I am going to take.

In your project, add a file called web/static/js/customer.vue. If you prefer, you can add an additional folder and put there all the vue components. Copy the following code into the file that we just created:

<template>
<div>
  <div class="form-group">
    <label class="control-label col-sm-3" for="pickup_address">Pickup address:</label>
    <div class="col-sm-9">
      <input type="text" class="form-control" id="pickup_address" v-model="pickup_address">
    </div>
  </div>
  <div class="form-group">
    <label class="control-label col-sm-3" for="dropoff_address">Drop off address:</label>
    <div class="col-sm-9">
      <input type="text" class="form-control" id="dropoff_address" v-model="dropoff_address">
    </div>
  </div>
  <div class="form-group">
    <div class="col-sm-offset-3 col-sm-9">
      <button class="btn btn-default" v-on:click="submitBookingRequest">Submit</button>
    </div>
  </div>
  <div><textarea class="col-sm-12" style="background-color:#f4f7ff" rows="4" v-model="messages"></textarea></div>
  <div id="map" style="width:100%;height:300px"></div>
</div>
</template>

<script>
import axios from "axios";

export default {
    data: function() {
        return {
            pickup_address: "Liivi 2, Tartu, Estonia",
            dropoff_address: "",
            messages: ""
        }
    },
    methods: {
        submitBookingRequest: function() {
            axios.post("/api/bookings",
                {pickup_address: this.pickup_address, dropoff_address: this.dropoff_address})
                .then(response => {
                    this.messages = response.data.msg;
                }).catch(error => {
                    console.log(error);
                });
        }
    }
}
</script>

As you can see, the code above contains two parts: the template and the script. I decided to use the global styles, such that you wont see such section in the components that we will be using. Well, I think that I do not need to explain further the code above as it is a simplified version of what we wrote last week.

Let us change the EEX template that will hold our frontend application. Copy the following snippet into file web/templates/page/index.html.eex.

<div id="takso-app">
  <customer></customer>
</div>

Now, open the file web/static/js/app.js and replace all the code with the following snippet.

import Vue from "vue";
import "axios";
import "./socket"; // To be used later

import customer from "./customer";
Vue.component("customer", customer);

new Vue({}).$mount("#takso-app");

There are several things to note here. First, we import here the library axios, although it is not directly used here, we need to include it to make it available to the Vue components. Second, we import the Vue component as we would do with any other local javascript file. The compilation tool vue-brunch will compile *.vue files into plain javacript. Hence, we can load the corresponding file and we then can declare it as a vew component with Vue.component("customer", customer). Note that the first string in the statement declares the name of the tag, which we have also used in the EEX template. Finally, we instantiate Vue and mount it into the HTML element that carries the identifier takso-app. This time I am using an alternative notation with respect to the code that we produced last week.

Our application is still incomplete because the code that we are using does not contain the module web/controllers/booking_api_controller.ex. For completeness, I will also include it here. Create the file and copy there the following snippet:

defmodule Takso.BookingAPIController do
  use Takso.Web, :controller
  import Ecto.Query, only: [from: 2]
  alias Takso.{Taxi,Repo}

  def create(conn, _params) do
    query = from t in Taxi, where: t.status == "available", select: t
    available_taxis = Repo.all(query)
    if length(available_taxis) > 0 do
      taxi = List.first(available_taxis)
      conn
      |> put_status(201)
      |> json(%{msg: "We are processing your request"})
    else
      conn
      |> put_status(406)
      |> json(%{msg: "Our apologies, we cannot serve your request in this moment"})
    end
  end
end

Remember that we would need to change the file web/router.ex to take into account our new controller.

  scope "/api", Takso do
    pipe_through :api

    post "/bookings", BookingAPIController, :create
  end

In this moment, the application should be in a consistent state. Play a little bit with it to check if everything is working.

Phoenix channels

Phoenix provides a very convenient communication infrastructure to implement push-based interactions. The latter means that we can implement code where the backend initiates the interaction and not the client side. In our case, one of such interactions will be used to notify the taxi drivers about ride requests. Although the interaction is started when the customer send a booking request, it is the backend that selects one taxi and forwards the request to the corresponding driver.

Interestingly, the code for implementing such interactions has always been there, silently. We just need to complete the set up and start using this capability.

In a terminal window, run the following commands:

mix phoenix.gen.channel Driver
mix phoenix.gen.channel Customer

As you can see, the connection provided by Phoenix is rendered in the form of channels. We would need one channel for communicating with drivers and another one for customers. Note that each channel can be multiplexed such that we can create private “rooms”, each one to communicate with a single user (driver or customer). The latter will be illustrated later.

Add the following two lines to file web/channels/user_socket.ex.

channel "driver:*", Takso.DriverChannel
channel "customer:*", Takso.CustomerChannel

We the code above, we complete the configuration. Let us now modify the module BookingAPIController to use the driver’s channel. Add the line Takso.Endpoint ... to the function create as shown below.

  def create(conn, _params) do
    query = from t in Taxi, where: t.status == "available", select: t
    available_taxis = Repo.all(query)
    if length(available_taxis) > 0 do
      taxi = List.first(available_taxis)

      Takso.Endpoint.broadcast("driver:lobby", "requests", params |> Map.put(:booking_id, 1))

      conn
      |> put_status(201)
      |> json(%{msg: "We are processing your request"})
    else
      conn
      |> put_status(406)
      |> json(%{msg: "Our apologies, we cannot serve your request in this moment"})
    end
  end
end

The code above, simply forwards (broadcasts) a message via the channel driver:lobby with the information of the ride booking request. Note that in this moment all the drivers would receive the request. That is something we will fix later.

Let us now write some javascript code. Change the file web/static/js/socket.js with the following code:

import {Socket} from "phoenix";
let socket = new Socket("/socket", {params: {token: "123"}});
socket.connect();
export default socket;

In fact, I just removed some sample code and lots of comments. Well, the code above sets up a (web) socket and starts the connection. That is it. Well, we can add some security related code (i.e. JWT token) but we will do this later.

Copy the following vue component into file web/static/js/driver.vue.

<template>
<div class="well">
  {{message}}
  <div id="map-driver" style="width:100%;height:300px"></div>
</div>
</template>

<script>
import socket from "./socket";

export default {
    data: function() {
        return {
            message: "Hello there"
        }
    },
    mounted: function() {
        var channel = socket.channel("driver:lobby", {});
        channel.join()
            .receive("ok", resp => { console.log("Joined successfully", resp) })
            .receive("error", resp => { console.log("Unable to join", resp) });

        channel.on("requests", payload => {
            this.message = payload;
        });
    }
}
</script>

To complete the setup we will need to change the file web/templates/page/index.html.eex

<div id="takso-app">
  <customer></customer>
  <driver></driver>
</div>

and to register the vue component within the main javascript file (i.e. web/static/js/app.js).

import Vue from "vue";
import "axios";
import "./socket"; // To be used later

import customer from "./customer";
import driver from "./driver";
Vue.component("customer", customer);
Vue.component("driver", driver);

new Vue({}).$mount("#takso-app");

Scheduled jobs

To conclude the lecture, we will introduce the notion of scheduled jobs for elixir.

There exists situations where we need to schedule tasks to be executed in the future. For instance, in our scenario, we would like to program a task to select a new driver for serving a ride request in case the one contacted by the system does not reply after a while.

In elixir and, hence, in Phoenix this sort of scheduled tasks can be implemented with processes. In fact, we are going to use the same sort of the processes that we used for homework 2, i.e. GenServer processes.

Let me illustrate the use of GenServer with an example. Copy the following snippet into file web/jobs/taxi_allocator.ex.

defmodule Takso.TaxiAllocator do
  use GenServer

  def start_link(request, booking_reference) do
    GenServer.start_link(Takso.TaxiAllocator, request, name: booking_reference)
  end
  
  def init(request) do
    Process.send_after(self(), :notify_customer, 10000)
    {:ok, request}
  end

  def handle_info(:notify_customer, request) do
    Takso.Endpoint.broadcast("customer:lobby", "requests", 
      %{msg: "Our apologies, we cannot serve your request in this moment"})
    {:noreply, request}       
  end
end

The code above should be already familiar to you. The job relies again on GenSever, but we are not adding the handle_call/2 and handle_cast/2 functions that we leart earlier in the course. This time, we are scheduling a task to happen in the future by calling Process.send_after(self(), :notify_customer, 10000). The previous call schedules the task to be triggered after 10000 milliseconds (e.g. 10 sec.) just for demonstration purposes. As you can see, we are delegating the execution of the task to self(). When the time comes to execute the task, the erlang virtual machine will instantiate the GenServer and call the function handle_info/2 that matches the atom :notify_customer. As you can see, the code above is written such that the task will send a message to the customer with a message "Our apologies, we cannot serve your request in this moment" just for demonstration purposes.

Now, let us change the booking controller to schedule our job. Replace the code of the controller’s create action with the following snippet:

  def create(conn, params) do
    query = from t in Taxi, where: t.status == "available", select: t
    available_taxis = Repo.all(query)
    if length(available_taxis) > 0 do
      taxi = List.first(available_taxis)

      Takso.Endpoint.broadcast("driver:lobby", "requests", params |> Map.put(:booking_id, 1))
      Takso.TaxiAllocator.start_link(params, String.to_atom("booking_#{1}"))
      
      conn
      |> put_status(201)
      |> json(%{msg: "We are processing your request"})
    else
      conn
      |> put_status(406)
      |> json(%{msg: "Our apologies, we cannot serve your request in this moment"})
    end
  end

The only difference with the previous version of the code is the line Takso.TaxiAllocator.start_link(params, String.to_atom("booking_#{1}")). In this line, we instanciate the GenSever Takso.TaxiAllocator that I presented before. Note that we are using String.to_atom("booking_#{1}") to give a name to the TaxiAllocator’s GenServer instance. This identifier could later be used to interact with that process (remember that processes can keep a state and, as we saw before, can also be used to implement scheduled jobs.). Note however that, because I decided to use "booking#{1}" to generate the name associated with the GenServer process is inconvenient, because the name of the process must be unique and, hence, the code above will work only once. You can fix the above very easily once you insert the booking into the database and using the the identifier given by the database to the booking instance.

With this example, I am giving you a hint about how we can handle the timeouts for selecting a new taxi driver if the one that the system contacted before does not reply to a booking request. Note that GenServer process is also storing the information about the booking request as its “state”. One thing we would be interested in storing are references to scheduled task, as such references would be used to disable them, for instance, when the taxi driver replies before the deadline of a scheduled task that would search for another taxi driver.

It is worth noting that using GenServer would help us in avoiding issues due to concurrent execution. The virtual machine maintains only one instance of a GenServer process and that intance will execute only one handle_call/2, handle_cast/2 or handle_info/2 at a time hence avoiding race conditions. In fact, this is in a nutshell the foundation of Erlang/Elixir’s concurrency model, which is often referred to as the actor model.

For the sake of simplicity, I will remove the name of the GenServer. However, you can put it back later. As I mentioned before, you would generate a unique identifier by using the identifier produced by the underlying database.

This is all we have for this session.