diff --git a/config/test.exs b/config/test.exs index 6d462e5..05cbdb5 100644 --- a/config/test.exs +++ b/config/test.exs @@ -6,10 +6,7 @@ config :ash, :disable_async?, true config :ash_authentication_phoenix, ash_domains: [Example.Accounts] -config :ash_authentication_phoenix, AshAuthentication.JsonWebToken, - signing_secret: "All I wanna do is to thank you, even though I don't know who you are." - -config :ash_authentication, AshAuthentication.Jwt, +config :ash_authentication_phoenix, signing_secret: "Marty McFly in the past with the Delorean" config :phoenix, :json_library, Jason diff --git a/lib/ash_authentication_phoenix/router.ex b/lib/ash_authentication_phoenix/router.ex index 4fffa3d..318ee9b 100644 --- a/lib/ash_authentication_phoenix/router.ex +++ b/lib/ash_authentication_phoenix/router.ex @@ -181,17 +181,20 @@ defmodule AshAuthentication.Phoenix.Router do Available options are: - * `path` the path under which to mount the sign-in live-view. Defaults to `"/sign-in"`. + * `path` the path under which to mount the sign-in live-view. Defaults to `"/sign-in"` within the current router scope. * `auth_routes_prefix` if set, this will be used instead of route helpers when determining routes. Allows disabling `helpers: true`. + If a tuple {:unscoped, path} is provided, the path prefix will not inherit the current route scope. * `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. + If a tuple {:unscoped, path} is provided, the registration path will not inherit the current route scope. * `reset_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. + If a tuple {:unscoped, path} is provided, the reset path will not inherit the current route scope. * `live_view` the name of the live view to render. Defaults to `AshAuthentication.Phoenix.SignInLive`. * `auth_routes_prefix` the prefix to use for the auth routes. Defaults to `"/auth"`. - * `as` which is passed to the generated `live` route. Defaults to `:auth`. + * `as` which is used to prefix the generated `live_session` and `live` route name. Defaults to `:auth`. * `otp_app` the otp app or apps to find authentication resources in. Pulls from the socket by default. * `overrides` specify any override modules for customisation. See `AshAuthentication.Phoenix.Overrides` for more information. @@ -240,6 +243,8 @@ defmodule AshAuthentication.Phoenix.Router do mod -> mod end) + sign_in_path = Phoenix.Router.scoped_path(__MODULE__, unquote(path)) + register_path = case unquote(register_path) do nil -> nil @@ -254,8 +259,12 @@ defmodule AshAuthentication.Phoenix.Router do value -> Phoenix.Router.scoped_path(__MODULE__, value) end - unquote(register_path) && - Phoenix.Router.scoped_path(__MODULE__, unquote(register_path)) + auth_routes_prefix = + case unquote(auth_routes_prefix) do + nil -> nil + {:unscoped, value} -> value + value -> Phoenix.Router.scoped_path(__MODULE__, value) + end live_session_opts = [ session: @@ -263,15 +272,11 @@ defmodule AshAuthentication.Phoenix.Router do [ %{ "overrides" => unquote(overrides), - "auth_routes_prefix" => unquote(auth_routes_prefix), + "auth_routes_prefix" => auth_routes_prefix, "otp_app" => unquote(otp_app), - "path" => Phoenix.Router.scoped_path(__MODULE__, unquote(path)), - "reset_path" => - unquote(reset_path) && - Phoenix.Router.scoped_path(__MODULE__, unquote(reset_path)), - "register_path" => - unquote(register_path) && - Phoenix.Router.scoped_path(__MODULE__, unquote(register_path)) + "path" => sign_in_path, + "reset_path" => reset_path, + "register_path" => register_path } ]}, on_mount: on_mount @@ -286,17 +291,15 @@ defmodule AshAuthentication.Phoenix.Router do Keyword.put(live_session_opts, :layout, layout) end - live_session :sign_in, live_session_opts do + live_session :"#{unquote(as)}_sign_in", live_session_opts do 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") + if reset_path do + live(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" - ) + if register_path do + live(register_path, unquote(live_view), :register, as: :"#{unquote(as)}_register") end end end @@ -337,7 +340,8 @@ defmodule AshAuthentication.Phoenix.Router do `AshAuthentication.Phoenix.Overrides` for more information. all other options are passed to the generated `scope`. - This is completely optional. + This is completely optional, in particular, if the `reset_path` option is passed to the + `sign_in_route` helper, using the `reset_route` helper is redundant. """ @spec reset_route( opts :: [ @@ -394,7 +398,7 @@ defmodule AshAuthentication.Phoenix.Router do Keyword.put(live_session_opts, :layout, layout) end - live_session :reset, live_session_opts do + live_session :"#{unquote(as)}_reset", live_session_opts do live("/:token", unquote(live_view), :reset, as: unquote(as)) end end diff --git a/lib/ash_authentication_phoenix/strategy_router.ex b/lib/ash_authentication_phoenix/strategy_router.ex index 3af0676..207d7e6 100644 --- a/lib/ash_authentication_phoenix/strategy_router.ex +++ b/lib/ash_authentication_phoenix/strategy_router.ex @@ -7,6 +7,9 @@ defmodule AshAuthentication.Phoenix.StrategyRouter do @impl true def call(conn, opts) do + # ensure query params have been fetched + conn = Plug.Conn.fetch_query_params(conn) + opts |> routes() |> Enum.reduce_while( diff --git a/test/controller_test.exs b/test/controller_test.exs new file mode 100644 index 0000000..f096259 --- /dev/null +++ b/test/controller_test.exs @@ -0,0 +1,113 @@ +defmodule AshAuthentication.Phoenix.ControllerTest do + @moduledoc false + use AshAuthentication.Phoenix.Test.ConnCase + + describe "AshAuthentication Controller" do + test "sign-in renders", %{conn: conn} do + assert_mount_render(conn, ~p"/sign-in", "Sign in") + end + + test "register renders", %{conn: conn} do + assert_mount_render(conn, ~p"/register", "Register") + end + + test "reset renders", %{conn: conn} do + assert_mount_render(conn, ~p"/reset", "Request magic link") + end + + test "sign-out renders", %{conn: conn} do + conn = get(conn, ~p"/sign-out") + assert html_response(conn, 200) =~ "Signed out" + assert {:error, :nosession} = live(conn) + end + + defp assert_mount_render(conn, path, response) do + conn = get(conn, path) + + # assert unconnected mount renders + assert html_response(conn, 200) =~ response + + # assert connected mount also renders + assert {:ok, _view, html} = live(conn) + assert html =~ response + conn + end + + test "register user with password", %{conn: conn} do + strategy = AshAuthentication.Info.strategy!(Example.Accounts.User, :password) + email = "register@email" + password = "register.secret" + + {:ok, lv, _html} = live(conn, ~p"/register") + + {:ok, conn} = + lv + |> form(~s{[action="/auth/user/password/register?"]}, + user: %{ + strategy.identity_field => email, + strategy.password_field => password, + strategy.password_confirmation_field => password + } + ) + |> render_submit() + |> follow_redirect(conn) + + assert html_response(conn, 200) =~ "Success" + assert Ash.CiString.value(conn.assigns.current_user.email) == email + end + + test "sign-in user with password", %{conn: conn} do + strategy = AshAuthentication.Info.strategy!(Example.Accounts.User, :password) + email = "sign.in@email" + password = "sign.in.secret" + create_user!(strategy, email, password) + conn = sign_in_user(conn, strategy, email, password) + + assert html_response(conn, 200) =~ "Success" + assert get_session(conn, :user) != nil + assert Ash.CiString.value(conn.assigns.current_user.email) == email + end + + test "sign-out user", %{conn: conn} do + strategy = AshAuthentication.Info.strategy!(Example.Accounts.User, :password) + email = "sign.out@email" + password = "sign.out.secret" + create_user!(strategy, email, password) + + conn = + conn + |> sign_in_user(strategy, email, password) + |> get(~p"/sign-out") + + assert html_response(conn, 200) =~ "Signed out" + assert get_session(conn, :user) == nil + end + end + + defp sign_in_user(conn, strategy, email, password) do + {:ok, lv, _html} = live(conn, ~p"/sign-in") + + {:ok, conn} = + lv + |> form(~s{[action="/auth/user/password/sign_in?"]}, + user: %{ + strategy.identity_field => email, + strategy.password_field => password + } + ) + |> render_submit() + |> follow_redirect(conn) + + conn + end + + defp create_user!(strategy, email, password) do + Example.Accounts.User + |> Ash.Changeset.for_create(:register_with_password, %{ + strategy.identity_field => email, + strategy.password_field => password, + strategy.password_confirmation_field => password + }) + |> Ash.create!() + end +end diff --git a/test/router_test.exs b/test/router_test.exs index eeb073e..906dce6 100644 --- a/test/router_test.exs +++ b/test/router_test.exs @@ -23,4 +23,48 @@ defmodule AshAuthentication.Phoenix.RouterTest do } ]} end + + test "sign_in_routes respects the inherited router scope" do + route = + AshAuthentication.Phoenix.Test.Router + |> Phoenix.Router.routes() + |> Enum.find(&(&1.path == "/nested/sign-in")) + + {_, _, _, %{extra: %{session: session}}} = route.metadata.phoenix_live_view + + assert session == + {AshAuthentication.Phoenix.Router, :generate_session, + [ + %{ + "auth_routes_prefix" => "/nested/auth", + "otp_app" => nil, + "overrides" => [AshAuthentication.Phoenix.Overrides.Default], + "path" => "/nested/sign-in", + "register_path" => "/nested/register", + "reset_path" => "/nested/reset" + } + ]} + end + + test "sign_in_routes respects unscoped" do + route = + AshAuthentication.Phoenix.Test.Router + |> Phoenix.Router.routes() + |> Enum.find(&(&1.path == "/unscoped/sign-in")) + + {_, _, _, %{extra: %{session: session}}} = route.metadata.phoenix_live_view + + assert session == + {AshAuthentication.Phoenix.Router, :generate_session, + [ + %{ + "auth_routes_prefix" => "/auth", + "otp_app" => nil, + "overrides" => [AshAuthentication.Phoenix.Overrides.Default], + "path" => "/unscoped/sign-in", + "register_path" => "/register", + "reset_path" => "/reset" + } + ]} + end end diff --git a/test/sign_in_test.exs b/test/sign_in_test.exs index c6b914e..d4b7f44 100644 --- a/test/sign_in_test.exs +++ b/test/sign_in_test.exs @@ -1,5 +1,6 @@ defmodule AshAuthentication.Phoenix.SignInTest do @moduledoc false + use ExUnit.Case, async: false import Phoenix.ConnTest import Phoenix.LiveViewTest diff --git a/test/support/auth_controller.ex b/test/support/auth_controller.ex index 18c138f..4b8dc2f 100644 --- a/test/support/auth_controller.ex +++ b/test/support/auth_controller.ex @@ -11,7 +11,7 @@ defmodule AshAuthentication.Phoenix.Test.AuthController do |> store_in_session(user) |> assign(:current_user, user) |> put_status(200) - |> render("success.html") + |> render(:success) end @doc false @@ -27,6 +27,6 @@ defmodule AshAuthentication.Phoenix.Test.AuthController do def sign_out(conn, _params) do conn |> clear_session() - |> render("sign_out.html") + |> render(:signed_out) end end diff --git a/test/support/auth_view.ex b/test/support/auth_view.ex new file mode 100644 index 0000000..e47c547 --- /dev/null +++ b/test/support/auth_view.ex @@ -0,0 +1,6 @@ +defmodule AshAuthentication.Phoenix.Test.AuthView do + use Phoenix.Component + + def success(assigns), do: ~H"

Success

" + def signed_out(assigns), do: ~H"

Signed out

" +end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex new file mode 100644 index 0000000..aac96a7 --- /dev/null +++ b/test/support/conn_case.ex @@ -0,0 +1,25 @@ +defmodule AshAuthentication.Phoenix.Test.ConnCase do + use ExUnit.CaseTemplate + + using do + quote do + # The default endpoint for testing + @endpoint AshAuthentication.Phoenix.Test.Endpoint + + use Phoenix.VerifiedRoutes, + endpoint: @endpoint, + router: AshAuthentication.Phoenix.Test.Router + #statics: ~w(assets fonts images favicon.ico robots.txt) + + # Import conveniences for testing with connections + import Plug.Conn + import Phoenix.ConnTest + import Phoenix.LiveViewTest + import AshAuthentication.Phoenix.Test.ConnCase + end + end + + setup _tags do + {:ok, conn: Phoenix.ConnTest.build_conn()} + end +end diff --git a/test/support/phoenix.ex b/test/support/phoenix.ex index c6296a7..a51fed8 100644 --- a/test/support/phoenix.ex +++ b/test/support/phoenix.ex @@ -81,6 +81,30 @@ defmodule AshAuthentication.Phoenix.Test.Router do auth_routes AuthController, Example.Accounts.User, path: "/auth" end + scope "/nested", AshAuthentication.Phoenix.Test do + pipe_through :browser + + sign_in_route register_path: "/register", + reset_path: "/reset", + auth_routes_prefix: "/auth", + as: :nested + + sign_out_route AuthController + reset_route as: :nested + end + + scope "/unscoped", AshAuthentication.Phoenix.Test do + pipe_through :browser + + sign_in_route register_path: {:unscoped, "/register"}, + reset_path: {:unscoped, "/reset"}, + auth_routes_prefix: {:unscoped, "/auth"}, + as: :unscoped + + sign_out_route AuthController + reset_route as: :unscoped + end + scope "/", AshAuthentication.Phoenix.Test do pipe_through(:browser)