diff --git a/CHANGELOG.md b/CHANGELOG.md index c36eaee0..6dff16c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v1.0.5 (TBA) + +* Added `extension_messages/1` and `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 + ## v1.0.4 (2019-03-13) * Added `PowInvitation` to the `mix pow.extension.phoenix.gen.templates` and `mix pow.extension.phoenix.mailer.gen.templates` tasks diff --git a/README.md b/README.md index bb76c12a..ec7081a9 100644 --- a/README.md +++ b/README.md @@ -432,13 +432,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.Messages, + 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 48dc1e49..774b1424 100644 --- a/lib/extensions/email_confirmation/README.md +++ b/lib/extensions/email_confirmation/README.md @@ -12,6 +12,8 @@ Follow the instructions for extensions in [README.md](../../../README.md), and s ## Configuration +### Let user know when confirmation is required for changing the e-mail + Add the following section to your `WEB_PATH/templates/pow/registration/edit.html.eex` template (you may need to generate the templates first) after the e-mail field: ```elixir @@ -22,6 +24,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 7d815ffd..76f161ac 100644 --- a/lib/pow/extension/phoenix/controllers/controller/base.ex +++ b/lib/pow/extension/phoenix/controllers/controller/base.ex @@ -10,7 +10,7 @@ defmodule Pow.Extension.Phoenix.Controller.Base do # ... end """ - alias Pow.{Config, Phoenix.Controller, Phoenix.Routes} + alias Pow.{Config, Phoenix.Controller} @doc false defmacro __using__(config) do @@ -29,8 +29,10 @@ 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 routes(conn), do: Controller.routes(conn, Routes) + def extension_routes(conn), do: unquote(__MODULE__).__routes_module__(conn, @routes_fallback) end end @@ -43,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 @@ -67,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 34e52f68..1016ec74 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) @@ -191,10 +191,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"