defmodule LiveGuard do @moduledoc """ 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 all attachable LiveView lifecycle stages. """ @typedoc "All the [attachable LiveView lifecycle stages](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#attach_hook/4)." @type attachable_lifecycle_stages() :: :handle_params | :handle_event | :handle_info | :after_render import Phoenix.LiveView, only: [attach_hook: 4] import LiveGuard.Allowed, only: [allowed?: 4] import LiveGuard.GuardedStages, only: [guarded_stages: 1] alias LiveGuard.{Helpers, GuardedStages} alias Phoenix.LiveView alias LiveView.Socket @attachable_lifecycle_stages [:handle_params, :handle_event, :handle_info, :after_render] @current_user Application.compile_env(:live_guard, :current_user, :current_user) @unauthorized_handler Application.compile_env( :live_guard, :unauthorized_handler, {Helpers, :handle_unauthorized} ) @doc """ 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, 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 hook_fn(stage :: :after_render) :: (socket :: Socket.t() -> socket :: Socket.t()) defp hook_fn(:after_render = stage), do: fn socket -> (allowed?(:erlang.map_get(@current_user, socket.assigns), socket.view, stage, {socket}) && socket) || unauthorized_handler({socket, true}) 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 """ #### _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 """ #### _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 all the [valid attachable LiveView lifecycle stages](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#attach_hook/4), 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