Skip to content

Commit 2c0fe97

Browse files
committed
Add extension_routes/1 similar to extension_messages/1
Also added overridable routes for email confirmation
1 parent 6086b03 commit 2c0fe97

File tree

9 files changed

+208
-16
lines changed

9 files changed

+208
-16
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
## v1.0.9 (TBA)
44

5+
### Changes
6+
7+
* Added `extension_routes/1` to extension controllers and callbacks
8+
* Added `PowEmailConfirmation.Phoenix.Routes.after_halted_registration_path/1` and `PowEmailConfirmation.Phoenix.Routes.after_halted_sign_in_path/1` routes
9+
10+
### Bug fixes
11+
512
* Removed call to `Pow.Ecto.Context.repo/1`
613
* Fixed bug with exception raised in `Pow.Ecto.Schema.normalize_user_id_field_value/1` when calling `Pow.Ecto.Context.get_by/2` with a non binary user id
714
* Fixed bug with exception raised in `Pow.Ecto.Schema.normalize_user_id_field_value/1` when calling `Pow.Ecto.Context.authenticate/2` with a non binary user id

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,13 +461,24 @@ You can customize callback routes by creating the following module:
461461
```elixir
462462
defmodule MyAppWeb.Pow.Routes do
463463
use Pow.Phoenix.Routes
464+
use Pow.Extension.Phoenix.Routes,
465+
extensions: [PowEmailConfirmation]
466+
464467
alias MyAppWeb.Router.Helpers, as: Routes
465468

466469
def after_sign_in_path(conn), do: Routes.some_path(conn, :index)
470+
471+
# Routes methods for extensions has to be prepended with the snake cased
472+
# extension name. So the `after_halted_registration_path/1` method from
473+
# `PowEmailConfirmation` is written as
474+
# `pow_email_confirmation_after_halted_registration_path/1` in your messages
475+
# module.
476+
def pow_email_confirmation_after_halted_registration_path(conn),
477+
do: Routes.some_path(conn, :index)
467478
end
468479
```
469480

