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