From 50f7147d2697c5e15a0da5d39804f3a48ac59bcb Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Mon, 25 Sep 2023 10:26:49 -0400 Subject: [PATCH] improvement: optional support for routing to register & reset links (#281) --- ...started-with-ash-authentication-phoenix.md | 4 +- .../components/password.ex | 68 ++++++++++++++----- .../components/password/register_form.ex | 3 +- .../components/sign_in.ex | 25 ++++++- lib/ash_authentication_phoenix/router.ex | 39 +++++++++-- .../sign_in_live.ex | 12 ++++ 6 files changed, 126 insertions(+), 25 deletions(-) diff --git a/documentation/tutorials/getting-started-with-ash-authentication-phoenix.md b/documentation/tutorials/getting-started-with-ash-authentication-phoenix.md index 1bfd653..c2c06a5 100644 --- a/documentation/tutorials/getting-started-with-ash-authentication-phoenix.md +++ b/documentation/tutorials/getting-started-with-ash-authentication-phoenix.md @@ -375,7 +375,9 @@ defmodule ExampleWeb.Router do get "/", PageController, :home # add these lines --> - sign_in_route() + # Leave out `register_path` and `reset_path` if you don't want to support + # user registration and/or password resets respectively. + sign_in_route(register_path: "/register", reset_path: "/reset") sign_out_route AuthController auth_routes_for Example.Accounts.User, to: AuthController reset_route [] diff --git a/lib/ash_authentication_phoenix/components/password.ex b/lib/ash_authentication_phoenix/components/password.ex index b4e2867..bf1da29 100644 --- a/lib/ash_authentication_phoenix/components/password.ex +++ b/lib/ash_authentication_phoenix/components/password.ex @@ -2,7 +2,8 @@ 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`.", + 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).", @@ -32,8 +33,6 @@ defmodule AshAuthentication.Phoenix.Components.Password do * `strategy` - The strategy configuration as per `AshAuthentication.Info.strategy/2`. Required. * `overrides` - A list of override modules. - * `show_first` - either `:sign_in`, `:register` or `:reset` which controls - which form is visible on first load. ## Slots @@ -43,6 +42,9 @@ defmodule AshAuthentication.Phoenix.Components.Password do 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 @@ -78,7 +80,9 @@ defmodule AshAuthentication.Phoenix.Components.Password do @type props :: %{ required(:strategy) => AshAuthentication.Strategy.t(), - optional(:overrides) => [module] + optional(:overrides) => [module], + optional(:live_action) => :sign_in | :register, + optional(:path) => String.t() } slot :sign_in_extra @@ -127,17 +131,30 @@ defmodule AshAuthentication.Phoenix.Components.Password do :register_id, generate_id(subject_name, strategy_name, strategy.register_action_name) ) - |> assign_new(:show_first, fn -> override_for(assigns.overrides, :show_first, :sign_in) end) |> 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(: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) + + 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} @@ -155,18 +172,20 @@ defmodule AshAuthentication.Phoenix.Components.Password do
<%= if @reset_enabled? do %> <.toggler + message={override_for(@overrides, :reset_toggle_text)} show={@reset_id} hide={[@sign_in_id, @register_id]} - message={override_for(@overrides, :reset_toggle_text)} + to={@reset_path} overrides={@overrides} /> <% end %> <%= if @register_enabled? do %> <.toggler + message={override_for(@overrides, :register_toggle_text)} show={@register_id} hide={[@sign_in_id, @reset_id]} - message={override_for(@overrides, :register_toggle_text)} + to={@register_path} overrides={@overrides} /> <% end %> @@ -175,7 +194,10 @@ defmodule AshAuthentication.Phoenix.Components.Password do
<%= if @register_enabled? do %> -
+
<.live_component :let={form} module={Password.RegisterForm} @@ -193,17 +215,19 @@ defmodule AshAuthentication.Phoenix.Components.Password do
<%= if @reset_enabled? do %> <.toggler + message={override_for(@overrides, :reset_toggle_text)} show={@reset_id} hide={[@sign_in_id, @register_id]} - message={override_for(@overrides, :reset_toggle_text)} + to={@reset_path} overrides={@overrides} /> <% end %> <%= if @sign_in_enabled? do %> <.toggler + message={override_for(@overrides, :sign_in_toggle_text)} show={@sign_in_id} hide={[@register_id, @reset_id]} - message={override_for(@overrides, :sign_in_toggle_text)} + to={@path} overrides={@overrides} /> <% end %> @@ -213,7 +237,7 @@ defmodule AshAuthentication.Phoenix.Components.Password do <% end %> <%= if @reset_enabled? do %> -
+
<.live_component :let={form} module={Password.ResetForm} @@ -231,6 +255,7 @@ defmodule AshAuthentication.Phoenix.Components.Password do
<%= if @register_enabled? do %> <.toggler + to={@register_path} show={@register_id} hide={[@sign_in_id, @reset_id]} message={override_for(@overrides, :register_toggle_text)} @@ -239,6 +264,7 @@ defmodule AshAuthentication.Phoenix.Components.Password do <% end %> <%= if @sign_in_enabled? do %> <.toggler + to={@path} show={@sign_in_id} hide={[@register_id, @reset_id]} message={override_for(@overrides, :sign_in_toggle_text)} @@ -265,11 +291,19 @@ defmodule AshAuthentication.Phoenix.Components.Password do @doc false @spec toggler(Socket.assigns()) :: Rendered.t() | no_return def toggler(assigns) do - ~H""" - - <%= @message %> - - """ + if assigns[:to] do + ~H""" + <.link patch={@to} class={override_for(@overrides, :toggler_class)}> + <%= @message %> + + """ + else + ~H""" + + <%= @message %> + + """ + end end defp toggle_js(show, hides, %JS{} = js \\ %JS{}) do diff --git a/lib/ash_authentication_phoenix/components/password/register_form.ex b/lib/ash_authentication_phoenix/components/password/register_form.ex index 241db17..89af2e1 100644 --- a/lib/ash_authentication_phoenix/components/password/register_form.ex +++ b/lib/ash_authentication_phoenix/components/password/register_form.ex @@ -42,7 +42,8 @@ defmodule AshAuthentication.Phoenix.Components.Password.RegisterForm do @type props :: %{ required(:strategy) => AshAuthentication.Strategy.t(), - optional(:overrides) => [module] + optional(:overrides) => [module], + optional(:live_action) => :sign_in | :register } @doc false diff --git a/lib/ash_authentication_phoenix/components/sign_in.ex b/lib/ash_authentication_phoenix/components/sign_in.ex index c2cfc08..f067d7c 100644 --- a/lib/ash_authentication_phoenix/components/sign_in.ex +++ b/lib/ash_authentication_phoenix/components/sign_in.ex @@ -33,6 +33,10 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do * `overrides` - A list of override modules. * `otp_app` - The otp app to look for authenticated resources in + * `live_action` - The live_action being routed to + * `path` - The path to use as the base for links + * `reset_path` - The path to use for reset links + * `register_path` - The path to use for register links """ use Phoenix.LiveComponent @@ -42,7 +46,10 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do import Slug @type props :: %{ - optional(:overrides) => [module] + optional(:overrides) => [module], + optional(:path) => String.t(), + optional(:reset_path) => String.t(), + optional(:register_path) => String.t() } @doc false @@ -68,6 +75,10 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do socket |> assign(:strategies_by_resource, strategies_by_resource) |> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] 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) {:ok, socket} end @@ -87,7 +98,11 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do <%= for strategy <- strategies.form do %> <.strategy component={component_for_strategy(strategy)} + live_action={@live_action} strategy={strategy} + path={@path} + reset_path={@reset_path} + register_path={@register_path} overrides={@overrides} /> <% end %> @@ -105,7 +120,11 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do <%= for strategy <- strategies.link do %> <.strategy component={component_for_strategy(strategy)} + live_action={@live_action} strategy={strategy} + path={@path} + reset_path={@reset_path} + register_path={@register_path} overrides={@overrides} /> <% end %> @@ -122,6 +141,10 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do module={@component} id={strategy_id(@strategy)} strategy={@strategy} + path={@path} + reset_path={@reset_path} + register_path={@register_path} + live_action={@live_action} overrides={@overrides} />
diff --git a/lib/ash_authentication_phoenix/router.ex b/lib/ash_authentication_phoenix/router.ex index 3fdd71b..b102831 100644 --- a/lib/ash_authentication_phoenix/router.ex +++ b/lib/ash_authentication_phoenix/router.ex @@ -90,6 +90,14 @@ defmodule AshAuthentication.Phoenix.Router do subject_name = AshAuthentication.Info.authentication_subject_name!(unquote(resource)) controller = Keyword.fetch!(unquote(opts), :to) path = Keyword.get(unquote(opts), :path, "/auth") + + path = + if String.starts_with?(path, "/") do + path + else + "/" <> path + end + scope_opts = Keyword.get(unquote(opts), :scope_opts, []) strategies = @@ -119,8 +127,11 @@ defmodule AshAuthentication.Phoenix.Router do Available options are: - * `path` the path under which to mount the live-view. Defaults to - `"/sign-in"`. + * `path` the path under which to mount the sign-in live-view. Defaults to `"/sign-in"`. + * `register_path` - the path under which to mount the password strategy's registration live-view. + If not set, and registration is supported, registration will use a dynamic toggle and will not be routeable to. + * `register_path` - the path under which to mount the password strategy's password reset live-view. + If not set, and password reset is supported, password reset will use a dynamic toggle and will not be routeable to. * `live_view` the name of the live view to render. Defaults to `AshAuthentication.Phoenix.SignInLive`. * `as` which is passed to the generated `live` route. Defaults to `:auth`. @@ -147,6 +158,8 @@ defmodule AshAuthentication.Phoenix.Router do {otp_app, opts} = Keyword.pop(opts, :otp_app) {layout, opts} = Keyword.pop(opts, :layout) {on_mount, opts} = Keyword.pop(opts, :on_mount) + {reset_path, opts} = Keyword.pop(opts, :reset_path) + {register_path, opts} = Keyword.pop(opts, :register_path) {overrides, opts} = Keyword.pop(opts, :overrides, [AshAuthentication.Phoenix.Overrides.Default]) @@ -156,11 +169,17 @@ defmodule AshAuthentication.Phoenix.Router do |> Keyword.put_new(:alias, false) quote do - scope unquote(path), unquote(opts) do + scope "/", unquote(opts) do import Phoenix.LiveView.Router, only: [live: 4, live_session: 3] live_session_opts = [ - session: %{"overrides" => unquote(overrides), "otp_app" => unquote(otp_app)}, + session: %{ + "overrides" => unquote(overrides), + "otp_app" => unquote(otp_app), + "path" => unquote(path), + "reset_path" => unquote(reset_path), + "register_path" => unquote(register_path) + }, on_mount: [AshAuthenticationPhoenix.Router.OnLiveViewMount | unquote(on_mount || [])] ] @@ -174,7 +193,17 @@ defmodule AshAuthentication.Phoenix.Router do end live_session :sign_in, live_session_opts do - live("/", unquote(live_view), :sign_in, as: unquote(as)) + live(unquote(path), unquote(live_view), :sign_in, as: unquote(as)) + + if unquote(reset_path) do + live(unquote(reset_path), unquote(live_view), :reset, as: :"#{unquote(as)}_reset") + end + + if unquote(register_path) do + live(unquote(register_path), unquote(live_view), :register, + as: :"#{unquote(as)}_register" + ) + end end end end diff --git a/lib/ash_authentication_phoenix/sign_in_live.ex b/lib/ash_authentication_phoenix/sign_in_live.ex index 83b6b82..035d899 100644 --- a/lib/ash_authentication_phoenix/sign_in_live.ex +++ b/lib/ash_authentication_phoenix/sign_in_live.ex @@ -33,10 +33,18 @@ defmodule AshAuthentication.Phoenix.SignInLive do socket |> assign(overrides: overrides) |> assign_new(:otp_app, fn -> nil end) + |> assign(:path, session["path"] || "/") + |> assign(:reset_path, session["reset_path"]) + |> assign(:register_path, session["register_path"]) {:ok, socket} end + @impl true + def handle_params(_, _uri, socket) do + {:noreply, socket} + end + @doc false @impl true @spec render(Socket.assigns()) :: Rendered.t() @@ -46,6 +54,10 @@ defmodule AshAuthentication.Phoenix.SignInLive do <.live_component module={Components.SignIn} otp_app={@otp_app} + live_action={@live_action} + path={@path} + reset_path={@reset_path} + register_path={@register_path} id={override_for(@overrides, :sign_in_id, "sign-in")} overrides={@overrides} />