Mix phx.gen.auth stuck

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.

3 Likes