Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add extension routes #141

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions lib/extensions/email_confirmation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions lib/extensions/email_confirmation/phoenix/routes.ex
Original file line number Diff line number Diff line change
@@ -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
40 changes: 29 additions & 11 deletions lib/pow/extension/phoenix/controllers/controller/base.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
102 changes: 102 additions & 0 deletions lib/pow/extension/phoenix/routes.ex
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions test/pow/extension/phoenix/routes_test.exs
Original file line number Diff line number Diff line change
@@ -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
6 changes: 4 additions & 2 deletions test/support/extensions/mocks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand Down