# `Permit.Phoenix.LiveView`
[🔗](https://github.com/curiosum-dev/permit_phoenix/blob/v0.5.1/lib/permit_phoenix/live_view.ex#L1)

Using this module, Permit authorization can be integrated with Phoenix LiveView at three key points:
1. During mount (via the `on_mount: Permit.Phoenix.LiveView.AuthorizeHook` hook)
2. During live navigation (via the `handle_params/3` callback)
3. During events (via the `handle_event/3` callback)

This way, Permit.Phoenix's load-and-authorize mechanism occurs regardless of whether the user has
navigated to the page directly (from outside a LiveView session), or has navigated to a URL that stays
within the same LiveView session (or the same LiveView instance), and also when an event is triggered
(e.g. a `delete` button is clicked).

## Schematic overview

The diagram below illustrates the flow of authorization in Permit.Phoenix LiveView in the context of
what happens when a user navigates to a LiveView route.

```mermaid
flowchart TD
  Start([USER NAVIGATES])
  Start --> Outside["From Outside
  (browser link, bookmark)"]
  Start --> SameSession["Same LiveView Session
  (diff instance)"]
  Start --> SameInstance["Same LiveView Instance
  (patch nav)"]

  Outside --> Mount["MOUNT PHASE
  New LiveView mounts, AuthorizeHook module
  attaches hooks to params and event handlers
  and loads & authorizes based on @live_action,
  then assigns to @loaded_resource(s)"]
  SameSession --> Mount

  SameInstance --> HandleParams["HANDLE_PARAMS/3
  Load & authorize based on @live_action
  Assign @loaded_resource(s)"]
  Mount --> HandleParams

  HandleParams --> Running["LiveView instance running"]
  Running -.-> HandleEvent["HANDLE_EVENT/3
  Authorize events via @permit_action
  or event_mapping/0"]

  Running -.-> HandleParams
  HandleEvent -.-> Running
```

## Setup

In the router, in a `live_session` that authenticates the user, add `Permit.Phoenix.LiveView.AuthorizeHook`
after the `:ensure_authenticated` hook at `:on_mount`:

    live_session :require_authenticated_user,
      on_mount: [
        {MyAppWeb.UserAuth, :ensure_authenticated},
        Permit.Phoenix.LiveView.AuthorizeHook         # add Permit.Phoenix's hook at mount
      ] do
      live "/live_articles", ArticleLive.Index, :index
      live "/live_articles/new", ArticleLive.Index, :new
      live "/live_articles/:id/edit", ArticleLive.Index, :edit

      live "/live_articles/:id", ArticleLive.Show, :show
      live "/live_articles/:id/show/edit", ArticleLive.Show, :edit
    end

Names of `:live_action`'s defined in the router are important - they are directly mapped to Permit action names.
If an action name is defined in your app's actions module (see `Permit.Phoenix.Actions`), or in the router, it will
be generated as a convenience function in your permissions module.

```
# Your router

defmodule MyAppWeb.Router do
  use Phoenix.Router
  import Phoenix.LiveView.Router

  live_session :require_authenticated_user, on_mount: [
    {MyAppWeb.UserAuth, :ensure_authenticated},
    Permit.Phoenix.LiveView.AuthorizeHook
  ] do
    # Define routes with :live_action named :view and :all
    live("/articles/:id/view", MyAppWeb.ArticleLive, :view)
    live("/articles/:id/all", MyAppWeb.ArticleLive, :all)
  end
end

# Your actions module
defmodule MyApp.Actions do
  use Permit.Phoenix.Actions, router: MyAppWeb.Router

  # Permit.Phoenix.Actions includes :index, :show, :update, :edit, :create, :new, :delete
  # Specifying the router will include action names from the router

  # Clarify that the :view action relates to a single resource, not a listing.
  # :all will be a plural action, loading all articles for the current user.
  @impl true
  def singular_actions do
    [:view]
  end
end

# Your permissions module
defmodule MyApp.Permissions do
  use Permit.Ecto.Permissions, actions_module: MyApp.Actions

  # The :view and :all actions generate `view/2` and `all/2` functions, so we can use them here
  def can(:show = _action) do
    permit()
    |> view(MyApp.Article)
    |> all(MyApp.Article)
  end
end
```

Then, configure LiveViews to use the authorization mechanism. It can be put in individual modules,
or in the `MyAppWeb` module's `live_view` function:

    defmodule MyAppWeb do
      def live_view do
        quote do
          use Permit.Phoenix.LiveView,
            authorization_module: MyApp.Authorization,
            # other options...
        end
      end
    end

Options can be set as `use` keywords, or as callback implementations (which take precedence). This way, you can override them
in individual LiveViews. Typically, at the very least, you'll want to set the `resource_module`
to the related schema.

    defmodule MyAppWeb.ArticleLive.Index do
      use MyAppWeb, :live_view

      @impl true
      def resource_module, do: MyApp.Article
    end

## Navigation & mounting authorization

Navigating to a LiveView route results in triggering the `handle_params/3` callback. This may occur
in three scenarios:
* navigating from outside a LiveView session (e.g. from a link in an email or a browser bookmark),
* navigating within the same LiveView session, but a different LiveView instance,
* navigating within the same LiveView instance.

### Authorization flow

The `Permit.Phoenix.LiveView.AuthorizeHook` module taps into the `handle_params/3` callback processing.
When it is triggered on navigation to a LiveView route, the route's `:live_action`
is used to determine the action to authorize. Records are filtered and loaded is loaded using
Permit.Ecto based on the `resource_module` and `id_param_name`/`id_struct_field_name` options,
the `base_query`, and resolved authorization conditions (or with a loader function when Permit.Ecto
is not used).

```mermaid
flowchart TD
  Start["HANDLE_PARAMS/3 TRIGGERED"]

  Hook["Hook attached by Permit.Phoenix
  ─────────────────────────────────────
  1. Get @live_action from socket
  2. Check if in except/0 (skip?)
  3. Determine singular vs plural"]

  ActionAuth["ACTION AUTHORIZATION
  ─────────────────────────────────────
  Fails if there is no permission to @live_action
  altogether: user |> can(:read, Article, ...)"]

  Singular["SINGULAR ACTION
  (:show, :edit, etc)
  ──────────────────────
  Load single record via id_param_name
  (id by default)"]

  Plural["PLURAL ACTION
  (:index, etc.)
  ──────────────────────
  Load all records filtered
  by user's permissions"]

  Start --> Hook
  Hook --> ActionAuth
  ActionAuth --> Singular
  ActionAuth --> Plural
  Singular --> Note1
  Plural --> Note1
  Note1[Record loading done using Permit.Ecto automatically generates query based on<br/>permissions and params. Alternatively, loader/1 callback loads records if Permit.Ecto not used.]
```

Now that the record (or list of records) is loaded, authorization is finally verified against resolved
permissions. If it succeeds, a single record is assigned to `:loaded_resource`, or a list is assigned
or streamed to `:loaded_resources`, and execution continues in the `handle_params/3` callback
implementation.
Otherwise, depending on what kind of error happened, `c:handle_unauthorized/2` or `c:handle_not_found/1`
is called. These callbacks may either halt (default), or continue so that we can still go back to
`handle_params/3`, which is possible but discouraged.

```mermaid
flowchart TD
  AuthCheck["AUTHORIZATION CHECK"]

  Authorized["AUTHORIZED
  ──────────────────────
  {:cont, socket}
  with resource(s) assigned or streamed"]

  Unauthorized["UNAUTHORIZED
  ──────────────────────
  handle_unauthorized/2

  Default: flash + stay on page
  if possible, or redirect to
  fallback path"]

  NotFound["NOT FOUND
  ──────────────────────
  handle_not_found/1

  Default: raise error"]

  UserImplementation["YOUR handle_params/3 IMPLEMENTATION
  (receives socket with resources)"]

  AuthCheck --> Authorized
  AuthCheck --> Unauthorized
  AuthCheck --> NotFound
  Authorized --> UserImplementation
  Unauthorized --> UserImplementation
  NotFound --> UserImplementation

  linkStyle 4,5 stroke-dasharray: 5 5
```

Example of usage with a singular action:

    @impl true
    def handle_params(_params, _uri, socket) do
      # Article is loaded and authorized by Permit
      article = socket.assigns.loaded_resource

      {:noreply, socket |> assign(:title, article.title)}
    end

If an action is defined as plural in the actions module, resources are either assigned to `:loaded_resources`
(by default), or streamed as `:loaded_resources` if `use_stream?/1` is `true`.

    # Default: assign to `:loaded_resources`
    @impl true
    def handle_params(_params, _uri, socket) do
      # Article list is loaded to @loaded_resources
      {:noreply, socket}
    end

    # Optional: set `use_stream?/1` to `true` to use streams instead of assigns
    @impl true
    def use_stream?(_socket), do: true

    @impl true
    def handle_params(_params, _uri, socket) do
      # Article list available in @streams.loaded_resources
      {:noreply, socket}
    end

## Event authorization

Actions such as updating or deleting a resource are typically implemented in LiveView using `handle_event/3`.
Permit taps into `handle_event/3` processing and, depending on the event's nature:
* For events carrying an `"id"` param (e.g. record deletion from an index page), **loads the record** with
  Permit.Ecto (or a loader function) based on the ID param and a query based on the currently resolved
  permissions and puts it in `assigns`.
* For events that do not carry an `"id"` param (e.g. updating a record with form data), **reloads the
  record** currently assigned to `@loaded_resource`, using either Permit.Ecto (and the record's ID) or
  the existing loader function. This is done by default to ensure permissions are evaluated against the
  latest data. You can disable this behaviour by overriding `reload_on_event?/2` (or by passing the
  `:reload_on_event?` option) if you prefer to reuse the already assigned record.

```mermaid
flowchart TD
  Start["USER TRIGGERS EVENT (e.g. click)"]

  Hook["Hook attached by Permit.Phoenix
  ─────────────────────────────────────
  1. Map event → action via:
     • @permit_action attributes
     • event_mapping/0 callback
     • default_event_mapping/0"]

  WithId["EVENT HAS 'id' PARAM
  (e.g. delete from index page)
  ──────────────────────
  Load record by ID from params"]

  NoId["NO 'id' PARAM
  (e.g. form submit)
  ──────────────────────
  RELOAD existing @loaded_resource
  (if reload_on_event? is true)"]

  AuthCheck["AUTHORIZATION CHECK"]

  Authorized["AUTHORIZED
  ──────────────────────
  Assign to: @loaded_resource"]

  Unauthorized["UNAUTHORIZED
  ──────────────────────
  handle_unauthorized/2

  Default: flash + stay on page
  or fallback_path"]

  NotFound["NOT FOUND
  ──────────────────────
  handle_not_found/1

  Default: raise error"]

  UserImplementation["YOUR handle_event/3 IMPLEMENTATION
  (@loaded_resource available)"]

  Start --> Hook
  Hook --> WithId
  Hook --> NoId
  WithId --> Note1
  NoId --> Note1
  Note1[Record loading done using Permit.Ecto automatically generates query based on<br/>permissions and params. Alternatively, loader/1 callback loads records if Permit.Ecto not used.]
  Note1 --> AuthCheck
  AuthCheck --> Authorized
  AuthCheck --> Unauthorized
  AuthCheck --> NotFound
  Authorized --> UserImplementation
  Unauthorized --> UserImplementation
  NotFound --> UserImplementation

  linkStyle 10,11 stroke-dasharray: 5 5
```

### Usage

Event to action mapping is given using the `@permit_action` module attribute put right before an event
handler.

    @impl true
    @permit_action :update
    def handle_event("save", %{"article" => article_params}, socket) do
      article = socket.assigns.loaded_resource

      case MyApp.update_article(article_params) do
        # ...
      end
    end

In this example, the `"save"` event handler is authorized against the `:update` action on `MyApp.Article`.

Default event mapping (`Permit.Phoenix.LiveView.default_event_mapping/0`) maps most common event
names (strings) to action names (atoms) **except for the `"save"` event. Phoenix generates `"save"`
event handler for both `:create` and `:update` actions, hence it must be explicitly provided in code.

When the `handle_event/3` function is not implemented using pattern matching on the first argument,
the `event_mapping` callback must be used instead.

    @impl true
    # "delete" event maps to :delete Permit action
    def event_mapping, do: %{"delete" => :delete, "remove" => :delete}

    @impl true
    def handle_event(event_name, _params, _socket) when event_name in ["delete", "remove"] do
      # Resource is loaded and authorized by Permit
      article = socket.assigns.loaded_resource

      # Delete the record
      {:ok, _} = MyApp.Blog.delete_article(article)

      # If in an action like :index, stream the deletion to the client.
      # Permit either streams the viewed items or assigns them (see `use_stream?/1` callback)
      {:noreply, stream_delete(socket, :loaded_resources, article)}
    end

If authorization fails, `handle_unauthorized/2` is called. Handling authorization failure is as simple as:

    @impl true
    def handle_unauthorized(:delete, socket) do
      # You actually don't need to implement it, but it's useful for defining custom behaviour.
      {:halt, socket |> put_flash(:error, "You are not authorized to delete this article")}
    end

The full list of options can be found in this module's callback specifications.

## Handling failures

Permit allows customizing the way authorization failures and record-not-found errors are handled,
providing sane defaults for both scenarios.

### Authorization failure

The `c:handle_unauthorized/2` callback is provided to enable authorization failure handling customization.
It is used both in navigation and event authorization. It should return either `{:halt, socket}` or
`{:cont, socket}` depending on desired behaviour.

When navigating to a different LiveView instance, or from outside a LiveView session, the new LiveView
has to be mounted. In this case, a `:halt` and a redirect is required. If it's a navigation within the same
LiveView instance, or when it occurs in event authorization, you can use either `:halt` or `:cont` and remain
on the page, displaying an error or performing any appropriate action.

For convenience, this module provides the `mounting?/1` function, which returns `true` if the
LiveView is in the mounting phase, and `false` otherwise. It can be used in the `handle_unauthorized/2`
callback implementation to determine the appropriate response in a custom way.

    @impl true
    def handle_unauthorized(action, socket) do
      if mounting?(socket) do
        {:halt, push_navigate(socket, to: socket.view.fallback_path())}
      else
        # Do whatever you want with the socket here...
        socket = assign(socket, :unauthorized, true)

        # Use :cont to continue processing the module's handle_params/3 handlers,
        # or :halt to halt the processing.
        {:halt, socket |> put_flash(:error, "You are not authorized to access this page")}
      end
    end

By default, the `c:handle_unauthorized/2` callback is implemented to do one of the following, whichever
is first possible:
- remain on the same page and display a flash error message,
- halt the processing and redirect to the `:fallback_path` (with a flash error), defaulting to `/`.

### Record not found

The `c:handle_not_found/1` callback is provided to enable record-not-found error handling customization.

When using Permit.Ecto to load authorized records, a query is constructed based on defined permissions
 - e.g. if the permission is `view(Article, author_id: user_id, published: true)`, the query will be constructed as
`SELECT * FROM articles WHERE author_id = $1 AND published = TRUE AND id = $2`, containing both record
ID and authorization conditions. If a matching record is found, it's assigned and available to the handler;
otherwise, it can mean either of the two:
- the record with given ID exists, but authorization conditions are not met,
- the record does not exist at all.
To distinguish between these two cases, Permit.Ecto will execute a second query with only the record ID
and whatever is defined in `c:base_query/1` callback. If no matching record is found, it will call the
`c:handle_not_found/1` callback. Otherwise, the `c:handle_unauthorized/2` callback is called.

By default, the `c:handle_not_found/1` callback is implemented to raise a `Permit.Phoenix.RecordNotFoundError`.

See documentation for `c:handle_unauthorized/2` and `c:handle_not_found/1` for more guidance and
explanation of default behaviour.

# `action_grouping`

```elixir
@callback action_grouping() :: map()
```

Used to define action grouping for this live view, overriding the schema from your
actions module (configured via `authorization_module`).

This is the mechanism that allows you to e.g. declare that the `:update` permission
allows both the `:edit` and `:update` actions to be performed.

See `Permit.Phoenix.Actions` for reference on semantics.

# `authorization_module`

```elixir
@callback authorization_module() :: Permit.Types.authorization_module()
```

Configures the controller with the application's authorization configuration.

## Example

    # Recommended: configure using a keyword in `use` - recommended in the main web module
    defmodule MyAppWeb do
      def live_view do
        quote do
          use Permit.Phoenix.LiveView,
            authorization_module: MyApp.Authorization
            # other options...
        end
      end
    end

    # Alternatively, implement directly in the live view module (e.g. to override)
    @impl Permit.Phoenix.LiveView
    def authorization_module, do: MyApp.Authorization

    # Requires defining an authorization configuration module
    defmodule MyApp.Authorization, do:
      use Permit.Ecto,
        permissions_module: MyApp.Permissions,
        repo: MyApp.Repo

# `authorize_with_transaction`
*optional* 

```elixir
@callback authorize_with_transaction(
  Permit.Phoenix.Types.socket(),
  (-&gt; {:ok, term()} | {:error, term()})
) :: {:ok, term()} | {:error, term()}
```

Wraps a record creating callback in a database transaction and verifies
that the created record satisfies the current user's authorization conditions.

This is used for `:create` actions where permissions are defined with
field level conditions (e.g. `create(Article, user_id: user.id)`). The hook
only performs a module level check, so the conditions are not verified against
the actual record.

## Usage

The `:action` defaults to `socket.assigns.live_action` (e.g. `:new`), which
transitively resolves to `:create` through Permit's action grouping.

    @impl true
    @permit_action :create
    def handle_event("save", %{"article" => article_params}, socket) do
      article_params = Map.put(article_params, "user_id", socket.assigns.current_scope.user.id)

      case authorize_with_transaction(socket, fn ->
        Blog.create_article(article_params)
      end) do
        {:ok, article} ->
          {:noreply,
           socket
           |> put_flash(:info, "Article created successfully")
           |> push_navigate(to: ~p"/articles/#{article}")}

        {:error, %Ecto.Changeset{} = changeset} ->
          {:noreply, assign(socket, :form, to_form(changeset))}

        {:error, socket} ->
          {:noreply, socket}
      end
    end

## Options

  * `:action` - override the action name used for the record-level authorization check.
    Defaults to `socket.assigns.live_action`, which is the route's `:live_action` (e.g.
    `:new` or `:edit`). This works correctly thanks to Permit's transitive action
    resolution — `:new` transitively checks `:create` permissions. Override this if
    your action grouping differs from the defaults.
  * `:subject` - override the subject used for authorization. By default, the subject
    is retrieved from `socket.assigns` using `use_scope?/0` (i.e. `current_scope`) or
    `current_user`. Use this option if you have a custom `c:fetch_subject/2` override
    that relies on session data, since session is not available in `handle_event` context.
  * `:on_unauthorized` - custom unauthorized handler function `(action, socket -> socket)`.
    Called instead of `c:handle_unauthorized/2` when the created record fails authorization.

## Return values

  * `{:ok, record}` - the callback returned `{:ok, record}` and authorization passed
  * `{:error, reason}` - the callback returned `{:error, reason}` (no auth check performed)
  * `{:error, socket}` - the callback returned `{:ok, record}` but authorization failed;
    the transaction was rolled back and `socket` has been handled by `c:handle_unauthorized/2`
    (or the `:on_unauthorized` handler if provided)

## Caveats

The callback runs inside `Repo.transaction/1`. Only database operations are
rolled back when authorization fails. Side effects such as sending emails,
publishing PubSub messages, or calling external APIs will **not** be undone.
Keep the callback limited to database operations.

# `authorize_with_transaction`
*optional* 

```elixir
@callback authorize_with_transaction(
  Permit.Phoenix.Types.socket(),
  (-&gt; {:ok, term()} | {:error, term()}),
  keyword()
) :: {:ok, term()} | {:error, term()}
```

# `base_query`
*optional* 

```elixir
@callback base_query(Permit.Types.resolution_context()) :: Ecto.Query.t()
```

Creates the basis for an Ecto query constructed by `Permit.Ecto` based on live view action,
resource module, subject (taken from `current_scope.user` unless configured otherwise)
and route parameters.

It's recommended to call `super(arg)` in your implementation to ensure proper
base query handling for both singular actions (like :show, which need ID filtering)
and plural actions (like :index, which may handle delete events).

## Example

    defmodule MyApp.CommentLive.Show do
      use MyAppWeb, :live_view

      # A route like `/articles/:article_id/comments/:id`
      @impl true
      def base_query(%{action: :show, params: %{"article_id" => article_id}} = context) do
        # Original base query is automatically constructed by Permit.Ecto
        # based on `:id`. We need to filter by `article_id` as well because of the route.

        super(context)
        |> MyApp.CommentQueries.by_article_id(article_id)
      end
    end

# `event_mapping`

```elixir
@callback event_mapping() :: map()
```

Provides a mapping of event names (e.g. `"save"`) to Permit actions (e.g. `:create` or `:update`).

**It is recommended to use `@permit_action` module attribute instead of this callback.** The purpose
of this callback remaining is that, when the event handler is not defined using pattern matching
on the event name, the module attribute cannot infer the event name from the function header -
in which case the callback should be used to provide an unambiguous mapping.

Note that calling this function will return its user-implemented form **merged with additional
mappings defined using `@permit_action` module attribute**, as they are consumed using
`__before_compile__/1`. Because of this, `super` will not work - to augment the default mapping,
you need to call and merge into `Permit.Phoenix.LiveView.default_event_mapping/0`.

## Example

    # Not recommended: event handler doesn't pattern match on the event name
    @impl true
    def handle_event(event_name, params, socket) do
      custom_logic(event_name, params, socket)
    else

    @impl true
    def event_mapping, do: %{
      "save" => :create,
      "update" => :update
    }

    # Recommended: use @permit_action module attribute
    @impl true
    @permit_action :create
    def handle_event("save", params, socket) do
      # ...
    end

    @impl true
    @permit_action :update
    def handle_event("update", params, socket) do
      # ...
    end

# `except`
*optional* 

```elixir
@callback except() :: [Permit.Types.action_group()]
```

Allows opting out of using Permit for given LiveView actions.

Defaults to `[]`, thus by default all actions are guarded with Permit.

## Example

    @impl true
    def except, do: [:index]

# `fallback_path`
*optional* 

```elixir
@callback fallback_path(Permit.Types.action_group(), Permit.Phoenix.Types.socket()) ::
  binary()
```

Sets the fallback path to which the user is redirected on authorization failure unless
it is possible to remain on the same page (i.e. if the LiveView is mounted directly
via the router).

**Ignored** if `handle_unauthorized/2` has a custom implementation.

Defaults to `/`.

## Example

    # Recommended: set a fallback path for all LiveViews
    defmodule MyAppWeb do
      def live_view do
        quote do
          use Permit.Phoenix.LiveView,
            fallback_path: "/unauthorized",
            # other options...
        end
      end
    end

    # Set a fallback path for a specific LiveView
    defmodule MyAppWeb.PageLive do
      use MyAppWeb, :live_view

      @impl true
      def fallback_path(action, socket), do: "/unauthorized"
    end

# `fetch_subject`
*optional* 

```elixir
@callback fetch_subject(Permit.Phoenix.Types.socket(), map()) :: Permit.Types.subject()
```

Allows overriding the subject (current user) retrieval logic.

When implemented, Permit executes this callback at the load-and-authorize stage instead of
using `@current_scope.user` or `@current_user`. The result of this callback is used as the
subject, and the `:use_scope?` option is ignored.

The fetched subject is **not** cached in anyway or assigned to the socket.

## Example

    # Custom current-user logic, e.g. in old versions of phx.gen.auth
    @impl true
    def fetch_subject(_socket, session) do
      # Fetch and return the current user directly
      user_token = session["user_token"]
      user_token && MyApp.Accounts.get_user_by_session_token(user_token)
    end

# `finalize_query`
*optional* 

```elixir
@callback finalize_query(Ecto.Query.t(), Permit.Types.resolution_context()) ::
  Ecto.Query.t()
```

Post-processes an Ecto query constructed by `Permit.Ecto`. Usually, `c:base_query/1` should
be used; the only case when `c:finalize_query/2` should be used is when you need to modify the query
based on conditions derived from the generated query structure.

## Example

    defmodule MyApp.CommentLive.Show do
      use MyAppWeb, :live_view

      @impl true
      def finalize_query(generated_query, %{action: :show, params: %{"article_id" => article_id}} = resolution_context) do
        # Post-process the query and return a new one
      end
    end

# `handle_not_found`
*optional* 

```elixir
@callback handle_not_found(Permit.Phoenix.Types.socket()) ::
  Permit.Phoenix.Types.hook_outcome()
```

Called when a record is not found. When using Permit.Ecto, this callback is called when both of
the following queries return no results:
- the query constructed with: the record ID, `c:base_query/1`, and the authorization conditions,
- a second query that only contains the record ID and `c:base_query/1`.
This is to distinguish between authorization failure and record-not-found scenarios.

When a loader function is defined instead of using Permit.Ecto, authorization conditions are only
checked directly on the loaded record (or all of the loaded records in a list), so this callback is
unambiguously called when the loader function has returned `nil`.

Defaults to raising a `Permit.Phoenix.RecordNotFoundError`.

## Example

    @impl true
    def handle_not_found(socket) do
      {:noreply, socket |> put_flash(:error, "Record not found")}
    end

# `handle_unauthorized`
*optional* 

```elixir
@callback handle_unauthorized(Permit.Types.action_group(), Permit.Phoenix.Types.socket()) ::
  Permit.Phoenix.Types.hook_outcome()
```

Called when authorization fails either in `handle_event` or `handle_params` (both during
mounting and navigation). `{:cont, ...}` or `{:halt, ...}` can be used to either continue
executing the normal handlers or halt.

Defaults to halting, displaying a flash and staying on the same page if possible, either
via not navigating at all or by navigating to `_live_referer` - otherwise (e.g. when entering
a page from outside a LiveView session) redirects to `:fallback_path`, defaulting to `/`.

When using Permit.Ecto with actions that load a single resource (e.g. `:show`), a query is
constructed based on the record ID, `c:base_query/1`, and authorization conditions.
If no matching record is found, a second query (without the authorization conditions) is
executed to distinguish between authorization failure and record not existing in the database.
If the second query returns a result, `c:handle_not_found/1` is called instead; otherwise,
this callback is used.

## Example

    # Default implementation
    @impl true
    def handle_unauthorized(action, socket) do
      # navigate_if_mounting/2 calls push_navigate/2 if Permit.Phoenix.LiveView.mounting?/1 returns true
      {:halt,
       socket
       |> put_flash(:error, socket.view.unauthorized_message(action, socket))
       |> navigate_if_mounting(to: socket.view.fallback_path(action, socket))}
    end

    defp navigate_if_mounting(socket, opts) do
      if mounting?(socket), do: navigate(socket, arg), else: socket
    end

# `id_param_name`
*optional* 

```elixir
@callback id_param_name(Permit.Types.action_group(), Permit.Phoenix.Types.socket()) ::
  binary()
```

Sets the name of the ID param that will be used for preloading a record for authorization.

Defaults to `"id"`. If the route contains a different name of the record ID param, it should be changed accordingly.

## Example

    # Recommended: set a default ID param name for all LiveViews
    defmodule MyAppWeb do
      def live_view do
        quote do
          use Permit.Phoenix.LiveView,
            id_param_name: "uuid",
            # other options...
        end
      end
    end

    # Set for a single LiveView
    @impl true
    def id_param_name(_action, _socket), do: "uuid"

# `id_struct_field_name`
*optional* 

```elixir
@callback id_struct_field_name(Permit.Types.action_group(), Permit.Phoenix.Types.socket()) ::
  atom()
```

Sets the name of the field that contains the resource's ID which should be looked for.

Defaults to `:id`. If the record's ID (usually a primary key) is in a different field, then it should be changed accordingly.

## Example

    # Recommended: set a default ID struct field name for all LiveViews
    defmodule MyAppWeb do
      def live_view do
        quote do
          use Permit.Phoenix.LiveView,
            id_struct_field_name: :uuid
        end
      end
    end

    # Set for a single LiveView
    @impl true
    def id_struct_field_name(_action, _socket), do: :uuid

# `loader`
*optional* 

```elixir
@callback loader(Permit.Types.resolution_context()) :: Permit.Types.object() | nil
```

If `Permit.Ecto` is not used, it allows defining a loader function that loads a record
or a list of records, depending on action type (singular or plural).

In the argument, the resolution context is passed, which contains the action, params, socket, etc.

## Example

    @impl true
    def loader(%{action: :index, params: %{page: page}}),
      do: ItemContext.load_all(page: page)

# `plural_actions`

```elixir
@callback plural_actions() :: [atom()]
```

Defines actions that should be treated as plural, overriding any router based
heuristic that would otherwise classify them as singular. Useful for custom
collection actions like `:list`, `:search` or `:feed` when mounted on routes
that end with a non id parameter.

See `Permit.Phoenix.Actions` for reference.

# `preload_actions`
*optional* 

```elixir
@callback preload_actions() :: [Permit.Types.action_group()]
```

**Deprecated:** Use `c:skip_preload/0` instead.

Declares which actions in the LiveView are to use Permit's automatic preloading and
authorization in addition to defaults: `[:show, :edit, :update, :delete, :index]`.

This callback is deprecated in favor of `c:skip_preload/0` which inverts the logic - instead of
whitelisting actions that preload, you blacklist actions that should skip preloading.

## Example

    # Declare that the `:view` live action should be preloaded and authorized
    @impl true
    def preload_actions, do: super() ++ [:view]

    @impl true
    def handle_params(_params, _uri, %{assigns: %{live_action: :view}} = socket) do
      # authorized record is in `assigns.loaded_resource`
      {:noreply, socket}
    end

# `reload_on_event?`
*optional* 

```elixir
@callback reload_on_event?(Permit.Types.action_group(), Permit.Phoenix.Types.socket()) ::
  boolean()
```

For events that do not carry an `"id"` param (e.g. updating a record with form data), determines whether to reload the record before each event authorization.

Defaults to `true`.

## Example

    @impl true
    def reload_on_event?(_action, _socket) do
      true
    end

# `resource_module`
*optional* 

```elixir
@callback resource_module() :: Permit.Types.resource_module()
```

Sets the resource module (typically an Ecto schema) associated with this live view.

In Phoenix LiveView's default convention, in modules grouped under `ArticleLive` this would
be `Article`. Permit then uses it in the following way:

* Load a singular resource by ID
  - When navigating to a path like `/articles/:id`, it will use Permit.Ecto (or function configured
  as `:loader`) to load the article with the given ID, and check it against authorization conditions -
  then either assign it to `@loaded_resource` possibly falling back to executing `handle_unauthorized/2`
  or `handle_not_found/1`.
  - When executing an event like `"delete"` mapped to a Permit action like `:delete` (see `c:event_mapping/0`),
  carrying the item ID, it will act likewise.
* Reload a singular resource
  - When executing an event like `"save"` mapped to a Permit action like `:update` (see `c:event_mapping/0`),
  which carries form data and not the item ID, it will reload the item currently assigned to `@loaded_resource`
  unless `:reload_on_event?` is explicitly set to `false` (see `c:reload_on_event?/2`), and then act just as
  previously described.
* Load a list of resources
  - When navigating to a path like `/articles`, it will build a query with Permit.Ecto based on authorization
  conditions to filter the articles by the current user's
  permissions (or load them with function configured as `:loader`), and assign them to the `:loaded_resources`
  assign or stream them to the client (depending on the `:use_stream?` option). If subject has no permission
  to the action whatsoever, `handle_unauthorized/2` is called.

## Example

    # Recommended: When the web module includes `use Permit.Phoenix.LiveView`:
    defmodule MyApp.ArticleLive.Show do
      use MyAppWeb, :live_view

      @impl true
      def resource_module, do: MyApp.Article
    end

    # When doing `use Permit.Phoenix.LiveView` in a specific live view:
    defmodule MyApp.ArticleLive.Show do
      use MyAppWeb, :live_view

      use Permit.Phoenix.LiveView,
        authorization_module: MyApp.Authorization,
        resource_module: MyApp.Article

      # ...
    end

# `scope_subject`
*optional* 

```elixir
@callback scope_subject(map()) :: Permit.Phoenix.Types.scope_subject()
```

Maps the current Phoenix scope to the subject, if Phoenix Scopes are used (see the `use_scope?/0` callback). Defaults to `scope.user`.

Defaults to `:user`.

## Example

    # Recommended: set a default scope_subject for all LiveViews
    defmodule MyAppWeb do
      def live_view do
        quote do
          use Permit.Phoenix.LiveView,
            # as an atom
            scope_subject: :user,
            # or as a captured function
            scope_subject: &SomeModule.some_function/1,
            # other options...
          end

          # Can also be given as a callback implementation
          @impl true
          def scope_subject(scope), do: scope.user
        end
      end
    end

    # Set for a single LiveView
    @impl true
    def scope_subject(scope) do
      # Use the entire scope as the subject
      scope

      # Use a specific key in the scope
      scope.user
    end

# `singular_actions`

```elixir
@callback singular_actions() :: [atom()]
```

Used to define which actions are considered singular (operating on a single resource),
overriding the schema from your actions module (configured via `authorization_module`).

For example, a `:view` action taking a record ID from path parameters should be configured
as singular, whereas a `:list` action that doesn't take an ID and fetches a list of records
is plural.

See `Permit.Phoenix.Actions` for reference.

# `skip_preload`
*optional* 

```elixir
@callback skip_preload() :: [Permit.Types.action_group()]
```

Declares which actions in the LiveView should skip automatic record preloading.

By default, all actions preload records automatically. Actions in `skip_preload/0` will
only authorize against the resource module, not specific records. This is useful for
actions like `:create` and `:new` where there's no existing record to load.

Defaults to `[:create, :new]`.

Note that the `:update` action is a special case in default LiveView usage, as it is
typically used to update a record with form data. In this case, the record is not loaded
and authorized, but rather reloaded and re-authorized, to ensure permissions are evaluated
against the latest data.

You can disable this behaviour by overriding `reload_on_event?/2` (or by passing the
`:reload_on_event?` option) if you prefer to reuse the already assigned record.

## Example

    @impl true
    def skip_preload do
      [:create, :new, :bulk_action]
    end

# `unauthorized_message`
*optional* 

```elixir
@callback unauthorized_message(Permit.Phoenix.Types.socket(), map()) :: binary()
```

Sets the flash message to display when authorization fails.

**Ignored** if `handle_unauthorized/2` has a custom implementation.

Defaults to `"You do not have permission to perform this action."`.

## Example

    @impl true
    def unauthorized_message(action, socket), do: "Thou shalt not pass."

# `use_scope?`
*optional* 

```elixir
@callback use_scope?() :: boolean()
```

Determines whether to use Phoenix Scopes for fetching the subject. Set to `false` in Phoenix <1.8.

If `true`, the subject will be fetched from `current_scope.user`. If `false`, the subject will be
fetched from `current_user` assign.

Defaults to `true`.

## Example

    # Recommended: set for all LiveViews
    defmodule MyAppWeb do
      def live_view do
        quote do
          use Permit.Phoenix.LiveView,
            use_scope?: false,
            # other options...
          end
        end
      end
    end

    # Set for a single LiveView
    @impl true
    def use_scope? do
      false
    end

# `use_stream?`
*optional* 

```elixir
@callback use_stream?(Permit.Phoenix.Types.socket()) :: boolean() | keyword()
```

Determines whether to use Phoenix Streams for plural actions (e.g. `:index`), or to assign
the resources to the `:loaded_resources` assign.

Defaults to `false`, which means that the resources will be assigned to `:loaded_resources`.

If it returns `true`, the resources will be streamed with default options (appended).
If it returns a keyword list, the resources will be streamed and the list will be passed
as options to `Phoenix.LiveView.stream/4` (e.g. `[reset: true]`).

## Example

    # Recommended: set a default use_stream? for all LiveViews
    defmodule MyAppWeb do
      def live_view do
        quote do
          use Permit.Phoenix.LiveView,
            use_stream?: [reset: true],
            # other options...
        end
      end
    end

    # Set for a single LiveView
    @impl true
    def use_stream?(%{assigns: %{live_action: :index}}), do: [reset: true]
    def use_stream?(_socket), do: false

# `default_event_mapping`

Default event mapping will not map "save" to any action. It is not unambiguous
whether "save" should be mapped to :create or :update. Since Phoenix generators use "save"
for both create and update actions, it will be up to the developer to clarify the mapping.

    @permit_action :create
    def handle_event("save", params, socket) do
      {:noreply, socket}
    end

    @permit_action :update
    def handle_event("save", params, socket) do
      {:noreply, socket}
    end

Default event name to action name mapping is:

    %{
      "create" => :create,
      "delete" => :delete,
      "edit" => :edit,
      "index" => :index,
      "new" => :new,
      "show" => :show,
      "update" => :update
    }

# `mounting?`

```elixir
@spec mounting?(Permit.Phoenix.Types.socket()) :: boolean()
```

Returns true if inside mount/1, false otherwise. Useful for distinguishing between
rendering directly via router or being in a handle_params lifecycle.

For example, a handle_unauthorized/1 implementation must redirect when halting during mounting,
while it needn't redirect when halting during the handle_params lifecycle.

## Example

    @impl true
    def handle_unauthorized(socket) do
      if mounting?(socket) do
        {:halt, push_redirect(socket, to: "/foo")}
      else
        {:halt, assign(socket, :unauthorized, true)}
      end
    end

---

*Consult [api-reference.md](api-reference.md) for complete listing*
