Skip to content

Commit

Permalink
Add authentication and session workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
murjax committed Sep 17, 2019
1 parent 98ed35f commit 7afe607
Show file tree
Hide file tree
Showing 20 changed files with 217 additions and 124 deletions.
24 changes: 24 additions & 0 deletions lib/jax_ex/accounts/auth.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule JaxEx.Accounts.Auth do
alias JaxEx.Accounts.{Encryption, User}

def login(params, repo) do
user = repo.get_by(User, username: String.downcase(params["username"]))
case authenticate(user, params["password"]) do
true -> {:ok, user}
_ -> :error
end
end

defp authenticate(user, password) do
if user do
{:ok, authenticated_user} = Encryption.validate_password(user, password)
authenticated_user.username == user.username
else
nil
end
end

def signed_in?(conn) do
conn.assigns[:current_user]
end
end
8 changes: 8 additions & 0 deletions lib/jax_ex/accounts/encryption.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule JaxEx.Accounts.Encryption do
alias Comeonin.Bcrypt
alias JaxEx.Accounts.User

def hash_password(password), do: Bcrypt.hashpwsalt(password)

def validate_password(%User{} = user, password), do: Bcrypt.check_pass(user, password)
end
27 changes: 26 additions & 1 deletion lib/jax_ex/accounts/user.ex
Original file line number Diff line number Diff line change
@@ -1,18 +1,43 @@
defmodule JaxEx.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
alias JaxEx.Accounts.{User, Encryption}

schema "users" do
field :username, :string
field :encrypted_password, :string

field :password, :string, virtual: true
field :password_confirmation, :string, virtual: true

timestamps()
end

@doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:username])
|> cast(attrs, [:username, :password])
|> validate_required([:username])
|> validate_length(:password, min: 6)
|> validate_confirmation(:password)
|> validate_format(:username, ~r/^[a-z0-9][a-z0-9]+[a-z0-9]$/i)
|> validate_length(:username, min: 3)
|> unique_constraint(:username)
|> downcase_username
|> encrypt_password
end

defp encrypt_password(changeset) do
password = get_change(changeset, :password)
if password do
encrypted_password = Encryption.hash_password(password)
put_change(changeset, :encrypted_password, encrypted_password)
else
changeset
end
end

defp downcase_username(changeset) do
update_change(changeset, :username, &String.downcase/1)
end
end
2 changes: 2 additions & 0 deletions lib/jax_ex_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ defmodule JaxExWeb do
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1]

import JaxEx.Accounts.Auth, only: [signed_in?: 1]

# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML

Expand Down
32 changes: 32 additions & 0 deletions lib/jax_ex_web/controllers/session_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule JaxExWeb.SessionController do
use JaxExWeb, :controller

alias JaxEx.Accounts.Auth
alias JaxEx.Repo

def new(conn, _params) do
render(conn, "new.html")
end

@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
def create(conn, %{"session" => auth_params}) do
case Auth.login(auth_params, Repo) do
{:ok, user} ->
conn
|> put_session(:current_user_id, user.id)
|> put_flash(:info, "Signed in successfully.")
|> redirect(to: Routes.page_path(conn, :index))
:error ->
conn
|> put_flash(:error, "There was a problem with your username/password")
|> render("new.html")
end
end

def delete(conn, _params) do
conn
|> delete_session(:current_user_id)
|> put_flash(:info, "Signed out successfully.")
|> redirect(to: Routes.session_path(conn, :new))
end
end
49 changes: 5 additions & 44 deletions lib/jax_ex_web/controllers/user_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ defmodule JaxExWeb.UserController do
alias JaxEx.Accounts
alias JaxEx.Accounts.User

def index(conn, _params) do
users = Accounts.list_users()
render(conn, "index.html", users: users)
end

def new(conn, _params) do
changeset = Accounts.change_user(%User{})
render(conn, "new.html", changeset: changeset)
Expand All @@ -18,45 +13,11 @@ defmodule JaxExWeb.UserController do
case Accounts.create_user(user_params) do
{:ok, user} ->
conn
|> put_flash(:info, "User created successfully.")
|> redirect(to: Routes.user_path(conn, :show, user))

