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)
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
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.