From 8f49c74079dd15660c8422ceb9b16c85e6b13d44 Mon Sep 17 00:00:00 2001 From: Dan Schultzer Date: Fri, 15 Mar 2019 10:06:48 -0700 Subject: [PATCH] Add `extension_routes/1` similar to `extension_messages/1` Also added overridable routes for email confirmation --- CHANGELOG.md | 2 + README.md | 13 ++- lib/extensions/email_confirmation/README.md | 4 + .../controllers/controller_callbacks.ex | 4 +- .../email_confirmation/phoenix/routes.ex | 23 ++++ .../phoenix/controllers/controller/base.ex | 40 +++++-- lib/pow/extension/phoenix/routes.ex | 102 ++++++++++++++++++ test/pow/extension/phoenix/routes_test.exs | 25 +++++ test/support/extensions/mocks.ex | 6 +- 9 files changed, 203 insertions(+), 16 deletions(-) create mode 100644 lib/extensions/email_confirmation/phoenix/routes.ex create mode 100644 lib/pow/extension/phoenix/routes.ex create mode 100644 test/pow/extension/phoenix/routes_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f94a76f..5b4252dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ * `Pow.Phoenix.Router` will now only add specific routes if there is no matching route already defined * Added `Pow.Plug.get_plug/1` and instead of `:mod`, `:plug` is used in config * `Pow.Ecto.Context.authenticate/2` now returns nil if user id or password is nil +* Added `extension_routes/1` to extension controllers and callbacks +* Added `PowEmailConfirmation.Phoenix.Routes.after_halted_registration_path/1` and `PowEmailConfirmation.Phoenix.Routes.after_halted_sign_in_path/1` routes ### Bug fixes diff --git a/README.md b/README.md index fcbda8c0..e729b329 100644 --- a/README.md +++ b/README.md @@ -461,13 +461,24 @@ You can customize callback routes by creating the following module: ```elixir defmodule MyAppWeb.Pow.Routes do use Pow.Phoenix.Routes + use Pow.Extension.Phoenix.Routes, + extensions: [PowEmailConfirmation] + alias MyAppWeb.Router.Helpers, as: Routes def after_sign_in_path(conn), do: Routes.some_path(conn, :index) + + # Routes methods for extensions has to be prepended with the snake cased + # extension name. So the `after_halted_registration_path/1` method from + # `PowEmailConfirmation` is written as + # `pow_email_confirmation_after_halted_registration_path/1` in your messages + # module. + def pow_email_confirmation_after_halted_registration_path(conn), + do: Routes.some_path(conn, :index) end ``` -Add `routes_backend: MyAppWeb.Pow.Routes` to your configuration. You can find all the routes in [`Pow.Phoenix.Routes`](lib/pow/phoenix/routes.ex). +Add `routes_backend: MyAppWeb.Pow.Routes` to your configuration. You can find all the routes in [`Pow.Phoenix.Routes`](lib/pow/phoenix/routes.ex) and `[Pow Extension].Phoenix.Routes`. ### Password hashing function diff --git a/lib/extensions/email_confirmation/README.md b/lib/extensions/email_confirmation/README.md index 7756aa77..7066d07f 100644 --- a/lib/extensions/email_confirmation/README.md +++ b/lib/extensions/email_confirmation/README.md @@ -22,6 +22,10 @@ Add the following section to your `WEB_PATH/templates/pow/registration/edit.html <% end %> ``` +### Routes + +The `PowEmailConfirmation.Phoenix.Routes.after_halted_registration_path/1` and `PowEmailConfirmation.Phoenix.Routes.after_halted_sign_in_path/1` routes are used when halting unconfirmed e-mails registration and sign in. These can be overridden in your custom `MyAppWeb.Pow.Routes` module. + ## Prevent persistent session sign in To prevent that `PowPeristentSession` creates a new persistent session when the email hasn't been confirmed, `PowEmailConfirmation` should be placed first in the extensions list. It'll halt the connection. diff --git a/lib/extensions/email_confirmation/phoenix/controllers/controller_callbacks.ex b/lib/extensions/email_confirmation/phoenix/controllers/controller_callbacks.ex index 62461e06..989123b9 100644 --- a/lib/extensions/email_confirmation/phoenix/controllers/controller_callbacks.ex +++ b/lib/extensions/email_confirmation/phoenix/controllers/controller_callbacks.ex @@ -58,8 +58,8 @@ defmodule PowEmailConfirmation.Phoenix.ControllerCallbacks do end defp halt_unconfirmed(_user, _conn, success_response, _type), do: success_response - defp return_path(conn, :registration), do: routes(conn).after_registration_path(conn) - defp return_path(conn, :session), do: routes(conn).after_sign_in_path(conn) + defp return_path(conn, :registration), do: extension_routes(conn).after_halted_registration_path(conn) + defp return_path(conn, :session), do: extension_routes(conn).after_halted_sign_in_path(conn) @spec send_confirmation_email(map(), Conn.t()) :: any() def send_confirmation_email(user, conn) do diff --git a/lib/extensions/email_confirmation/phoenix/routes.ex b/lib/extensions/email_confirmation/phoenix/routes.ex new file mode 100644 index 00000000..247e2734 --- /dev/null +++ b/lib/extensions/email_confirmation/phoenix/routes.ex @@ -0,0 +1,23 @@ +defmodule PowEmailConfirmation.Phoenix.Routes do + @moduledoc """ + Module that handles routes. + """ + + alias PowEmailConfirmation.Phoenix.ConfirmationController + + @doc """ + Path to redirect user to when user signs in, but e-mail hasn't been + confirmed. + + By default this is the same as the `after_sign_in_path/1`. + """ + def after_halted_sign_in_path(conn), do: ConfirmationController.routes(conn).after_sign_in_path(conn) + + @doc """ + Path to redirect user to when user signs up, but e-mail hasn't been + confirmed. + + By default this is the same as the `after_registration_path/1`. + """ + def after_halted_registration_path(conn), do: ConfirmationController.routes(conn).after_registration_path(conn) +end diff --git a/lib/pow/extension/phoenix/controllers/controller/base.ex b/lib/pow/extension/phoenix/controllers/controller/base.ex index 3ad61f6c..76f161ac 100644 --- a/lib/pow/extension/phoenix/controllers/controller/base.ex +++ b/lib/pow/extension/phoenix/controllers/controller/base.ex @@ -28,6 +28,11 @@ defmodule Pow.Extension.Phoenix.Controller.Base do @doc false def extension_messages(conn), do: unquote(__MODULE__).__messages_module__(conn, @messages_fallback) + + @routes_fallback unquote(__MODULE__).__routes_fallback__(__MODULE__) + + @doc false + def extension_routes(conn), do: unquote(__MODULE__).__routes_module__(conn, @routes_fallback) end end @@ -40,17 +45,7 @@ defmodule Pow.Extension.Phoenix.Controller.Base do end @doc false - def __messages_fallback__(module) do - [_controller | base] = - module - |> Module.split() - |> Enum.reverse() - - [Messages] - |> Enum.concat(base) - |> Enum.reverse() - |> Module.concat() - end + def __messages_fallback__(module), do: fallback(module, Messages) # TODO: Remove config fallback by 1.1.0 def __messages_fallback__(config, module, env) do @@ -64,4 +59,27 @@ defmodule Pow.Extension.Phoenix.Controller.Base do module end end + + @doc false + def __routes_fallback__(module), do: fallback(module, Routes) + + @doc false + def __routes_module__(conn, fallback) do + case Controller.routes(conn, fallback) do + ^fallback -> fallback + routes -> Module.concat([routes, fallback]) + end + end + + defp fallback(controller, module) do + [_controller | base] = + controller + |> Module.split() + |> Enum.reverse() + + [module] + |> Enum.concat(base) + |> Enum.reverse() + |> Module.concat() + end end diff --git a/lib/pow/extension/phoenix/routes.ex b/lib/pow/extension/phoenix/routes.ex new file mode 100644 index 00000000..92ee8078 --- /dev/null +++ b/lib/pow/extension/phoenix/routes.ex @@ -0,0 +1,102 @@ +defmodule Pow.Extension.Phoenix.Routes do + @moduledoc """ + Module that handles routes for extensions. + + ## Usage + + defmodule MyAppWeb.Pow.Routes do + use Pow.Phoenix.Routes + use Pow.Extension.Phoenix.Routes, + extensions: [PowExtensionOne, PowExtensionTwo] + + alias MyAppWeb.Router.Helpers, as: Routes + + def pow_extension_one_a_path(conn), do: Routes.some_path(conn, :index) + end + + Remember to update configuration with `routes_backend: MyAppWeb.Pow.Routes`. + """ + alias Pow.Extension + + @doc false + defmacro __using__(config) do + quote do + unquote(config) + |> unquote(__MODULE__).__routes_modules__() + |> Enum.map(&unquote(__MODULE__).__define_route_methods__/1) + end + end + + @doc false + def __routes_modules__(config) do + Extension.Config.discover_modules(config, ["Phoenix", "Routes"]) + end + + @doc false + defmacro __define_route_methods__(extension) do + quote do + extension = unquote(extension) + methods = extension.__info__(:functions) + + for {fallback_method, 1} <- methods do + method_name = unquote(__MODULE__).method_name(extension, fallback_method) + unquote(__MODULE__).__define_route_method__(extension, method_name, fallback_method) + end + + unquote(__MODULE__).__define_fallback_module__(extension, methods) + end + end + + @doc false + defmacro __define_route_method__(extension, method_name, fallback_method) do + quote bind_quoted: [extension: extension, method_name: method_name, fallback_method: fallback_method] do + @spec unquote(method_name)(Conn.t()) :: binary() + def unquote(method_name)(conn) do + unquote(extension).unquote(fallback_method)(conn) + end + + defoverridable [{method_name, 1}] + end + end + + @doc false + defmacro __define_fallback_module__(extension, methods) do + quote do + name = Module.concat([__MODULE__, unquote(extension)]) + quoted = for {method, 1} <- unquote(methods) do + method_name = unquote(__MODULE__).method_name(unquote(extension), method) + + quote do + @spec unquote(method)(Conn.t()) :: binary() + def unquote(method)(conn) do + unquote(__MODULE__).unquote(method_name)(conn) + end + end + end + + Module.create(name, quoted, Macro.Env.location(__ENV__)) + end + end + + @doc """ + Generates a namespaced method name for a route method. + """ + @spec method_name(atom(), atom()) :: atom() + def method_name(extension, type) do + namespace = namespace(extension) + + String.to_atom("#{namespace}_#{type}") + end + + defp namespace(extension) do + ["Routes", "Phoenix" | base] = + extension + |> Module.split() + |> Enum.reverse() + + base + |> Enum.reverse() + |> Enum.join() + |> Macro.underscore() + end +end diff --git a/test/pow/extension/phoenix/routes_test.exs b/test/pow/extension/phoenix/routes_test.exs new file mode 100644 index 00000000..fb7743bc --- /dev/null +++ b/test/pow/extension/phoenix/routes_test.exs @@ -0,0 +1,25 @@ +defmodule Pow.Extension.Phoenix.RoutesTest do + defmodule Phoenix.Routes do + def a_path(_conn), do: "/first" + def b_path(_conn), do: "/second" + end + + defmodule Routes do + use Pow.Extension.Phoenix.Routes, + extensions: [Pow.Extension.Phoenix.RoutesTest] + + def pow_extension_phoenix_routes_test_a_path(_conn), do: "/overridden" + end + + use ExUnit.Case + doctest Pow.Extension.Phoenix.Routes + + test "can override routes" do + assert Routes.pow_extension_phoenix_routes_test_a_path(nil) == "/overridden" + assert Routes.pow_extension_phoenix_routes_test_b_path(nil) == "/second" + end + + test "has fallback module" do + assert Routes.Pow.Extension.Phoenix.RoutesTest.Phoenix.Routes.a_path(nil) == "/overridden" + end +end diff --git a/test/support/extensions/mocks.ex b/test/support/extensions/mocks.ex index ee3e1a58..c6d508ae 100644 --- a/test/support/extensions/mocks.ex +++ b/test/support/extensions/mocks.ex @@ -49,7 +49,7 @@ defmodule Pow.Test.ExtensionMocks do __phoenix_views__(web_module) __conn_case__(web_module, cache_backend) __messages__(web_module, extensions) - __routes__(web_module) + __routes__(web_module, extensions) quote do @config unquote(config) @@ -202,10 +202,12 @@ defmodule Pow.Test.ExtensionMocks do Module.create(module, quoted, Macro.Env.location(__ENV__)) end - def __routes__(web_module) do + def __routes__(web_module, extensions) do module = Module.concat([web_module, Phoenix.Routes]) quoted = quote do use Pow.Phoenix.Routes + use Pow.Extension.Phoenix.Routes, + extensions: unquote(extensions) def after_sign_in_path(_conn), do: "/after_signed_in" def after_registration_path(_conn), do: "/after_registration"