defmodule LiveGuard do @moduledoc ~S""" A simple module with `on_mount/4` callback. This can used in [Phoenix LiveView](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#on_mount/1) applications. The main goal is to protect the Phoenix LiveView lifecycle stages easily. It uses the [`attach_hook/4`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#attach_hook/4) function to authorize attachable LiveView lifecycle stages (`:handle_params`, `:handle_event` and `:handle_info`). """ @typedoc "The [attachable LiveView lifecycle stages](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#attach_hook/4) (`:handle_params`, `:handle_event` and `:handle_info`)." @type attachable_lifecycle_stages() :: :handle_params | :handle_event | :handle_info import Phoenix.LiveView, only: [attach_hook: 4] import LiveGuard.Allowed, only: [allowed?: 4] import LiveGuard.GuardedStages, only: [guarded_stages: 1] import LiveGuard.Config alias LiveGuard.GuardedStages alias Phoenix.LiveView alias LiveView.Socket @attachable_lifecycle_stages [:handle_params, :handle_event, :handle_info] @doc ~S""" All attachable LiveView lifecycle stages by LiveGuard. """ @spec attachable_lifecycle_stages() :: [attachable_lifecycle_stages()] def attachable_lifecycle_stages(), do: @attachable_lifecycle_stages @doc ~S""" You can find the documentation of `on_mount/1` [here](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#on_mount/1). """ @spec on_mount( on_mount_name :: :default, params :: LiveView.unsigned_params() | :not_mounted_at_router, session :: map(), socket :: Socket.t() ) :: {:cont | :halt, socket :: Socket.t()} def on_mount(:default = _on_mount_name, params, session, socket), do: (allowed?( :erlang.map_get(current_user(), socket.assigns), socket.view, :mount, {params, session, socket} ) && {:cont, Enum.reduce( (GuardedStages.impl_for(:atom) && guarded_stages(socket.view)) || @attachable_lifecycle_stages, socket, fn stage, socket -> attach_hook( socket, "live_guard_#{stage}", stage, hook_fn(stage) ) end )}) || {:halt, unauthorized_handler({socket, true})} @spec hook_fn(stage :: :handle_params) :: (unsigned_params :: LiveView.unsigned_params(), uri :: String.t(), socket :: Socket.t() -> {:cont | :halt, socket :: Socket.t()}) defp hook_fn(:handle_params = stage), do: fn unsigned_params, uri, socket -> (allowed?( :erlang.map_get(current_user(), socket.assigns), socket.view, stage, {unsigned_params, uri, socket} ) && {:cont, socket}) || {:halt, unauthorized_handler({socket, true})} end @spec hook_fn(stage :: :handle_event) :: (event :: binary(), unsigned_params :: LiveView.unsigned_params(), socket :: Socket.t() -> {:cont | :halt, socket :: Socket.t()}) defp hook_fn(:handle_event = stage), do: fn event, unsigned_params, socket -> (allowed?( :erlang.map_get(current_user(), socket.assigns), socket.view, stage, {event, unsigned_params, socket} ) && {:cont, socket}) || {:halt, unauthorized_handler({socket, false})} end @spec hook_fn(stage :: :handle_info) :: (term(), socket :: Socket.t() -> {:cont | :halt, socket :: Socket.t()}) defp hook_fn(:handle_info = stage), do: fn msg, socket -> (allowed?( :erlang.map_get(current_user(), socket.assigns), socket.view, stage, {msg, socket} ) && {:cont, socket}) || {:halt, unauthorized_handler({socket, false})} end @spec unauthorized_handler({socket :: Socket.t(), is_redirect :: boolean()}) :: Socket.t() defp unauthorized_handler({socket, is_redirect}), do: unauthorized_handler() |> elem(0) |> apply(elem(unauthorized_handler(), 1), [socket, is_redirect]) @doc ~S""" #### _Optional_ This macro can be used with [`@before_compile`](https://hexdocs.pm/elixir/Module.html#module-before_compile) hook. It will add a catch-all `allowed?/4` function returning `true`, to the end the module. ## Example ```elixir defimpl LiveGuard.Allowed, for: User do @before_compile {LiveGuard, :before_compile_allowed} # some code... end ``` """ @spec before_compile_allowed(env :: map()) :: tuple() defmacro before_compile_allowed(_env), do: quote(do: def(allowed?(_user, _live_view_module, _stage, _stage_inputs), do: true)) @doc ~S""" #### _Optional_ This macro can be used with [`@before_compile`](https://hexdocs.pm/elixir/Module.html#module-before_compile) hook. It will add a catch-all `guarded_stages/1` function returning the [valid attachable LiveView lifecycle stages](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#attach_hook/4) (`:handle_params`, `:handle_event` and `:handle_info`), to the end the module. ## Example ```elixir defimpl LiveGuard.GuardedStages, for: Atom do @before_compile {LiveGuard, :before_compile_guarded_stages} # some code... end ``` """ @spec before_compile_guarded_stages(env :: map()) :: tuple() defmacro before_compile_guarded_stages(_env), do: quote(do: def(guarded_stages(_live_view_module), do: unquote(@attachable_lifecycle_stages))) end