README.md |
Auth tutorial for setting up basic user auth with put_session()
Deps used
# Admin auth
{:basic_auth, "~> 2.2.2"},
# Password hashing
{:bcrypt_elixir, "~> 2.0"}
Setup
Start off by running mix deps.get
User Schema
Make sure that you have some context that is like Accounts.User
with email:unique
and password_hash
and that you have Accounts
module with get_user!(id)
... etc.
You can do this with
terminal:~/tutorial/$ mix phx.gen.html Accounts User users email:string:unique password_hash:string
Add resources "/users", UserController, singleton: true
to the router.ex
Password hashing
After that open up accounts/user.ex
and add line bellow to changeset()
def changeset(user, attrs) do
...
|> update_change(:password_hash, &Bcrypt.hash_pwd_salt/1)
end
Auth helper
In /tutorial_web/
create folder /helpers
and create file auth.ex
defmodule TutorialWeb.Helpers.Auth do
import Plug.Conn, only: [get_session: 2]
alias Tutorial.{Repo, Accounts.User}
def signed_in?(conn) do
user_id = get_session(conn, :current_user_id)
if user_id, do: !!Repo.get(User, user_id)
end
end
Then open your tutorial_web.ex
and inside add this import
, we are doing this because we want to have this function in every view
def view do
quote do
...
import TutorialWeb.Helpers.Auth, only: [signed_in?: 1]
end
end
Session templates/view/router/controller
Templates
Create /session
folder in /templates
and inside create 2 files sign-in.html.eex
and sign-up.html.eex
One will be used for Signing in and the other for Signing out
/session/sign-in.html
<%= form_for @conn, Routes.session_path(@conn, :create_session),[as: :session] , fn f -> %>
<%= label f, :email %>
<%= text_input f, :email %>
<%= error_tag f, :email %>
<%= label f, :password %>
<%= password_input f, :password_hash %>
<%= error_tag f, :password_hash %>
<div>
<%= submit "Sign in" %>
</div>
<% end %>
/session/sign-up.html
<%= form_for @changeset, Routes.session_path(@conn, :create_user) , fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= label f, :email %>
<%= text_input f, :email %>
<%= error_tag f, :email %>
<%= label f, :password %>
<%= password_input f, :password_hash %>
<%= error_tag f, :password_hash %>
<div>
<%= submit "Sign up" %>
</div>
<% end %>
Views
Create session_view.ex
in /views
folder
defmodule TutorialWeb.SessionView do
use TutorialWeb, :view
end
Router
under you scope "/"
add following
scope "/", TutorialWeb do
pipe_through :browser
...
# Sign in
get "/sign-in", SessionController, :sign_in
post"/sign-in", SessionController, :create_session
# Sign up
get "/sign-up", SessionController, :sign_up
post"/sign-up", SessionController, :create_user
# Sign out
post "/sign-out", SessionController, :sign_out
end
Controller
Create session_controller.ex
in /controllers
defmodule TutorialWeb.SessionController do
use TutorialWeb, :controller
alias Tutorial.Accounts
alias Accounts.User
def sign_in(conn, _params) do
# If user is logged in and tries to connecto to "/sign-in" redirect him
if is_logged?(conn) do
redirect(conn, to: Routes.user_path(conn, :show))
else
render(conn, "sign-in.html")
end
end
def sign_up(conn, _params) do
# If user is logged in and tries to connecto to "/sign-up" redirect him
if is_logged?(conn) do
redirect(conn, to: Routes.user_path(conn, :show))
else
changeset = Accounts.change_user(%User{})
render(conn, "sign-up.html", changeset: changeset)
end
end
defp is_logged?(conn), do: !!get_session(conn, :current_user_id)
def create_session(conn, %{"session" => auth_params} = _params) do
user = Accounts.get_by_email(auth_params["email"])
case Bcrypt.check_pass(user, auth_params["password_hash"]) do
{:ok, user} ->
conn
|> put_session(:current_user_id, user.id)
|> put_flash(:info, "Sign in, successful!")
|> redirect(to: Routes.user_path(conn, :show))
{:error, _} ->
conn
|> put_flash(:error, "Invalid e-mail/password. Try again!")
|> redirect(to: Routes.session_path(conn, :sign_in))
end
end
def create_user(conn, %{"user" => user_params}) do
case Accounts.create_user(user_params) do
{:ok, user} ->
conn
|> put_session(:current_user_id, user.id)
|> put_flash(:info, "Sign up, successful!")
|> redirect(to: Routes.user_path(conn, :show))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "sign-up.html", changeset: changeset)
end
end
def sign_out(conn, _params) do
conn
|> delete_session(:current_user_id)
|> put_flash(:info, "Signed out.")
|> redirect(to: Routes.page_path(conn, :index))
end
end
Update accounts.ex
Remember when we wrote user = Accounts.get_by_email(auth_params["email"])
we need to define that fn
Open accounts.ex
and add this fn after get_user!(id)
def get_by_email(nil), do: nil
def get_by_email(email), do: User |> Repo.get_by(email: email)
NOTE We will use advantage of pattern matching and create two fn-s just in case we get nil
Update user_controller.ex
Part 1 (update params)
In UserController
show
, edit
, update
and delete
second param is trying to pattern match for %{"id" => id}
but since we are going to get user data from conn.asssigns.current_user
go ahead and replace %{"id" => id}
with _params
def FN(conn, %{"id" => id}) do
...
end
# CHANGE TO
def FN(conn, _params) do
...
end
Do that for all 4 of them
Part 2 (update user data)
We are still in UserController
and fn's show
, edit
, update
and delete
are using that id
that we just removed, to obtain user data. We are going to change that now
# Change
user = Accounts.get_user!(id)
# Into
user = conn.assigns.current_user
Set conn.assigns.current_user
Now we need to assign current_user
to @conn
. But how?
Read more to find out. :)
Setting up Auth Plug
You remember that /helpers
folder that we created earlier? Open it and create new file inside, called plug_auth.ex
and put this code inside.
defmodule TutorialWeb.Helpers.AuthGuest do
import Plug.Conn
import Phoenix.Controller
alias Tutorial.Accounts
def init(default), do: default
def call(conn, _opts) do
user_id = get_session(conn, :current_user_id)
auth_reply(conn, user_id)
end
defp auth_reply(conn, nil) do
conn
|> put_flash(:error, "You have to sign in first!")
|> redirect(to: "/sign-in")
|> halt()
end
defp auth_reply(conn, user_id) do
user = Accounts.get_user!(user_id)
conn
|> assign(:current_user, user)
end
end
After that add this code into your router.ex
pipeline :auth do
plug TutorialWeb.Helpers.AuthGuest
end
And add that pipeline in pipe_through
for scope you want to require logged user
For example:
scope "/", TutorialWeb do
pipe_through [:browser, :auth]
resources "/users", UserController, singleton: true
end
Templates update /users/ && add Sign-in and Sign-out links
User template
After adding all of this, few things need to be considered.
-
singleton: true
- We don't want our user to have acces to the list of ALL USERS so we need to remove links that point
to: Routes.user_path(@conn, :index)
. Lets give that power only to admin.
- We don't want our user to have acces to the list of ALL USERS so we need to remove links that point
-
UserController fn-s
new
andcreate
- Again we don't want our user to be able to use these functions that are pre-generated by
mix phx.gen.html
so we should delete those files, controller functions and limit them inrouter.ex
. Update existing resources tag with following.resources "/users", UserController, only: [:show, :edit, :update], singleton: true
- Again we don't want our user to be able to use these functions that are pre-generated by
-
Update UserController fn's with redirect
- Some fn's where you redirect to path that doesn't take user as params
but you still pass it because of
mix phx.gen.html
. You just need to remove that user.
- Some fn's where you redirect to path that doesn't take user as params
but you still pass it because of
And finally lets open /layout/app.html.eex
and add following
<%= if signed_in?(@conn) do %>
<%= link "Sign out", to: Routes.session_path(@conn, :sign_out), method: :post %>
<% else %>
<%= link "Sign in", to: Routes.session_path(@conn, :sign_in), method: :get %>
|
<%= link "Sign up", to: Routes.session_path(@conn, :sign_up), method: :get %>
<% end %>
mix phx.server
Yep you read it, just run your app now and it should most of it gucci. :)
except the html.eex
files that you didn't change :P