{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
|> put_session(:current_user_id, user.id)
|> put_flash(:info, "Signed up successfully.")
|> redirect(to: Routes.page_path(conn, :index))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end

def show(conn, %{"id" => id}) do
user = Accounts.get_user!(id)
render(conn, "show.html", user: user)
end

def edit(conn, %{"id" => id}) do
user = Accounts.get_user!(id)
changeset = Accounts.change_user(user)
render(conn, "edit.html", user: user, changeset: changeset)
end

def update(conn, %{"id" => id, "user" => user_params}) do
user = Accounts.get_user!(id)

case Accounts.update_user(user, user_params) do
{:ok, user} ->
conn
|> put_flash(:info, "User updated successfully.")
|> redirect(to: Routes.user_path(conn, :show, user))

{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "edit.html", user: user, changeset: changeset)
end
end

def delete(conn, %{"id" => id}) do
user = Accounts.get_user!(id)
{:ok, _user} = Accounts.delete_user(user)

conn
|> put_flash(:info, "User deleted successfully.")
|> redirect(to: Routes.user_path(conn, :index))
end
end
20 changes: 20 additions & 0 deletions lib/jax_ex_web/plugs/auth.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule JaxExWeb.Plugs.Auth do
import Plug.Conn
import Phoenix.Controller

alias JaxEx.Accounts

def init(opts), do: opts

def call(conn, _opts) do
if user_id = Plug.Conn.get_session(conn, :current_user_id) do
current_user = Accounts.get_user!(user_id)
conn
|> assign(:current_user, current_user)
else
conn
|> redirect(to: "/login")
|> halt()
end
end
end
15 changes: 15 additions & 0 deletions lib/jax_ex_web/plugs/guest.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule JaxExWeb.Plugs.Guest do
import Plug.Conn
import Phoenix.Controller

def init(opts), do: opts

def call(conn, _opts) do
if Plug.Conn.get_session(conn, :current_user_id) do
conn
|> redirect(to: JaxExWeb.Router.Helpers.page_path(conn, :index))
|> halt()
end
conn
end
end
18 changes: 11 additions & 7 deletions lib/jax_ex_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ defmodule JaxExWeb.Router do
end

scope "/", JaxExWeb do
pipe_through :browser
pipe_through [:browser, JaxExWeb.Plugs.Guest]

get "/", PageController, :index
resources "/users", UserController
resources "/register", UserController, only: [:create, :new]
get "/login", SessionController, :new
post "/login", SessionController, :create
end

# Other scopes may use custom stacks.
# scope "/api", JaxExWeb do
# pipe_through :api
# end
scope "/", JaxExWeb do
pipe_through [:browser, JaxExWeb.Plugs.Auth]

delete "/logout", SessionController, :delete

get "/", PageController, :index
end
end
5 changes: 5 additions & 0 deletions lib/jax_ex_web/templates/layout/app.html.eex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
<ul>
<li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
</ul>
<%= if signed_in?(@conn) do %>
<ul id="user-actions" class="dropdown-content">
<li><%= link "Logout", to: Routes.session_path(@conn, :delete), method: :delete %></li>
</ul>
<% end %>
</nav>
<a href="http://phoenixframework.org/" class="phx-logo">
<img src="<%= Routes.static_path(@conn, "/images/phoenix.png") %>" alt="Phoenix Framework Logo"/>
Expand Down
17 changes: 17 additions & 0 deletions lib/jax_ex_web/templates/session/new.html.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<div class='row'>
<div class='col s12 m6 offset-m3'>
<div class='card'>
<div class='card-content'>
<span class='card-title'>LOGIN</span>
<div class='row card-form'>
<%= form_for @conn, Routes.session_path(@conn, :new), [as: :session], fn f -> %>
<%= text_input f, :username, placeholder: "Username" %>
<%= password_input f, :password, placeholder: "Password" %>
<%= submit "Submit", class: "btn right secondary-color" %>
<%= link "Register", to: Routes.user_path(@conn, :new), class: "btn secondary-color" %>
<% end %>
</div>
</div>
</div>
</div>
</div>
11 changes: 8 additions & 3 deletions lib/jax_ex_web/templates/user/form.html.eex
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
</div>
<% end %>

<%= label f, :username %>
<%= text_input f, :username %>
<%= text_input f, :username, placeholder: "Username" %>
<%= error_tag f, :username %>

<%= password_input f, :password, placeholder: "Password" %>
<%= error_tag f, :password %>

<%= password_input f, :password_confirmation, placeholder: "Confirm Password" %>
<%= error_tag f, :password_confirmation %>

<div>
<%= submit "Save" %>
<%= submit "Submit", class: "btn right secondary-color" %>
</div>
<% end %>
2 changes: 0 additions & 2 deletions lib/jax_ex_web/templates/user/new.html.eex
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
<h1>New User</h1>

<%= render "form.html", Map.put(assigns, :action, Routes.user_path(@conn, :create)) %>

<span><%= link "Back", to: Routes.user_path(@conn, :index) %></span>
3 changes: 3 additions & 0 deletions lib/jax_ex_web/views/session_view.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule JaxExWeb.SessionView do
use JaxExWeb, :view
end
4 changes: 3 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ defmodule JaxEx.MixProject do
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:gettext, "~> 0.11"},
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"}
{:plug_cowboy, "~> 2.0"},
{:comeonin, "~> 4.0"},
{:bcrypt_elixir, "~> 1.0"},
]
end

