IoT with Phoenix Part 1
This is the first blog post that I am writing to share my experience with the Phoenix framework in building an Iot application. I have written a blog post about Elixir and Phoenix here. The objectives that we are going to achieve in here are
-
A ‘Model View Controller’ scheme for the users. So that the users can register themselves in the application and have a login access to monitor as well as control their machines.
-
A messaging interface between the machine and the user so that the application can be pretty much asynchronous and fast. This will accomplish the goal of an user specific Iot architecture, where a user can control and monitor only his/her machines, not others. There is no need of refreshing the pages to know the current state of the machines as we will be using websocket connections to make it a real time application, where the connection to the server never goes away.
-
Writing machine clients. The clients which will help the machine send sensor outputs to the server.
Phoenix is a framework written in Elixir programming language which runs on Erlang VM. The installation instructions for the framework is given here. After successful installation, let’s start building the application.
To create our Iot project run the following in the terminal.
$mix phoenix.new iot
Now, enter into the application. Note: you must have postgres installed in your system for the database requirements.
$cd iot
Now run the server or you can run the server within an interpreter too.
$mix phoenix.server
or
$iex -S mix phoenix.server
Now we can view the application at http://localhost:4000/. Now let’s change the contents of some files to have the default page as the login page for users. To achieve that, we should have an users table in the database with the details of the users as attributes. For that, we have to generate a users model using the mix model generator.
$mix phoenix.gen.model User user username:string encrypted_password:string email:string token:string
It will create a file called user.ex
in the web/models/
directory. Now the users table has 4 attributes called the username
, encrypted_password
, email
and a token
. But, to register a user with a password for login purposes, we should have two virtual fields, the password
field and a password_confirmation
field. These are virtual attributes and they are never stored as attributes of the users table. The password after entered in the registration form is converted into a encrypted form and saved in the database as encrypted password. When the user logs in later, again that entered password is encrypted and matched with the encrypted password already saved in the database for that user. This process helps in a secured access by not storing the password anywhere during the login or register process. The user only knows his password, no one else knows, not even the admins of the application. :D
Now, let’s add those virtual fields and have some validations for the attributes in user.ex
file.
defmodule Iot.User do
use Iot.Web, :model
alias Iot.Repo
schema "users" do
field :username, :string
field :email, :string
field :encrypted_password, :string
field :password, :string, virtual: true
field :password_confirmation, :string, virtual: true
field :token, :string
timestamps
end
@required_fields ~w(username email password password_confirmation)
@optional_fields ~w()
@doc """
Creates a changeset based on the `model` and `params`.
If no params are provided, an invalid changeset is returned
with no validation performed.
"""
def changeset(model, params \\ :empty) do
model
|> cast(params, @required_fields, @optional_fields)
|> unique_constraint(:username, on: Repo, downcase: true)
|> validate_length(:password, min: 8)
|> validate_length(:password_confirmation, min: 8)
|> validate_confirmation(:password)
end
end
Here, the required fields doesn’t include the encrypted_password
field and the reason is obvious. The encrypted_password
we are going to generate later. Now, to register the users, we should have a registration controller, a registration template of course with a form to be filled by the users, and a url like http://localhost:4000/registraion. So, let’s modify the web/router.ex
file.
defmodule Iot.Router do
use Iot.Web, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", Iot do
pipe_through :browser # Use the default browser stack
get "/registration", RegistrationController, :new
post "/registration", RegistrationController, :create
get "/", PageController, :index
end
end
Now we have added 2 routes, one to get the content (The user registration form) and the other is a post request for the same. Now we need to have a registration_controller.ex
file in the web/controller
directory, where we are going to write the new
and create
methods.
defmodule Iot.RegistrationController do
use Iot.Web, :controller
alias Iot.Password
def new(conn, _params) do
changeset = User.changeset(%User{})
render conn, changeset: changeset
end
def create(conn, %{"user" => user_params}) do
changeset = User.changeset(%User{}, user_params)
if changeset.valid? do
new_user = Password.generate_password_and_store_user(changeset)
conn
|> put_flash(:info, "You are Sucessfully Registered :P ")
|> redirect(to: page_path(conn, :index))
else
render conn, "new.html", changeset: changeset
end
end
end
Here in the new method, we are accepting the attributes of the user in the variable changeset
. The create method checks if those required fields fulfill the validations present in web/models/user.ex
by changeset.valid?
method and stores the user in the database and renders the page_index page. (The default page at http://localhost:4000/ ). If the changeset.valid?
is not fulfilled again the new registration page will open up.
The saving of the user in the database by creating the encrypted_password
is done here by the Password
library. Let’s write the password library with the generate_password_and_store_user
that we have just used in the registration controller and other methods.
#lib/Iot/password.ex
defmodule Iot.Password do
alias Iot.Repo
import Ecto.Changeset, only: [put_change: 3]
import Comeonin.Bcrypt, only: [hashpwsalt: 1]
def generate_password_and_token(changeset) do
to_be_encoded_string = Enum.join([changeset.params["email"], changeset.params["password"]])
token = Base.encode64(to_be_encoded_string)
changeset = put_change(changeset, :token, token)
put_change(changeset, :encrypted_password, hashpwsalt(changeset.params["password"]))
end
def generate_password_and_store_user(changeset) do
changeset
|> generate_password_and_token
|> Repo.insert
end
end
Here I have used comeonin
package to encrypt the password. You can get it by adding it to the dependencies in the mix.exs file as
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"},
{:comeonin, "~> 2.5"}]
end
Then install it through
$mix deps.get
The generate_password_and_token
method generates the encrypted password and a token(to be used later) for the user. The generate_password_and_store_user
method stores the user in the database. Now, it’s time to add a template for the new registration page. Create a directory named registration in the templates folder and add a new.html.eex file in it. Fill it with the content below.
<h3>Registration</h3>
<%= form_for @changeset, registration_path(@conn, :create), fn f -> %>
<%= if f.errors != [] do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below:</p>
<ul>
<%= for {attr, message} <- f.errors do %>
<li><%= humanize(attr) %> <%= translate_error(message) %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="form-group">
<label>Username</label>
<%= text_input f, :username, class: "form-control" %>
</div>
<div class="form-group">
<label>Email</label>
<%= text_input f, :email, class: "form-control" %>
</div>
<div class="form-group">
<label>Password</label>
<%= password_input f, :password, class: "form-control" %>
</div>
<div class="form-group">
<label>Password Confirmation</label>
<%= password_input f, :password_confirmation, class: "form-control" %>
</div>
<div class="form-group">
<%= submit "Register", class: "btn btn-primary" %>
<%= #link("Login", to: session_path(@conn, :new), class: "btn btn-success pull-right") %>
</div>
<% end %>
The session_login button is now commented because we haven’t added the sessions controller yet.
Let’s try it all out. First we have to create the database for the application using ecto.
$mix ecto.create
Then, we have to migrate the changes that we have in the users model.
$mix ecto.migrate
Now we have to restart the server as we have added a new lib file called password.ex
. Then, check if that registration form at http://localhost:4000/registration/ is working. If it renders the default phoenix page with a “You are Sucessfully Registered :P”, it’s working.
Now let’s replace the default phoenix page with a login page for the users. Let’s understand what a session is. Sessions are the users’ active time spans after login. A sessions controller matches the username and password of the user and renders the pages. A session controller stores the users data (you can customize the data to be stored) for the session. It is recommended to store minimum required data like only the username or the email in the session. With a logout request, we free the session variables. So, now we have to add a login
and a logout
route in the router.ex
along with a new
route for the session. The page route will be replaced by the new
method of the session.
scope "/", Iot do
pipe_through :browser # Use the default browser stack
get "/registration", RegistrationController, :new
post "/registration", RegistrationController, :create
get "/", SessionController, :new
post "/login", SessionController, :create
get "/logout", SessionController, :delete
end
Clearly from the routes, we now know that we have to write a session_controller.ex
in the web/controllers
directory with new, create and delete methods in it.
#web/controllers/session_controller.ex
defmodule Iot.SessionController do
use Iot.Web, :controller
plug :scrub_params, "user" when action in [:create]
plug :action
def new(conn, _params) do
render conn, changeset: User.changeset(%User{})
end
def create(conn, %{"user" => user_params}) do
user = if is_nil(user_params["username"]) do
nil
else
Repo.get_by(User, username: user_params["username"])
end
user
|> sign_in(user_params["password"], conn)
end
def delete(conn, _) do
delete_session(conn, :current_user)
|> put_flash(:info, 'You have been logged out')
|> redirect(to: session_path(conn, :new))
end
defp sign_in(user, password, conn) when is_nil(user) do
conn
|> put_flash(:error, 'Could not find a user with that username.')
|> render "new.html", changeset: User.changeset(%User{})
end
defp sign_in(user, password, conn) when is_map(user) do
cond do
Comeonin.Bcrypt.checkpw(password, user.encrypted_password) ->
conn
|> put_session(:current_user, user)
|> put_flash(:info, 'You are now signed in.')
|> redirect(to: energy_meter_path(conn, :index))
true ->
conn
|> put_flash(:error, 'Username or password are incorrect.')
|> render "new.html", changeset: User.changeset(%User{})
end
end
end
The code above is self explanatory. The sign_in
method lets the user log into the pages. The put_session
method stores temporary session data. In this case the user is saved in the session as :current_user
. Now, we will write the new template for the login form. Create a folder called session in the web/templates/
folder and a file called new.html.eex in it.
<h3>Login</h3>
<%= form_for @changeset, session_path(@conn, :create), fn f -> %>
<%= if f.errors != [] do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below:</p>
<ul>
<%= for {attr, message} <- f.errors do %>
<li><%= humanize(attr) %> <%= translate_error(message) %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="form-group">
<label>Username</label>
<%= text_input f, :username, class: "form-control" %>
</div>
<div class="form-group">
<label>Password</label>
<%= password_input f, :password, class: "form-control" %>
</div>
<div class="form-group">
<%= submit "Login", class: "btn btn-primary" %>
<%= link("Sign Up", to: registration_path(@conn, :new), class: "btn btn-success pull-right") %>
</div>
<% end %>
Now, we can uncomment the line in web/templates/registration/new.html.eex
for the session login path. And we change the create
method of the registration controller to render the login page rather than the default phoenix page.
def create(conn, %{"user" => user_params}) do
changeset = User.changeset(%User{}, user_params)
if changeset.valid? do
new_user = Password.generate_password_and_store_user(changeset)
conn
|> put_flash(:info, "You are Sucessfully Registered :P ")
|> redirect(to: session_path(conn, :new))
else
render conn, "new.html", changeset: changeset
end
end
Now, where will it go after login? We must have some place where it should move to after login and we must have a control over that page so that that page can not be opened unless the user is logged in. That is why the line redirect(to: energy_meter_path(conn, :index))
is added in the sign_in
method (EnergyMeter is our Iot device). We will create a energy_meter template with only the username of the user in it. At first, let’s edit the router.ex file.
scope "/", Iot do
pipe_through :browser # Use the default browser stack
get "/registration", RegistrationController, :new
post "/registration", RegistrationController, :create
get "/", SessionController, :new
post "/login", SessionController, :create
get "/logout", SessionController, :delete
get "/em", EnergyMeterController, :index
end
Now let’s create a energy_meter_controller.ex
.
defmodule Iot.EnergyMeterController do
use Iot.Web, :controller
plug Iot.Plug.Authenticate
def index(conn, _params) do
current_user = get_session(conn, :current_user)
render conn, "index.html", current_user: current_user
end
end
Now let’s write the template for this.
#web/templates/energy_meter/index.html.eex
<div>Hi <%= @current_user.username %></div>
<%= link("Logout", to: session_path(@conn, :delete), class: "btn btn-success pull-right") %>
This has the username of that user and a logout button. The controller has a Authenticate Plug, which ensures the authenticity of the current user. If the user is not logged in, this Plug helps not to show any page in the EnergyMeter controller. Let’s write that Plug. We have to create a directory called authentication
in the web
directory and add authenticate.ex
file in it.
defmodule Iot.Plug.Authenticate do
import Plug.Conn
import Iot.Router.Helpers
import Phoenix.Controller
def init(default), do: default
def call(conn, default) do
current_user = get_session(conn, :current_user)
if current_user do
assign(conn, :current_user, current_user)
else
conn
|> put_flash(:error, 'You Need to be signed in to view this page !')
|> redirect(to: session_path(conn, :new))
end
end
end
The code is self explanatory. If a user is trying to get the energy_meter index page without logging in, it will throw the error as “You Need to be signed in to view this page !”. Now try that all out by going to http://localhost:4000/.
Note: a view must be added with all the controllers. Otherwise, it will throw an error. A demo view in the views folder is given below.
#web/views/session_view.ex
defmodule Iot.SessionView do
use Iot.Web, :view
end
Now, the authentication part is done. In the next part, we will code the messaging application that is needed for users to listen and control their machines.
Happy Hacking !
Reference - http://meatherly.github.io/2015/05/11/phoenixauthentication/