defmodule AshAuthentication.Phoenix.Components.Password do use AshAuthentication.Phoenix.Overrides.Overridable, root_class: "CSS class for the root `div` element.", hide_class: "CSS class to apply to hide an element.", show_first: "The form to show on first load. Either `:sign_in` or `:register`. Only relevant if paths aren't set for them in the router.", interstitial_class: "CSS class for the `div` element between the form and the button.", sign_in_toggle_text: "Toggle text to display when the sign in form is not showing (or `nil` to disable).", register_toggle_text: "Toggle text to display when the register form is not showing (or `nil` to disable).", reset_toggle_text: "Toggle text to display when the reset form is not showing (or `nil` to disable).", toggler_class: "CSS class for the toggler `a` element.", slot_class: "CSS class for the `div` surrounding the slot." @moduledoc """ Generates sign in, registration and reset forms for a resource. ## Component hierarchy This is the top-most strategy-specific component, nested below `AshAuthentication.Phoenix.Components.SignIn`. Children: * `AshAuthentication.Phoenix.Components.Password.SignInForm` * `AshAuthentication.Phoenix.Components.Password.RegisterForm` * `AshAuthentication.Phoenix.Components.Password.ResetForm` ## Props * `strategy` - The strategy configuration as per `AshAuthentication.Info.strategy/2`. Required. * `overrides` - A list of override modules. * `gettext_fn` - Optional text translation function. ## Slots * `sign_in_extra` - rendered inside the sign-in form with the form passed as a slot argument. * `register_extra` - rendered inside the registration form with the form passed as a slot argument. * `reset_extra` - rendered inside the reset form with the form passed as a slot argument. * `path` - used as the base for links to other pages. * `reset_path` - the path to use for reset links. * `register_path` - the path to use for register links. ```heex <.live_component module={#{inspect(__MODULE__)}} strategy={AshAuthentication.Info.strategy!(Example.User, :password)} id="user-with-password" socket={@socket} overrides={[AshAuthentication.Phoenix.Overrides.Default]}> <:sign_in_extra :let={form}> <.input field={form[:capcha]} /> <:register_extra :let={form}> <.input field={form[:name]} /> <:reset_extra :let={form}> <.input field={form[:capcha]} /> ``` #{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()} """ use AshAuthentication.Phoenix.Web, :live_component alias AshAuthentication.{Info, Phoenix.Components.Password, Strategy} alias Phoenix.LiveView.{JS, Rendered, Socket} import Slug @type props :: %{ required(:strategy) => AshAuthentication.Strategy.t(), optional(:live_action) => :sign_in | :register, optional(:path) => String.t(), optional(:current_tenant) => String.t(), optional(:context) => map(), optional(:overrides) => [module], optional(:gettext_fn) => {module, atom} } slot :sign_in_extra slot :register_extra slot :reset_extra @doc false @impl true @spec render(props) :: Rendered.t() | no_return def render(assigns) do strategy = assigns.strategy subject_name = assigns.strategy.resource |> Info.authentication_subject_name!() |> to_string() |> slugify() strategy_name = assigns.strategy |> Strategy.name() |> to_string() |> slugify() register_enabled? = strategy.registration_enabled? && override_for(assigns.overrides, :register_toggle_text) reset_enabled? = strategy.resettable && override_for(assigns.overrides, :reset_toggle_text) reset_id = strategy.resettable && generate_id( subject_name, strategy_name, strategy.resettable.request_password_reset_action_name ) assigns = assigns |> assign( :sign_in_id, generate_id(subject_name, strategy_name, strategy.sign_in_action_name) ) |> assign( :register_id, generate_id(subject_name, strategy_name, strategy.register_action_name) ) |> assign(:hide_class, override_for(assigns.overrides, :hide_class)) |> assign(:reset_enabled?, reset_enabled?) |> assign(:register_enabled?, register_enabled?) |> assign(:sign_in_enabled?, !is_nil(override_for(assigns.overrides, :sign_in_toggle_text))) |> assign(:reset_id, reset_id) |> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end) |> assign_new(:gettext_fn, fn -> nil end) |> assign_new(:live_action, fn -> :sign_in end) |> assign_new(:path, fn -> "/" end) |> assign_new(:reset_path, fn -> nil end) |> assign_new(:register_path, fn -> nil end) |> assign_new(:current_tenant, fn -> nil end) |> assign_new(:context, fn -> %{} end) |> assign_new(:auth_routes_prefix, fn -> nil end) show = if assigns[:live_action] == :sign_in && is_nil(assigns[:reset_path]) && is_nil(assigns[:register_path]) do assigns[:show_first] || :sign_in else assigns[:live_action] end assigns = assign(assigns, :show, show) ~H"""
<.live_component :let={form} module={Password.SignInForm} auth_routes_prefix={@auth_routes_prefix} id={@sign_in_id} strategy={@strategy} label={false} overrides={@overrides} current_tenant={@current_tenant} context={@context} gettext_fn={@gettext_fn} > <%= if @sign_in_extra do %>
{render_slot(@sign_in_extra, form)}
<% end %>
<%= if @reset_enabled? do %> <.toggler message={override_for(@overrides, :reset_toggle_text)} show={@reset_id} hide={[@sign_in_id, @register_id]} to={@reset_path} overrides={@overrides} gettext_fn={@gettext_fn} /> <% end %> <%= if @register_enabled? do %> <.toggler message={override_for(@overrides, :register_toggle_text)} show={@register_id} hide={[@sign_in_id, @reset_id]} to={@register_path} overrides={@overrides} gettext_fn={@gettext_fn} /> <% end %>
<%= if @register_enabled? do %>
<.live_component :let={form} module={Password.RegisterForm} auth_routes_prefix={@auth_routes_prefix} id={@register_id} strategy={@strategy} label={false} overrides={@overrides} current_tenant={@current_tenant} context={@context} gettext_fn={@gettext_fn} > <%= if @register_extra do %>
{render_slot(@register_extra, form)}
<% end %>
<%= if @reset_enabled? do %> <.toggler message={override_for(@overrides, :reset_toggle_text)} show={[@reset_id]} hide={[@sign_in_id, @register_id]} to={@reset_path} overrides={@overrides} gettext_fn={@gettext_fn} /> <% end %> <%= if @sign_in_enabled? do %> <.toggler message={override_for(@overrides, :sign_in_toggle_text)} show={@sign_in_id} hide={[@register_id, @reset_id]} to={if @register_path, do: @path} overrides={@overrides} gettext_fn={@gettext_fn} /> <% end %>
<% end %> <%= if @reset_enabled? do %>
<.live_component :let={form} module={Password.ResetForm} auth_routes_prefix={@auth_routes_prefix} id={@reset_id} strategy={@strategy} label={false} overrides={@overrides} current_tenant={@current_tenant} context={@context} gettext_fn={@gettext_fn} > <%= if @reset_extra do %>
{render_slot(@reset_extra, form)}
<% end %>
<%= if @register_enabled? do %> <.toggler to={@register_path} show={@register_id} hide={[@sign_in_id, @reset_id]} message={override_for(@overrides, :register_toggle_text)} overrides={@overrides} gettext_fn={@gettext_fn} /> <% end %> <%= if @sign_in_enabled? do %> <.toggler message={override_for(@overrides, :sign_in_toggle_text)} show={@sign_in_id} hide={[@register_id, @reset_id]} to={if @reset_path, do: @path} overrides={@overrides} gettext_fn={@gettext_fn} /> <% end %>
<% end %>
""" end defp generate_id(subject_name, strategy_name, action) do action = action |> to_string() |> slugify() "#{subject_name}-#{strategy_name}-#{action}" end @doc false @spec toggler(Socket.assigns()) :: Rendered.t() | no_return def toggler(assigns) do if assigns[:to] do ~H""" <.link patch={@to} class={override_for(@overrides, :toggler_class)}> {_gettext(@message)} """ else ~H""" {_gettext(@message)} """ end end defp toggle_js(show, hides, %JS{} = js \\ %JS{}) do show_wrapper = "##{show}-wrapper" js = js |> JS.show(to: show_wrapper) |> JS.focus_first(to: show_wrapper) hides |> Enum.reject(&is_nil/1) |> Enum.reduce(js, fn hide, js -> JS.hide(js, to: "##{hide}-wrapper") end) end end