def require_authenticated_user(conn, _opts) do
user = conn.assigns[:current_user]
if user do
if user.confirmed_at do
conn
else
conn
|> put_flash(:error, "You must confirm your account first.")
|> maybe_store_return_to()
|> redirect(to: Routes.user_confirmation_path(conn, :new))
|> halt()
end
else
conn
|> put_flash(:error, "You must log in to access this page.")
|> maybe_store_return_to()
|> redirect(to: Routes.user_session_path(conn, :new))
|> halt()
end
end
def require_authenticated_user(conn, _opts) do
user = conn.assigns[:current_user]
if user do
if user.confirmed_at do
conn
else
conn
|> put_flash(:error, "You must confirm your account first.")
|> maybe_store_return_to()
|> redirect(to: Routes.user_confirmation_path(conn, :new))
|> halt()
end
else
conn
|> put_flash(:error, "You must log in to access this page.")
|> maybe_store_return_to()
|> redirect(to: Routes.user_session_path(conn, :new))
|> halt()
end
end
Trying to implement confirm enforcing. Did I get it right? Also wondering what the maybe_store_return_to() is supposed to do. And why do we call halt()?
Instead of using if else in a nested way, it would be great if you think in Elixir way!! (i.e. Use Pipelines)
TL;DR:
I have a collection of functions, that I can call in a pipeline, so it won’t get unwieldy and unmaintainable!! (See my reply in the post I have linked below, for more details)
I wrote a long reply for somewhat similar question, see:
For instance, here’s how I implemented authorisation check in a sensible way:
Router:
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {DerpyCoderWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers, %{"Content-Security-Policy" => @content_security_policy}
plug :fetch_current_user
plug KickLockedUserOut
end
pipeline :authentication do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {DerpyCoderWeb.LayoutView, :minimalist}
plug :protect_from_forgery
plug :put_secure_browser_headers, %{"Content-Security-Policy" => @content_security_policy}
plug :fetch_current_user
end
pipeline :any_user do
plug :require_authenticated_user
plug EnsureRolePlug, [:super_admin, :admin, :super_user, :user]
end
pipeline :only_admin do
plug :require_authenticated_user
plug EnsureRolePlug, [:super_admin, :admin]
end
# ==============================================================================
# Following routes have Authentication mandatory
# ==============================================================================
scope "/", DerpyCoderWeb do
pipe_through [:browser, :any_user]
get "/users/settings", UserSettingsController, :edit
put "/users/settings", UserSettingsController, :update
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
end
# ==============================================================================
# Following routes may require Authentication & Authorization on some actions.
# ==============================================================================
live_session :visitor, on_mount: {DerpyCoderWeb.Permit, :anyone} do
scope "/", DerpyCoderWeb do
pipe_through [:browser]
live "/", HomePageLive, :index
live "/photos", PhotoLive.Index, :index
live "/photos/new", PhotoLive.Index, :new
live "/photos/:id", PhotoLive.Show, :show
end
end
# ==============================================================================
# Following routes have Authentication as well as Authorization mandatory
# ==============================================================================
live_session :user, on_mount: {DerpyCoderWeb.Permit, :any_user} do
scope "/", DerpyCoderWeb do
pipe_through [:browser, :any_user]
live "/photos/:id/edit", PhotoLive.Index, :edit
live "/photos/:id/show/edit", PhotoLive.Show, :edit
end
end
scope "/admin", DerpyCoderWeb do
pipe_through [:browser, :only_admin]
live_dashboard "/live_dashboard", metrics: DerpyCoderWeb.Telemetry
end
# ==============================================================================
# Below routes have custom root layout
# ==============================================================================
live_session :user_dashboard,
on_mount: {DerpyCoderWeb.Permit, :any_user},
root_layout: {DerpyCoderWeb.LayoutView, "user_dashboard.html"} do
scope "/users", DerpyCoderWeb do
pipe_through [:browser, :any_user]
live "/dashboard", UserDashboardLive, :index
end
end
live_session :admin_dashboard,
on_mount: {DerpyCoderWeb.Permit, :only_admin},
root_layout: {DerpyCoderWeb.LayoutView, "admin_dashboard.html"} do
scope "/admin", DerpyCoderWeb do
pipe_through [:browser, :only_admin]
live "/dashboard", AdminDashboardLive, :index
end
end
Plugs:
Ensure Role Plug:
defmodule DerpyCoderWeb.EnsureRolePlug do
@moduledoc """
This plug ensures that a user has a particular role before accessing a given route.
## Example
Let's suppose we have three roles: :admin, :manager and :user.
If you want a user to have at least manager role, so admins and managers are authorized to access a given route
plug DerpyCoderWeb.EnsureRolePlug, [:admin, :manager]
If you want to give access only to an admin:
plug DerpyCoderWeb.EnsureRolePlug, :admin
"""
import Plug.Conn
alias Phoenix.Controller
alias Plug.Conn
@doc false
@spec init(any()) :: any()
def init(config), do: config
@doc false
@spec call(Conn.t(), atom() | [atom()]) :: Conn.t()
def call(conn, roles) do
conn.assigns.current_user
|> has_role?(roles)
|> maybe_halt(conn)
end
defp has_role?(%{} = user, roles) when is_list(roles),
do: Enum.any?(roles, &has_role?(user, &1))
defp has_role?(%{role: role}, role), do: true
defp has_role?(_user, _role), do: false
defp maybe_halt(true, conn), do: conn
defp maybe_halt(_any, conn) do
conn
|> Controller.put_flash(:error, "Unauthorized")
|> Controller.redirect(to: signed_in_path(conn))
|> halt()
end
defp signed_in_path(_conn), do: "/"
end
KickLockedUserOut
defmodule DerpyCoderWeb.KickLockedUserOut do
@moduledoc """
This plug ensures that a user isn't locked.
## Example
plug DerpyCoderWeb.KickLockedUserOut
"""
import Plug.Conn, only: [halt: 1]
alias Phoenix.Controller
alias Plug.Conn
alias DerpyCoderWeb.UserAuth
@doc false
@spec init(any()) :: any()
def init(opts), do: opts
@doc false
@spec call(Conn.t(), any()) :: Conn.t()
def call(conn, _opts) do
conn.assigns.current_user
|> locked?()
|> maybe_halt(conn)
end
defp locked?(%{id: id, locked_at: locked_at}) when not is_nil(locked_at),
do: not DerpyCoder.Accounts.is_super_admin?(id)
defp locked?(_user), do: false
defp maybe_halt(true, conn) do
conn
|> Controller.put_flash(:error, "Your account is locked.")
|> Controller.redirect(to: signed_in_path(conn))
|> UserAuth.log_out_user()
|> halt()
end
defp maybe_halt(_any, conn), do: conn
defp signed_in_path(_conn), do: "/"
end
Permits:
defmodule DerpyCoderWeb.Permit do
@moduledoc """
Ensures current_user is assigned to LiveViews attaching this hook.
"""
import Phoenix.LiveView
alias DerpyCoder.Accounts
import DerpyCoderWeb.LiveHelpers
# ==============================================================================
# Used for live view routes, that do not require authentication for first render.
# Assigns current_user to the socket, if available.
# Returns `socket`
# ==============================================================================
def on_mount(:anyone, _params, session, socket) do
{:cont, socket}
|> assign_user(session)
|> maybe_verify_lock()
|> maybe_subscribe_user()
end
# ==============================================================================
# Used for live view routes, that do require authentication for first render.
# Assigns current_user to the socket.
# User must have confirmed their email.
# If user is not authenticated, they are asked to login.
# Returns `socket`
# ==============================================================================
def on_mount(:any_user, _params, session, socket) do
{:cont, socket}
|> assign_user(session)
|> verify_user()
|> verify_lock()
|> verify_email()
|> subscribe_user()
end
# ==============================================================================
# Used for live view routes, that requires authentication and authorization for first render.
# Assigns current_user to the socket.
# User must have confirmed their email.
# If user is not authenticated, they are asked to login.
# Finally if user has no authorization, based on roles passed in, they are notified.
# Returns `socket`
# ==============================================================================
def on_mount(:only_admin, _params, session, socket) do
{:cont, socket}
|> assign_user(session)
|> verify_user()
|> verify_lock()
|> verify_email()
|> verify_role(~w(super_admin admin)a)
|> subscribe_user()
end
# ==============================================================================
# Find and assign current user.
# ==============================================================================
defp assign_user({:cont, socket}, session) do
socket =
socket
|> assign_new(:current_user, fn ->
find_current_user(session)
end)
{:cont, socket}
end
# ==============================================================================
# If current user exists, do something.
# ==============================================================================
defp maybe_verify_lock({:cont, socket}) do
current_user = socket.assigns.current_user
if current_user do
{:cont, socket}
|> verify_lock()
end
{:cont, socket}
end
defp maybe_subscribe_user({:cont, socket}) do
current_user = socket.assigns.current_user
if current_user do
{:cont, socket}
|> subscribe_user()
end
{:cont, socket}
end
# ==============================================================================
# Subscribe to user_centric topic, for broadcasts, like:
# Notifications, Account Lock, etc.
# ==============================================================================
defp subscribe_user({:cont, socket}) do
current_user = socket.assigns.current_user
if connected?(socket), do: Accounts.subscribe(current_user)
{:cont, socket}
end
defp subscribe_user({:halt, _} = arg), do: arg
defp find_current_user(%{"user_token" => user_token}) do
Accounts.get_user_by_session_token(user_token)
end
defp find_current_user(_), do: nil
end
Helpers:
defmodule DerpyCoderWeb.LiveHelpers do
@moduledoc """
ALl helpers commonly used in LiveView, lives here.
"""
import Phoenix.LiveView
import Phoenix.LiveView.Helpers
alias Phoenix.LiveView.JS
alias DerpyCoderWeb.Router.Helpers, as: Routes
def env(atom) when is_atom(atom), do: Application.get_env(:derpy_coder, :environment) == to_string(atom)
def env(str) when is_binary(str), do: Application.get_env(:derpy_coder, :environment) == str
def env(list) when is_list(list), do: Application.get_env(:derpy_coder, :environment) in list
def inspect_source(path, line \\ 1) do
System.cmd("code", ["--goto", "#{path}:#{line}"])
end
# ==============================================================================
# Verify that user is there.
# ==============================================================================
def verify_user({:cont, socket}) do
current_user = socket.assigns.current_user
if current_user do
{:cont, socket}
else
{:halt, socket |> ask_user_to_login()}
end
end
def verify_user({:halt, _} = arg), do: arg
# ==============================================================================
# Verify that user's Account isn't locked.
# ==============================================================================
def verify_lock({:cont, socket}) do
current_user = socket.assigns.current_user
if current_user.locked_at && current_user.role != :super_admin &&
not DerpyCoder.Accounts.is_super_admin?(current_user.id) do
{:halt, socket |> kick_locked_user_out()}
else
{:cont, socket}
end
end
def verify_lock({:halt, _} = arg), do: arg
# ==============================================================================
# Verify that user has confirmed their email.
# ==============================================================================
def verify_email({:cont, socket}) do
current_user = socket.assigns.current_user
if current_user.confirmed_at do
{:cont, socket}
else
{:halt, socket |> ask_user_to_confirm_email()}
end
end
def verify_email({:halt, _} = arg), do: arg
# ==============================================================================
# Verify that the current user has the roles required.
# ==============================================================================
def verify_role({:cont, socket}, roles) do
current_user = socket.assigns.current_user
if role_matches?(current_user.role, roles) do
{:cont, socket}
else
{:halt, socket |> kick_unauthorized_user_out()}
end
end
def verify_role({:halt, _} = arg, _), do: arg
# ==============================================================================
# Helpers
# ==============================================================================
def ask_user_to_login(socket) do
socket
|> put_flash(:error, "You must log in to access this page.")
|> assign(redirect_to: Routes.user_session_path(socket, :new))
|> halt()
end
def ask_user_to_confirm_email(socket) do
socket
|> put_flash(:error, "You must confirm your email address to proceed.")
|> halt()
end
def kick_unauthorized_user_out(socket) do
socket
|> put_flash(:error, "Unauthorized.")
|> halt()
end
def kick_locked_user_out(socket) do
socket
|> put_flash(:error, "Your account is locked.")
|> halt()
end
# ==============================================================================
# Misc
# ==============================================================================
defp halt(%{assigns: %{redirect_to: redirect_to, return_to: return_to}} = socket),
do: socket |> redirect(to: "#{redirect_to}?return_to=#{return_to}")
defp halt(%{assigns: %{redirect_to: redirect_to}} = socket),
do: socket |> redirect(to: redirect_to)
defp halt(%{assigns: %{return_to: return_to}} = socket),
do: socket |> redirect(to: return_to)
defp halt(%{assigns: _} = socket),
do: socket |> redirect(to: "/")
defp role_matches?(user_role, role) when is_atom(role), do: user_role === role
defp role_matches?(user_role, roles) when is_list(roles), do: user_role in roles
end
P.S. I may not have the most experience, and this code isn’t close to complete. Once I am satisfied and Phoenix 1.7 is released, I will open source everything.
Upon checking user’s login status, you might have to redirect them to login page.
For them to return back to where they were, we need to store their return to address!!
For example, if the user was going to like an image in a /photos/cute_cat_photo page, and you require them to login first to like something.
Then return_to_address will be /photos/cute_cat_photo page, and once the user is done login in /login page, they can be nicely escorted back to their original destination of interest!!
halt() usage:
I figured it out intuitively, I’m sure there are books to explain this.
In Elixir, Pipelines can be used to chain together functions and halt can break out of the pipeline, just like return statement in other languages can break out of a function!!:
Run the following code, in LiveBook:
defmodule Demo do
alias Integer
def add_two_to_odd_number({:cont, num}) do
if is_odd(num) do
{:cont, num + 2}
else
{:halt, num}
end
end
def add_two_to_odd_number({:halt, _} = arg), do: arg
def mul_two_to_odd_number({:cont, num}) do
if is_odd(num) do
{:cont, num * 2}
else
{:halt, num}
end
end
def mul_two_to_odd_number({:halt, _} = arg), do: arg
end
{:cont, 3}
|> Demo.add_two_to_odd_number() # Since 3 is odd, 2 will be added and 5 will be passed along
|> Demo.mul_two_to_odd_number() # Since 5 is odd, 2 will be multiplied and 10 will be passed along
|> Demo.add_two_to_odd_number() # Since 10 is even, halt will be passed along
|> Demo.add_two_to_odd_number() # Since halt is passed along, this function call is skipped
|> dbg()
As you can see, the pipeline skipped last two function calls as internally halt was introduced to the payload.
Similarly the halt() call in your example also does the same thing, it introduces a :halt atom into the conn payload, so that next in line function calls in the pipeline skip, by pattern matching in functions.
i.e. the def some_func({:halt, ...}) definition you see in the example above.`
P.S. I am new to Elixir, haven’t purchased any Elixir book yet, so I can’t point out some good example or blog post for this.