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

Configures and injects the authorization plug for Phoenix controllers.

## Mechanism overview

Permit.Phoenix.Controller uses an internal plug module that does the following:
1. Get everything needed to authorize the current action:
   - **Action** - from current controller action name, e.g. `:update` or `:index`,
   - **Resource module** - from the controller module configuration (`:resource_module` Plug option), e.g. `MyApp.Article`
   - **Subject** - from `@current_user`, `@current_scope.user` or whatever else is configured.
2. In "many" actions (e.g. `:index`):
   - Check for permission to perform the action on the resource module.
   - If authorized, load the list of records using Ecto (queried by authorization conditions), or a custom laoder function,
    and assign the filtered list to `@loaded_resources`.
3. In "one" actions (e.g. `:update`):
   - Load the resource using Ecto (queried by authorization conditions and the `"id"` param, by default) or a custom
    loader function,
   - Check authorization conditions on the loaded resource. If authorized, assign the resource to `@loaded_resource`.
4. Handle authorization failure:
   - Call `c:handle_unauthorized/2` callback if unauthorized, which defaults to redirecting to `c:fallback_path/2`.
   - Call `c:handle_not_found/1` callback if the resource is not found, which defaults to raising `Permit.Phoenix.RecordNotFoundError`.

## Usage

Basic setup:

```
defmodule MyAppWeb.ArticleController do
  use MyAppWeb, :controller

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

  def index(conn, params) do
    # @loaded_resources is assigned if authorized, containing filtered records
  end

  def show(conn, params) do
    # @loaded_resource is assigned if authorized
  end
end
```

It is recommended to set it up in your app's main web module and then override specific options in individual
controllers.:

```
defmodule MyAppWeb do
  def controller do
    quote do
      use Permit.Phoenix.Controller,
        authorization_module: MyApp.Authorization
    end
  end
end

defmodule MyAppWeb.ArticleController do
  use MyAppWeb, :controller

  # Set the resource module for this controller
  @impl true
  def resource_module, do: MyApp.Article

  # Controller actions...
end
```

Handling authorization failure can be customized at several levels:

### Customize the fallback path and error message

By default, the plug will redirect to the fallback path and display a flash message with the default error message.

```
@impl true
def fallback_path(action, conn) do
  # Default implementation
  "/"
end

@impl true
def unauthorized_message(action, conn) do
  # Default implementation
  "You are not authorized to perform this action"
end
```

### Fully customize error handling behaviour

Optionally, you can fully customize the error handling behaviour by implementing the `c:handle_unauthorized/2`
and `c:handle_not_found/1` callbacks. These are also available as keyword options, but not recommended.

```
use Permit.Phoenix.Controller,
  authorization_module: MyApp.Authorization,
  resource_module: MyApp.Article

@impl true
def handle_unauthorized(action, conn) do
  # Default implementation
  conn
  |> put_flash(:error, "You are not authorized to perform this action")
  |> redirect(to: "/")
  |> halt()
end

@impl true
def handle_not_found(conn) do
  # Default implementation
  raise Permit.Phoenix.RecordNotFoundError, "Expected at least one result but got none"
end
```

### Ecto query generation