Expand Down
3 changes: 3 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
%{
"bcrypt_elixir": {:hex, :bcrypt_elixir, "1.1.1", "6b5560e47a02196ce5f0ab3f1d8265db79a23868c137e973b27afef928ed8006", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
"comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
"cowboy": {:hex, :cowboy, "2.6.3", "99aa50e94e685557cad82e704457336a453d4abcb77839ad22dbe71f311fcc06", [:rebar3], [{:cowlib, "~> 2.7.3", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
"cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm"},
"db_connection": {:hex, :db_connection, "2.1.0", "122e2f62c4906bf2e49554f1e64db5030c19229aa40935f33088e7d543aa79d0", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"},
"decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "3.1.7", "fa21d06ef56cdc2fdaa62574e8c3ba34a2751d44ea34c30bc65f0728421043e5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"ecto_sql": {:hex, :ecto_sql, "3.1.6", "1e80e30d16138a729c717f73dcb938590bcdb3a4502f3012414d0cbb261045d8", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0 or ~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm"},
"file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm"},
"gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule JaxEx.Repo.Migrations.AddEncryptedPasswordToUsers do
use Ecto.Migration

def change do
alter table(:users) do
add :encrypted_password, :string
end
end
end
8 changes: 4 additions & 4 deletions test/jax_ex/accounts_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ defmodule JaxEx.AccountsTest do
describe "users" do
alias JaxEx.Accounts.User

@valid_attrs %{username: "some username"}
@update_attrs %{username: "some updated username"}
@valid_attrs %{username: "username"}
@update_attrs %{username: "anotherusername"}
@invalid_attrs %{username: nil}

def user_fixture(attrs \\ %{}) do
Expand All @@ -31,7 +31,7 @@ defmodule JaxEx.AccountsTest do

test "create_user/1 with valid data creates a user" do
assert {:ok, %User{} = user} = Accounts.create_user(@valid_attrs)
assert user.username == "some username"
assert user.username == "username"
end

test "create_user/1 with invalid data returns error changeset" do
Expand All @@ -41,7 +41,7 @@ defmodule JaxEx.AccountsTest do
test "update_user/2 with valid data updates the user" do
user = user_fixture()
assert {:ok, %User{} = user} = Accounts.update_user(user, @update_attrs)
assert user.username == "some updated username"
assert user.username == "anotherusername"
end

test "update_user/2 with invalid data returns error changeset" do
Expand Down
22 changes: 19 additions & 3 deletions test/jax_ex_web/controllers/page_controller_test.exs
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
defmodule JaxExWeb.PageControllerTest do
use JaxExWeb.ConnCase

test "GET /", %{conn: conn} do
conn = get(conn, "/")
assert html_response(conn, 200) =~ "Welcome to Phoenix!"
alias JaxEx.Accounts

@create_attrs %{username: "username"}

describe "unauthenticated" do
test "GET /", %{conn: conn} do
conn = get(conn, "/")
assert redirected_to(conn) == Routes.session_path(conn, :new)
end
end

describe "authenticated" do
test "GET /", %{conn: conn} do
{:ok, user} = Accounts.create_user(@create_attrs)
conn = Plug.Test.init_test_session(conn, current_user_id: user.id)

conn = get(conn, "/")
assert html_response(conn, 200) =~ "Welcome to Phoenix!"
end
end
end
Loading

0 comments on commit 7afe607

Please sign in to comment.