470-
Add `routes_backend: MyAppWeb.Pow.Routes` to your configuration. You can find all the routes in [`Pow.Phoenix.Routes`](lib/pow/phoenix/routes.ex).
481+
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`.
471482

472483
### Password hashing function
473484

lib/extensions/email_confirmation/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ Add the following section to your `WEB_PATH/templates/pow/registration/edit.html
2222
<% end %>
2323
```
2424

25+
### Routes
26+
27+
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.
28+
2529
## Prevent persistent session sign in
2630

2731
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.

lib/extensions/email_confirmation/phoenix/controllers/controller_callbacks.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ defmodule PowEmailConfirmation.Phoenix.ControllerCallbacks do
5858
end
5959
defp halt_unconfirmed(_user, _conn, success_response, _type), do: success_response
6060

61-
defp return_path(conn, :registration), do: routes(conn).after_registration_path(conn)
62-
defp return_path(conn, :session), do: routes(conn).after_sign_in_path(conn)
61+
defp return_path(conn, :registration), do: extension_routes(conn).after_halted_registration_path(conn)
62+
defp return_path(conn, :session), do: extension_routes(conn).after_halted_sign_in_path(conn)
6363

6464
@spec send_confirmation_email(map(), Conn.t()) :: any()
6565
def send_confirmation_email(user, conn) do
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
defmodule PowEmailConfirmation.Phoenix.Routes do
2+
@moduledoc """
3+
Module that handles routes.
4+
"""
5+
6+
alias PowEmailConfirmation.Phoenix.ConfirmationController
7+
8+
@doc """
9+
Path to redirect user to when user signs in, but e-mail hasn't been
10+
confirmed.
11+
12+
By default this is the same as the `after_sign_in_path/1`.
13+
"""
14+
def after_halted_sign_in_path(conn), do: ConfirmationController.routes(conn).after_sign_in_path(conn)
15+
16+
@doc """
17+
Path to redirect user to when user signs up, but e-mail hasn't been
18+
confirmed.
19+
20+
By default this is the same as the `after_registration_path/1`.
21+
"""
22+
def after_halted_registration_path(conn), do: ConfirmationController.routes(conn).after_registration_path(conn)
23+
end

lib/pow/extension/phoenix/controllers/controller/base.ex

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ defmodule Pow.Extension.Phoenix.Controller.Base do
2828

2929
@doc false
3030
def extension_messages(conn), do: unquote(__MODULE__).__messages_module__(conn, @messages_fallback)
31+
32+
@routes_fallback unquote(__MODULE__).__routes_fallback__(__MODULE__)
33+
34+
@doc false
35+
def extension_routes(conn), do: unquote(__MODULE__).__routes_module__(conn, @routes_fallback)
3136
end
3237
end
3338

@@ -40,17 +45,7 @@ defmodule Pow.Extension.Phoenix.Controller.Base do
4045
end
4146

4247
@doc false
43-
def __messages_fallback__(module) do
44-
[_controller | base] =
45-
module
46-
|> Module.split()
47-
|> Enum.reverse()
48-
49-
[Messages]
50-
|> Enum.concat(base)
51-
|> Enum.reverse()
52-
|> Module.concat()
53-
end
48+
def __messages_fallback__(module), do: fallback(module, Messages)
5449

5550
# TODO: Remove config fallback by 1.1.0
5651
def __messages_fallback__(config, module, env) do
@@ -64,4 +59,27 @@ defmodule Pow.Extension.Phoenix.Controller.Base do
6459
module
6560
end
6661
end
62+
63+
@doc false
64+
def __routes_fallback__(module), do: fallback(module, Routes)
65+
66+
@doc false
67+
def __routes_module__(conn, fallback) do
68+
case Controller.routes(conn, fallback) do
69+
^fallback -> fallback
70+
routes -> Module.concat([routes, fallback])
71+
end
72+
end
73+
74+
defp fallback(controller, module) do
75+
[_controller | base] =
76+
controller
77+
|> Module.split()
78+
|> Enum.reverse()
79+
80+
[module]
81+
|> Enum.concat(base)
82+
|> Enum.reverse()
83+
|> Module.concat()
84+
end
6785
end
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
defmodule Pow.Extension.Phoenix.Routes do
2+
@moduledoc """
3+
Module that handles routes for extensions.
4+
5+
## Usage
6+
7+
defmodule MyAppWeb.Pow.Routes do
8+
use Pow.Phoenix.Routes
9+
use Pow.Extension.Phoenix.Routes,
10+
extensions: [PowExtensionOne, PowExtensionTwo]
11+
12+
alias MyAppWeb.Router.Helpers, as: Routes
13+
14+
def pow_extension_one_a_path(conn), do: Routes.some_path(conn, :index)
15+
end
16+
17+
Remember to update configuration with `routes_backend: MyAppWeb.Pow.Routes`.
18+
"""
19+
alias Pow.Extension
20+
21+
@doc false
22+
defmacro __using__(config) do
23+
quote do
24+
unquote(config)
25+
|> unquote(__MODULE__).__routes_modules__()
26+
|> Enum.map(&unquote(__MODULE__).__define_route_methods__/1)
27+
end
28+
end
29+
30+
@doc false
31+
def __routes_modules__(config) do
32+
Extension.Config.discover_modules(config, ["Phoenix", "Routes"])
33+
end
34+
35+
@doc false
36+
defmacro __define_route_methods__(extension) do
37+
quote do
38+
extension = unquote(extension)
39+
methods = extension.__info__(:functions)
40+
41+
for {fallback_method, 1} <- methods do
42+
method_name = unquote(__MODULE__).method_name(extension, fallback_method)
43+
unquote(__MODULE__).__define_route_method__(extension, method_name, fallback_method)
44+
end
45+
46+
unquote(__MODULE__).__define_fallback_module__(extension, methods)
47+
end
48+
end
49+
50+
@doc false
51+
defmacro __define_route_method__(extension, method_name, fallback_method) do
52+
quote bind_quoted: [extension: extension, method_name: method_name, fallback_method: fallback_method] do
53+
@spec unquote(method_name)(Conn.t()) :: binary()
54+
def unquote(method_name)(conn) do
55+
unquote(extension).unquote(fallback_method)(conn)
56+
end
57+
58+
defoverridable [{method_name, 1}]
59+
end
60+
end
61+
62+
@doc false
63+
defmacro __define_fallback_module__(extension, methods) do
64+
quote do
65+
name = Module.concat([__MODULE__, unquote(extension)])
66+
quoted = for {method, 1} <- unquote(methods) do
67+
method_name = unquote(__MODULE__).method_name(unquote(extension), method)
68+
69+
quote do
70+
@spec unquote(method)(Conn.t()) :: binary()
71+
def unquote(method)(conn) do
72+
unquote(__MODULE__).unquote(method_name)(conn)
73+
end
74+
end
75+
end
76+
77+
Module.create(name, quoted, Macro.Env.location(__ENV__))
78+
end
79+
end
80+
81+
@doc """
82+
Generates a namespaced method name for a route method.
83+
"""
84+
@spec method_name(atom(), atom()) :: atom()
85+
def method_name(extension, type) do
86+
namespace = namespace(extension)
87+
88+
String.to_atom("#{namespace}_#{type}")
89+
end
90+
91+
defp namespace(extension) do
92+
["Routes", "Phoenix" | base] =
93+
extension
94+
|> Module.split()
95+
|> Enum.reverse()
96+
97+
base
98+
|> Enum.reverse()
99+
|> Enum.join()
100+
|> Macro.underscore()
101+
end
102+
end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
defmodule Pow.Extension.Phoenix.RoutesTest do
2+
defmodule Phoenix.Routes do
3+
def a_path(_conn), do: "/first"
4+
def b_path(_conn), do: "/second"
5+
end
6+
7+
defmodule Routes do
8+
use Pow.Extension.Phoenix.Routes,
9+
extensions: [Pow.Extension.Phoenix.RoutesTest]
10+
11+
def pow_extension_phoenix_routes_test_a_path(_conn), do: "/overridden"
12+
end
13+
14+
use ExUnit.Case
15+
doctest Pow.Extension.Phoenix.Routes
16+
17+
test "can override routes" do
18+
assert Routes.pow_extension_phoenix_routes_test_a_path(nil) == "/overridden"
19+
assert Routes.pow_extension_phoenix_routes_test_b_path(nil) == "/second"
20+
end
21+
22+
test "has fallback module" do
23+
assert Routes.Pow.Extension.Phoenix.RoutesTest.Phoenix.Routes.a_path(nil) == "/overridden"
24+
end
25+
end

test/support/extensions/mocks.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ defmodule Pow.Test.ExtensionMocks do
4949
__phoenix_views__(web_module)
5050
__conn_case__(web_module, cache_backend)
5151
__messages__(web_module, extensions)
52-
__routes__(web_module)
52+
__routes__(web_module, extensions)
5353

5454
quote do
5555
@config unquote(config)
@@ -202,10 +202,12 @@ defmodule Pow.Test.ExtensionMocks do
202202
Module.create(module, quoted, Macro.Env.location(__ENV__))
203203
end
204204

205-
def __routes__(web_module) do
205+
def __routes__(web_module, extensions) do
206206
module = Module.concat([web_module, Phoenix.Routes])
207207
quoted = quote do
208208
use Pow.Phoenix.Routes
209+
use Pow.Extension.Phoenix.Routes,
210+
extensions: unquote(extensions)
209211

210212
def after_sign_in_path(_conn), do: "/after_signed_in"
211213
def after_registration_path(_conn), do: "/after_registration"

0 commit comments

Comments
 (0)