Phoenix Framework is built with the powerful and resilient programming language called Elixir Lang. It is gaining momentum by the day thanks to it’s expressive syntax which makes you productive and it’s built upon the shoulders of the Erlang vm called BEAM which makes your code performant.
Here will build a user registration with Email verification and a login systems. Phoenix framework can be used to build normal html 5 crud apps, json api and real time backends. We’ll be building a real time backend using Phoenix channels and an Elixir library Guardian jwt.
You can find installation instructions on the Elixir site and Phoenix framework .
We be using Phoenix 1.3 which is the current version.
Lets create our app called sample with this command:
$ mix phx.new sample --no-html --no-brunch
* creating sample/config/config.exs
* creating sample/config/dev.exs
* creating sample/config/prod.exs
* creating sample/config/prod.secret.exs
* creating sample/config/test.exs
* creating sample/lib/sample/application.ex
* creating sample/lib/sample/web/channels/user_socket.ex
* creating sample/lib/sample/web/views/error_helpers.ex
* creating sample/lib/sample/web/views/error_view.ex
* creating sample/lib/sample/web/endpoint.ex
* creating sample/lib/sample/web/router.ex
* creating sample/lib/sample/web/web.ex
* creating sample/mix.exs
* creating sample/README.md
* creating sample/test/support/channel_case.ex
* creating sample/test/support/conn_case.ex
* creating sample/test/test_helper.exs
* creating sample/test/sample/web/views/error_view_test.exs
* creating sample/lib/sample/web/gettext.ex
* creating sample/priv/gettext/en/LC_MESSAGES/errors.po
* creating sample/priv/gettext/errors.pot
* creating sample/lib/sample/repo.ex
* creating sample/priv/repo/seeds.exs
* creating sample/test/support/data_case.ex
* creating sample/.gitignore
Fetch and install dependencies? [Yn] n
We are almost there! The following steps are missing:
$ cd sample
$ mix deps.get
Then configure your database in config/dev.exs and run:
$ mix ecto.create
Start your Phoenix app with:
$ mix phx.server
You can also run your app inside IEx (Interactive Elixir) as:
$ iex -S mix phx.server
cd into your sample app and go to the mix.ex file and add these libraries in your deps function : {:guardian, “~> 0.14”} , {:swoosh, “~> 0.8.1”}, {:secure_random, “~> 0.5.1”}. These will provide us with Json web token for user authentication , sending email for the user to verify their emailaddress before they can fully loging into our app, and generating a email verification token.
Lets now setup swoosh to send emails, we’ll be using Mailgun api to signup for a free sandbox accout to start sending email with their api.
# In your config/config.exs file
config :sample, Sample.Mailer,
adapter: Swoosh.Adapters.Mailgun,
api_key: "x.x.x"
# In your lib/sample folder create mailer.ex
defmodule Sample.Mailer do
use Swoosh.Mailer, otp_app: :sample
end
defmodule StockitServer.UserEmail do
import Swoosh.Email
# In your lib/sample folder create user_email.ex
defmodule StockitServer.UserEmail do
import Swoosh.Email
def welcome(user) do
new()
|> to({user.name, user.email})
|> from({"your app name", "your-appxxx@gmail.com"})
|> subject("Your sample verification email")
|> html_body("<p>Thanks for signing up with us</p>
<p>Please click the link below to verify your email address</p>
<a href=https://sxxxx.com/v1/api/auth/verify_email/# {user.token}>Verify address</a>"
)
end
end
Lets config Guardian library:
# In your config/config.ex
config :guardian, Guardian,
allowed_algos: ["HS512"], # optional
verify_module: Guardian.JWT, # optional
issuer: "Sample",
ttl: { 30, :days },
allowed_drift: 2000,
verify_issuer: true, # optional
secret_key: System.get_env("GUARDIAN_SECRET") || "xx...xxxx",
serializer: Sample.Web.GuardianSerializer
# In your lib/sample/web folder create guardian_serializer.ex
defmodule Sample.Web.GuardianSerializer do
@behaviour Guardian.Serializer
alias Sample.Repo
alias Sample.Account.User
def for_token(user = %User{}), do: { :ok, "User:#{user.id}" }
def for_token(_), do: { :error, "Unknown resource type" }
def from_token("User:" <> id), do: { :ok, Repo.get(User, id) }
def from_token(_), do: { :error, "Unknown resource type" }
end
Phoenix framework directory structure now favours creating context to access your data model. So lets create Accounts context with apis to access our User model.
#In your lib/sample create account/account.ex
defmodule Sample.Account do
@moduledoc """
The boundary for the Account system.
"""
import Ecto.Query, warn: false
alias Sample.Repo
alias Sample.Account.User
@doc """
Returns the list of users.
## Examples
iex> list_users()
[%User{}, ...]
"""
def list_users do
Repo.all(from u in "account_users",
order_by: u.inserted_at,
select: %{name: u.name, username: u.username, email_verified: u.email_verified, token: u.token, email: u.email, id: u.id})
end
@doc """
Gets a single user.
Raises `Ecto.NoResultsError` if the User does not exist.
## Examples
iex> get_user!(123)
%User{}
iex> get_user!(456)
** (Ecto.NoResultsError)
"""
def get_user!(id), do: Repo.one(from u in "account_users",
where: u.id == ^String.to_integer(id),
select: %{name: u.name, username: u.username, email_verified: u.email_verified, token: u.token, email: u.email, id: u.id})
@doc """
Creates a user.
## Examples
iex> create_user(%{field: value})
{:ok, %User{}}
iex> create_user(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_user(attrs \\ %{}) do
%User{}
|> User.registration_changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a user.
## Examples
iex> update_user(user, %{field: new_value})
{:ok, %User{}}
iex> update_user(user, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_user(%User{} = user, attrs) do
user
|> User.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a User.
## Examples
iex> delete_user(user)
{:ok, %User{}}
iex> delete_user(user)
{:error, %Ecto.Changeset{}}
"""
def delete_user(%User{} = user) do
Repo.delete(user)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking user changes.
## Examples
iex> change_user(user)
%Ecto.Changeset{source: %User{}}
"""
def change_user(%User{} = user) do
User.changeset(user, %{})
end
def get_by(username) do
Repo.get_by(User, username: username)
end
def get_by_token(token) do
Repo.get_by(User, token: token)
end
def verify_email(user) do
user
|> User.verify_changeset()
|> Repo.update()
end
def get!(id) do
Repo.get!(User, id)
end
end
#In your lib/sample create account/user.ex for our user schema
defmodule Sample.Account.User do
use Ecto.Schema
import Ecto.Changeset
alias Sample.Account.User
schema "account_users" do
field :email, :string
field :name, :string
field :username, :string
field :password, :string, virtual: true
field :password_hash, :string
field :email_verified, :boolean
field :token, :string
timestamps()
end
def changeset(%User{} = user, params) do
user
|> cast(params, [:name, :email, :username])
|> validate_required([:name, :username, :email])
|> validate_length(:username, min: 5, max: 20)
|> validate_format(:email, ~r/@/)
|> unique_constraint(:email)
|> put_not_verified()
|> put_token()
end
defp put_not_verified(changeset) do
case changeset do
%Ecto.Changeset{valid?: true} ->
put_change(changeset, :email_verified, false)
_ ->
changeset
end
end
defp put_token(changeset) do
case changeset do
%Ecto.Changeset{valid?: true} ->
put_change(changeset, :token, SecureRandom.urlsafe_base64())
_ ->
changeset
end
end
def registration_changeset(model, params \\ %{}) do
model
|> changeset(params)
|> cast(params, [:password])
|> validate_length(:password, min: 4, max: 200)
|> put_pass_hash()
end
defp put_pass_hash(changeset) do
case changeset do
%Ecto.Changeset{valid?: true, changes: %{password: password}} ->
put_change(changeset, :password_hash, Hasher.salted_password_hash(password))
_ ->
changeset
end
end
def verify_changeset(%User{} = user, params \\ %{}) do
user
|> cast(params, [])
|> put_verify_email()
end
defp put_verify_email(changeset) do
case changeset do
%Ecto.Changeset{valid?: true} ->
put_change(changeset, :email_verified, true)
_ ->
changeset
end
end
end
# Let create our migration from your command line
$ mix ecto.gen.migration create_user
cd into your priv/repo/migrations folder. You’ll see the already created migration file.
defmodule Sample.Repo.Migrations.CreateAccountUser do
use Ecto.Migration
def change do
create table(:account_users) do
add :name, :string
add :username, :string
add :email, :string
add :token, :string
add :email_verified, :boolean
add :password_hash, :string
timestamps()
end
create unique_index(:account_users, [:email, :username])
end
end
Now all that’s left is to migrate up our database
$ mix ecto.migrate
Now let’s create our authentication module. cd into lib/sample/web folder and create aan auth.ex file
defmodule Sample.Web.Auth do
import Hasher
alias Sample.Account
def login_by_username_and_pass(username, given_pass) do
user = Account.get_by(username)
if user do
cond do
user.email_verified && check_password_hash(given_pass, user.password_hash) ->
{:ok, user}
user.email_verified && check_password_hash(given_pass, user.password_hash) == false ->
{:err, :unauthorized}
not user.email_verified ->
{:err, :notverified}
end
else
{:err, :notfound}
end
end
end
Let’s now create our route to verify our email address
defmodule Sample.Web.Router do
use Sample.Web, :router
pipeline :api do
plug :accepts, ["json", "json-api"]
plug JaSerializer.Deserializer
end
pipeline :api_auth do
plug :accepts, ["json", "json-api"]
plug Guardian.Plug.VerifyHeader, realm: "Bearer"
plug Guardian.Plug.LoadResource
plug JaSerializer.Deserializer
end
scope "/", Sample.Web do
pipe_through :api # Use the default browser stack
get "/", PageController, :index
end
scope "/v1/api/auth", Sample.Web do
pipe_through :api
get "/verify_email/:token", AccessTokenController, :verify_email
end
end
Lets create our AccessTokenController.
#In your lib/sample/web/controllers/access_token_controller.ex
defmodule Sample.Web.AccessTokenController do
use Sample.Web, :controller
alias Sample.Account
alias Sample.Account.User
action_fallback Sample.Web.FallbackController
def verify_email(conn, %{"token" => token}) do
user = Account.get_by_token(token)
if user do
with {:ok, %User{} = user} <- Account.verify_email(user) do
conn
|> put_status(200)
|> render("verify_email.json", user: user)
end
else
conn
|> put_status(:not_found)
|> json(%{error: "invalid token"})
end
end
end
Lets create our access token view.
defmodule Sample.Web.AccessTokenView do
use Sample.Web, :view
def render("verify_email.json", %{user: user}) do
%{
verified: user.email_verified
}
end
end
Lets create an access token channel to handle our user registration and login.
#In lib/sample/web/channels create access_token_channel.ex
defmodule Sample.Web.AccessTokenChannel do
use Sample.Web, :channel
alias Sample.Account
alias Sample.UserEmail
alias Sample.Mailer
def join("access:lobby", _payload, socket) do
{:ok, socket}
end
def handle_in("create:user", %{"user" => user_params}, socket) do
case Account.create_user(user_params) do
{:ok, user} ->
UserEmail.welcome(user)|>Mailer.deliver
resp = %{data: %{id: user.id,
name: user.name,
username: user.username,
email: user.email,
token: user.token,
verified: user.email_verified }
}
push socket, "create:user", resp
{:reply, :ok, socket}
{:error, _changeset} ->
{:reply, {:error, %{errors: "Could not create user"}}, socket}
end
end
def handle_in("create:login", %{"user" => %{"username" => username, "password" => password}}, socket) do
case Sample.Web.Auth.login_by_username_and_pass(username, password) do
{:ok, user} ->
{:ok, jwt, _claims} = Guardian.encode_and_sign(user, :access)
resp = %{data: %{access_token: jwt}}
push socket, "create", resp
{:noreply, socket}
{:err, :notverified} ->
{:reply, {:error, %{errors: "please verify your email address"}}, socket}
{:err, :unauthorized} ->
{:reply, {:error, %{errors: "user password incorrect"}}, socket}
{:err, :notfound} ->
{:reply, {:error, %{errors: "user not found"}}, socket}
end
end
end
#In your lib/sample/web/channels/user_socket.ex file add to the list of channels
channel "access:*", Sample.Web.AccessTokenChannel
This concludes our user signup and login system for a real time backend server.
No comments:
Post a Comment