Skip to main content

lib/ash_authentication_phoenix/oauth2_server/consent_view.ex

# SPDX-FileCopyrightText: 2026 ash_authentication_oauth2_server contributors <https://github.com/ash-project/ash_authentication_oauth2_server/graphs/contributors>
#
# SPDX-License-Identifier: MIT

defmodule AshAuthentication.Phoenix.Oauth2Server.ConsentView do
  @moduledoc """
  Default HTML consent screen.

  An app can ship a custom consent screen by configuring its own module
  exporting `render(:consent, assigns)` and passing it to the router as
  `consent_view: MyApp.MyConsentView`.

  ## Assigns

    * `:client_name` — registered client display name
    * `:client_id` — UUID (for display)
    * `:redirect_uri` — where approval will redirect (for display)
    * `:scope` — the space-separated scope string requested (for display)
    * `:resource` — the resource URL the token will bind to (for display)
    * `:action_path` — POST destination for the consent form
    * `:csrf_token` — CSRF token to embed
    * `:consent_request` — sealed token capturing the validated request;
      the POST handler reconstructs all protocol fields from this rather
      than trusting form-submitted values.
  """

  require EEx

  EEx.function_from_string(
    :def,
    :render_consent,
    """
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>Authorize <%= h(@client_name) %></title>
        <style>
          body { font-family: system-ui, -apple-system, sans-serif; max-width: 480px; margin: 4rem auto; padding: 0 1rem; line-height: 1.5; color: #1f2937; }
          h1 { font-size: 1.4rem; margin: 0 0 1.5rem; }
          .meta { background: #f3f4f6; padding: 1rem; border-radius: 0.5rem; margin: 1rem 0; font-size: 0.9rem; }
          .meta dt { font-weight: 600; margin-top: 0.5rem; }
          .meta dt:first-child { margin-top: 0; }
          .meta dd { margin: 0; word-break: break-all; font-family: ui-monospace, monospace; font-size: 0.85rem; }
          .actions { display: flex; gap: 0.5rem; margin-top: 1.5rem; }
          button { flex: 1; padding: 0.75rem 1rem; font-size: 1rem; border: 1px solid #d1d5db; border-radius: 0.375rem; cursor: pointer; }
          button.primary { background: #2563eb; color: white; border-color: #2563eb; }
          button.secondary { background: white; color: #1f2937; }
        </style>
      </head>
      <body>
        <h1>Authorize <strong><%= h(@client_name) %></strong>?</h1>
        <p>This application is requesting access to your account at this server.</p>
        <dl class="meta">
          <dt>Scope</dt>
          <dd><%= h(@scope) %></dd>
          <dt>Resource</dt>
          <dd><%= h(@resource) %></dd>
          <dt>Redirects to</dt>
          <dd><%= h(@redirect_uri) %></dd>
        </dl>
        <form method="POST" action="<%= h(@action_path) %>">
          <input type="hidden" name="_csrf_token" value="<%= h(@csrf_token) %>" />
          <input type="hidden" name="consent_request" value="<%= h(@consent_request) %>" />
          <div class="actions">
            <button type="submit" name="action" value="approve" class="primary">Approve</button>
            <button type="submit" name="action" value="deny" class="secondary">Deny</button>
          </div>
        </form>
      </body>
    </html>
    """,
    [:assigns]
  )

  @doc """
  Render the consent screen as a binary HTML body.
  """
  @spec render(:consent, map()) :: iodata()
  def render(:consent, assigns), do: render_consent(assigns)

  @doc false
  def h(value), do: Phoenix.HTML.html_escape(to_string(value)) |> Phoenix.HTML.safe_to_string()
end