README.md

# LiveGuard

A simple package to protect the LiveView lifecycle stages such as `:mount`, `:handle_params`, `:handle_event`, `:handle_info` and `:handle_async`.

## Installation

For the latest master:
```elixir
def deps do
  [
    {:live_guard, github: "FabianDaniel00/live_guard"}
  ]
end
```
For the latest release:
```elixir
def deps do
  [
    {:live_guard, "~> 0.1.8"}
  ]
end
```
Then run `mix deps.get` to fetch the dependencies.

## Config

- `:current_user`

  **You need to assign the current user to the socket before LiveGuard [`on_mount/4`](https://hexdocs.pm/live_guard/LiveGuard.html#on_mount/4) callback is called.**.
  The default assign name for the current user is `:current_user`.
  If you assign the current user as another than `:current_user` you can set in the config:
  ```elixir
  config :live_guard, :current_user, :user
  ```

- `:unauthorized_handler`

  This function handles unauthorized LiveView lifecycle stages.
  It's called when the [`allowed?/4`](https://hexdocs.pm/live_guard/LiveGuard.Allowed.html#allowed?/4) function returns `false`.

  By default it will put an error flash message with text "_You don't have permission to do that!_".

  `:mount` and `:handle_params` LiveView lifecycle stages needs redirect after it detected as unauthorized.
  In this case by default it will redirect to the home page (`/`).

  You can set a custom handler in the config:
  ```elixir
  config :live_guard, :unauthorized_handler, {MyModule, :my_handle_unauthorized}
  ```
  It's called with 2 inputs, first is a `socket`, second is `is_redirect` _(boolean)_.

## Usage

LiveGuard provide an `on_mount/4` callback which can be used in Phoenix LiveViews. [Read the docs](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#on_mount/1).

Since this is an `on_mount/1` callback you can use it three ways:

- with [`live_session/3`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Router.html#live_session/3) to protect a couple of LiveViews lifecycle stages at once:
  ```elixir
  # lib/my_app_web/router.ex

  defmodule MyAppWeb.Router do
    use MyAppWeb, :router

    live_session :default, on_mount: LiveGuard do
      # routes...
    end
  end
  ```
- if you want to protect LiveView lifecycle stages in every LiveViews, you can achive that with the following code:
  ```elixir
  # lib/my_app_web.ex

  defmodule MyAppWeb do
    # some code...

    def live_view do
      quote do
        use Phoenix.LiveView, layout: {MyAppWeb.Layouts, :app}

        on_mount LiveGuard

        unquote(html_helpers())
      end
    end

    # some code...
  end
  ```
- and if you want to protect an individual LiveView lifecycle stages you can achive that with the following code:
  ```elixir
  # lib/my_app_web/live/my_module_live.ex

  defmodule MyAppWeb.MyModuleLive do
    use MyAppWeb, :live_view

    on_mount LiveGuard

    def mount(_params, _session, socket) do
      {:ok, socket}
    end

    # some code...
  end
  ```

### Implementation

For now you should ask, _okay but how it will know how to protect the LiveView lifecycle stages?_

You need to implement [`allowed?/4`](https://hexdocs.pm/live_guard/LiveGuard.Allowed.html#allowed?/4) protocol functions.
The first input of `allowed?/4` function is the **user**, the second is the **LiveView module**, the third is the **LiveView lifecycle stage** and the last is **LiveView lifecycle stage inputs**. In this way you can pattern match to your needings. You can put this file anywhere but `/lib/my_app_web/live/abilities.ex` is recommended.

**It must return a boolean.**

```elixir
# /lib/my_app_web/live/abilities.ex

defimpl LiveGuard.Allowed, for: User do
  @before_compile {LiveGuard, :before_compile_allowed}

  def allowed?(
        %User{role: role},
        MyModuleLive,
        :handle_event,
        {"delete_item", _unsigned_params, _socket}
      )
      when role in [:viewer, :customer],
      do: false

  # other `allowed?/4` functions...
end
```
> Note: As you can see, you don't have to define catch-all `allowed?/4` function because we used `@before_compile {LiveGuard, :before_compile_allowed}` hook. It returns `true`. This is optional.

If the user is not authenticated you can add the following implementation as below:
```elixir
defimpl LiveGuard.Allowed, for: Atom do
  @before_compile {LiveGuard, :before_compile_allowed}

  def allowed?(nil, MyModuleLive, :handle_event, {"delete_item", _unsigned_params, _socket}),
    do: false

  # other `allowed?/4` functions...
end
```

### Optimization _(optional)_

By default if you use the `on_mount/4` callback of LiveGuard, it will attach hooks to attachable LiveView lifecycle stages (`:handle_params`, `:handle_event`, `:handle_info` and `:handle_async`).
If you need to protect for example only the `:handle_event` LiveView lifecycle stage for an individual LiveView module you can use this function.
You can put this file anywhere but `/lib/my_app_web/live/guarded_stages.ex` is recommended.

**It must return a list of [valid attachable LiveView lifecycle stages](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#attach_hook/4) (unless `:after_render`).**

#### Example

```elixir
# /lib/my_app_web/live/guarded_stages.ex

defimpl LiveGuard.GuardedStages, for: Atom do
  @before_compile {LiveGuard, :before_compile_guarded_stages}

  def guarded_stages(MyModuleLive), do: [:handle_event]

  # other `guarded_stages?/1` functions...
end
```
In this case it will only attach hook to `:handle_event` LiveView lifecycle stage.
> Note: As you can see, you don't have to define catch-all `guarded_stages/1` function because we used `@before_compile {LiveGuard, :before_compile_guarded_stages}` hook. It returns the valid attachable LiveView lifecycle stages (`:handle_params`, `:handle_event`, `:handle_info` and `:handle_async`). This is optional.

## License

MIT License. Copyright 2023 Daniel Fabian.

## Few words from the author
[GitHub repository](https://github.com/FabianDaniel00/live_guard)

_This package is inspired by [canary](https://github.com/cpjk/canary)._