Permit.Phoenix uses Permit.Ecto to convert defined permissions into Ecto queries. For example, if there is a permission
to `delete(Article, author_id: user_id, draft: true)`, inside the `delete` controller action
it will generate a `WHERE article.author_id = $1 AND draft = TRUE` query. All operators defined in `Permit.Operators`
are supported - for reference, see [`Permit.Operators` documentation](https://hexdocs.pm/permit/Permit.Operators.html).

In actions routed via a parent resource, you need to customize the Ecto query to filter records by the parent resource ID.
For this purpose, `c:base_query/1` callback is available; you can also use the `finalize_query/2` callback to post-process
the query.

```
defmodule MyAppWeb.CommentController do
  use MyAppWeb, :controller

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

  @impl true
  def base_query(%{action: :index, params: %{"article_id" => article_id}} = context) do
    # Chain the originally constructed query with a custom query to filter by the parent resource ID
    super(context)
    |> MyApp.CommentQueries.by_article_id(article_id)
  end

  def index(conn, params) do
    # @loaded_resources is assigned if authorized, records filtered by both the parent resource ID
    # and the current user's permissions
  end
end
```

## Controller and Permit actions

Controller actions are mapped to permission action names in the following order:
1. via the `c:action_grouping/0` callback,
2. as configured in your app's `Permit.Phoenix.Actions` implementation.

Likewise, Permit determines which actions need to preload a single record (e.g. `:show`)
or a list of records (e.g. `:index`) in the following order:
1. via the `c:singular_actions/0` callback,
2. as configured in your app's `Permit.Phoenix.Actions` implementation.

Default singular actions are `[:show, :edit, :new, :delete, :update]`, any other action
is plural by default. By default, all actions preload records except those in `c:skip_preload/0`
(`:create` and `:new` by default, as there's nothing to preload for these actions).
Implementing `c:skip_preload/0` allows opting out of preloading records for chosen actions,
in which case only the resource name is authorized against.

By default, `Permit.Phoenix.Actions` defines the following convenience shorthands:
- `:index` and `:show` controller actions are authorized with the `:read` permission,
- `:new` and `:create` controller actions are authorized with the `:create` permission,
- `:edit` and `:update` controller actions are authorized with the `:update` permission.
- `:delete` action is defined as standalone.
See `Permit.Phoenix.Actions` documentation for more details on action grouping.

It is recommended to have the actions module read action names from the router, so that
your permissions module has convenience functions for using each action.

```
defmodule MyApp.Actions do
  # Merge the actions from the router into the default grouping schema.
  use Permit.Phoenix.Actions, router: MyApp.Router
end
```

## Options

For reference regarding the options, see callback documentation below.

In `use` keywords, options correspond to callback names and can be defined as:
- literal expressions,
- captured functions that match the corresponding callback signature. Anonymous functions
are not supported because of compiler limitations.

# `action_grouping`

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

Defines the action grouping schema for this controller.
This can be overridden in individual controllers to customize the action mapping.

## Example

    @impl true
    def action_grouping do
      %{
        new: [:create],
        index: [:read],
        show: [:read],
        edit: [:update],
        create: [:create],
        update: [:update],
        delete: [:delete]
      }
    end

# `authorization_module`

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

Configures the controller with the application's authorization configuration.

## Example

    @impl Permit.Phoenix.Controller
    def authorization_module, do: MyApp.Authorization

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

# `authorize_with_transaction`
*optional* 

```elixir
@callback authorize_with_transaction(Permit.Phoenix.Types.conn(), (-&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 plug
only performs a module-level check, so the conditions are not verified against
the actual record.

## Usage

    def create(conn, %{"article" => params}) do
      case authorize_with_transaction(conn, fn ->
        Repo.insert(Article.changeset(%Article{}, params))
      end) do
        {:ok, article} -> redirect(conn, to: ~p"/articles/#{article}")
        {:error, %Ecto.Changeset{} = changeset} -> render(conn, :new, changeset: changeset)
        {:error, conn} -> conn
      end
    end

## Options

  * `:action` - override the action name (defaults to `Phoenix.Controller.action_name(conn)`)
  * `:on_unauthorized` - custom unauthorized handler function `(action, conn -> conn)`.
    Called instead of `c:handle_unauthorized/2` when the created record fails authorization.
    Must halt the conn.

## Return values

  * `{:ok, record}` - the callback returned `{:ok, record}` and authorization passed
  * `{:error, reason}` - the callback returned `{:error, reason}` (no auth check performed)
  * `{:error, conn}` - the callback returned `{:ok, record}` but authorization failed;
    the transaction was rolled back and `conn` is halted via `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.conn(),
  (-&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 controller action,
resource module, subject (taken from `current_scope.user` unless configured otherwise)
and controller params.

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).

Typically useful when using [nested resource routes](https://hexdocs.pm/phoenix/routing.html#nested-resources).
In an action routed like `/users/:user_id/posts/:id`, you can use the `c:base_query/1` callback to
filter records by `user_id`, while filtering by `id` itself will be applied automatically
(the name of the ID parameter can be overridden with the `c:id_param_name/2` callback).

## Example

    defmodule MyApp.CommentController do
      use Permit.Phoenix.Controller,
        authorization_module: MyApp.Authorization
        resource_module: MyApp.Blog.Comment

      @impl true
      def base_query(%{
        action: :index,
        params: %{"article_id" => article_id}
      }) do
        MyApp.CommentQueries.by_article_id(article_id)
      end
    end

# `except`
*optional* 

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

Allows opting out of using Permit for given controller actions.

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

## Example

    @impl true
    def except do
      [:index]
    end

# `fallback_path`
*optional* 

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

If `c:handle_unauthorized/2` is not customized, sets the fallback path to which the user is redirected
on authorization failure.

Defaults to `/`.

## Example

    @impl true
    def fallback_path(action, conn) do
      case action do
        :view -> "/unauthorized"
        _ -> "/"
      end
    end

# `fetch_subject`
*optional* 

```elixir
@callback fetch_subject(Permit.Phoenix.Types.conn()) :: Permit.Types.subject()
```

Retrieves the authorization subject from `conn`. Defaults to `current_scope.user` if `use_scope?/0` is `true`,
otherwise `conn.assigns[:current_user]`.

## Example

    @impl true
    def fetch_subject(%{assigns: assigns}) do
      assigns[:user]
    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.CommentController do
      use Permit.Phoenix.Controller,
        authorization_module: MyApp.Authorization
        resource_module: MyApp.Blog.Comment

      # just for demonstration - please don't do it directly in controllers
      import Ecto.Query

      @impl true
      def finalize_query(query, %{
        action: :index,
      }) do
        query
        |> preload([c], [:user])
      end
    end

# `handle_not_found`
*optional* 

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

Called when a record is not found.

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

## Example

    @impl true
    def handle_not_found(conn) do
      case get_format(conn) do
        "json" ->
          # render a 4xx JSON response

        "html" ->
          # handle HTML response, e.g. redirect
      end
    end

# `handle_unauthorized`
*optional* 

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

Called when authorization on an action or a loaded record is not granted. Must halt `conn` after rendering or redirecting.

## Example

    @impl true
    def handle_unauthorized(action, conn) do
      case get_format(conn) do
        "json" ->
          # render a 4xx JSON response

        "html" ->
          # handle HTML response, e.g. redirect
      end
    end

# `id_param_name`

```elixir
@callback id_param_name(Permit.Types.action_group(), Permit.Phoenix.Types.conn()) ::
  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

    @impl true
    def id_param_name(_action, _conn) do
      "document"
    end

# `id_struct_field_name`

```elixir
@callback id_struct_field_name(Permit.Types.action_group(), Permit.Phoenix.Types.conn()) ::
  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

    @impl true
    def id_struct_field_name(_action, _conn) do
      :uuid
    end

# `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, conn, etc.

## Example

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

    def loader(%{action: :show}, params: %{id: id}),
      do: ItemContext.load(id)

# `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.

## Example

    @impl true
    def plural_actions do
      [:feed, :search]
    end

# `preload_actions`
*optional* 

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

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

Declares which actions in the controller are to use Permit's automatic preloading and authorization.
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

    @impl true
    def preload_actions do
      [:view]
    end

# `resource_module`
*optional* 

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

Declares the controller's resource module. For instance, when Phoenix and Ecto is used, typically for an `ArticleController` the resource will be an `Article` Ecto schema.

This resource module, along with the controller action name, will be used for authorization checks before each action.

If `Permit.Ecto` is used, this setting selects the Ecto schema which will be used for automatic preloading a record for authorization.
If you need to control access to a page which does not clearly map to a database schema, you can reference any other model, like `MyApp.Dashboard` here and then need to set skip_preload/0 so that Permit will not try to preload this non-database module.

## Example

    defmodule MyApp.ArticleController do
      use Permit.Phoenix.Controller

      def authorization_module, do: MyApp.Authorization

      def resource_module, do: MyApp.Article

      # Alternatively, you can do the following:

      use Permit.Phoenix.Controller,
        authorization_module: MyApp.Authorization,
        resource_module: MyApp.Blog.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`.

## Example

    @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()]
```

Defines which actions are considered singular (operating on a single resource).
This can be overridden in individual controllers to customize the singular actions.

## Example

    @impl true
    def singular_actions do
      [:show, :edit, :new, :delete, :update]
    end

# `skip_preload`
*optional* 

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

Declares which actions in the controller 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]`.

## Example

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

# `unauthorized_message`
*optional* 

```elixir
@callback unauthorized_message(Permit.Types.action_group(), Permit.Phoenix.Types.conn()) ::
  binary()
```

If `c:handle_unauthorized/2` is not customized, sets the error message to display when authorization fails.

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

## Example

    @impl true
    def unauthorized_message(action, conn) do
      "You cannot #{action} this article"
    end

# `use_scope?`
*optional* 

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

Determines whether to use Phoenix Scopes for fetching the subject.

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

Defaults to `true`, must be set to `false` in Phoenix <1.8 or when you've migrated your code from
an earlier Phoenix version.

## Example

    @impl true
    def use_scope? do
      false
    end

---

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