feat: Dynamic Router + compile time dependency fixes (#487)

* improvement: create a new dynamic router, and avoid other compile time dependencies

* chore: "fix" credo
This commit is contained in:
Zach Daniel 2024-08-08 20:03:48 -04:00 committed by GitHub
parent 29364dcf4d
commit 9f5feedc7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 936 additions and 85 deletions

217
.credo.exs Normal file
View file

@ -0,0 +1,217 @@
# This file contains the configuration for Credo and you are probably reading
# this after creating it with `mix credo.gen.config`.
#
# If you find anything wrong or unclear in this file, please report an
# issue on GitHub: https://github.com/rrrene/credo/issues
#
%{
#
# You can have as many configs as you like in the `configs:` field.
configs: [
%{
#
# Run any config using `mix credo -C <name>`. If no config name is given
# "default" is used.
#
name: "default",
#
# These are the files included in the analysis:
files: %{
#
# You can give explicit globs or simply directories.
# In the latter case `**/*.{ex,exs}` will be used.
#
included: [
"lib/",
"src/",
"test/",
"web/",
"apps/*/lib/",
"apps/*/src/",
"apps/*/test/",
"apps/*/web/"
],
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
},
#
# Load and configure plugins here:
#
plugins: [],
#
# If you create your own checks, you must specify the source files for
# them here, so they can be loaded by Credo before running the analysis.
#
requires: [],
#
# If you want to enforce a style guide and need a more traditional linting
# experience, you can change `strict` to `true` below:
#
strict: false,
#
# To modify the timeout for parsing files, change this value:
#
parse_timeout: 5000,
#
# If you want to use uncolored output by default, you can change `color`
# to `false` below:
#
color: true,
#
# You can customize the parameters of any check by adding a second element
# to the tuple.
#
# To disable a check put `false` as second element:
#
# {Credo.Check.Design.DuplicatedCode, false}
#
checks: %{
enabled: [
#
## Consistency Checks
#
{Credo.Check.Consistency.ExceptionNames, []},
{Credo.Check.Consistency.LineEndings, []},
{Credo.Check.Consistency.ParameterPatternMatching, []},
{Credo.Check.Consistency.SpaceAroundOperators, []},
{Credo.Check.Consistency.SpaceInParentheses, []},
{Credo.Check.Consistency.TabsOrSpaces, []},
#
## Design Checks
#
# You can customize the priority of any check
# Priority values are: `low, normal, high, higher`
#
{Credo.Check.Design.AliasUsage,
[priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
{Credo.Check.Design.TagFIXME, []},
# You can also customize the exit_status of each check.
# If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero).
#
{Credo.Check.Design.TagTODO, [exit_status: 2]},
#
## Readability Checks
#
{Credo.Check.Readability.AliasOrder, []},
{Credo.Check.Readability.FunctionNames, []},
{Credo.Check.Readability.LargeNumbers, []},
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
{Credo.Check.Readability.ModuleAttributeNames, []},
{Credo.Check.Readability.ModuleDoc, []},
{Credo.Check.Readability.ModuleNames, []},
{Credo.Check.Readability.ParenthesesInCondition, []},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
{Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
{Credo.Check.Readability.PredicateFunctionNames, []},
{Credo.Check.Readability.PreferImplicitTry, []},
{Credo.Check.Readability.RedundantBlankLines, []},
{Credo.Check.Readability.Semicolons, []},
{Credo.Check.Readability.SpaceAfterCommas, []},
{Credo.Check.Readability.StringSigils, []},
{Credo.Check.Readability.TrailingBlankLine, []},
{Credo.Check.Readability.TrailingWhiteSpace, []},
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
{Credo.Check.Readability.VariableNames, []},
{Credo.Check.Readability.WithSingleClause, []},
#
## Refactoring Opportunities
#
{Credo.Check.Refactor.Apply, []},
{Credo.Check.Refactor.CondStatements, []},
{Credo.Check.Refactor.CyclomaticComplexity, [max_complexity: 15]},
{Credo.Check.Refactor.FilterCount, []},
{Credo.Check.Refactor.FilterFilter, []},
{Credo.Check.Refactor.FunctionArity, []},
{Credo.Check.Refactor.LongQuoteBlocks, []},
{Credo.Check.Refactor.MapJoin, []},
{Credo.Check.Refactor.MatchInCondition, []},
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
{Credo.Check.Refactor.Nesting, []},
{Credo.Check.Refactor.RedundantWithClauseResult, []},
{Credo.Check.Refactor.RejectReject, []},
{Credo.Check.Refactor.UnlessWithElse, []},
{Credo.Check.Refactor.WithClauses, []},
#
## Warnings
#
{Credo.Check.Warning.ApplicationConfigInModuleAttribute, []},
{Credo.Check.Warning.BoolOperationOnSameValues, []},
{Credo.Check.Warning.Dbg, []},
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
{Credo.Check.Warning.IExPry, []},
{Credo.Check.Warning.IoInspect, []},
{Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []},
{Credo.Check.Warning.OperationOnSameValues, []},
{Credo.Check.Warning.OperationWithConstantResult, []},
{Credo.Check.Warning.RaiseInsideRescue, []},
{Credo.Check.Warning.SpecWithStruct, []},
{Credo.Check.Warning.UnsafeExec, []},
{Credo.Check.Warning.UnusedEnumOperation, []},
{Credo.Check.Warning.UnusedFileOperation, []},
{Credo.Check.Warning.UnusedKeywordOperation, []},
{Credo.Check.Warning.UnusedListOperation, []},
{Credo.Check.Warning.UnusedPathOperation, []},
{Credo.Check.Warning.UnusedRegexOperation, []},
{Credo.Check.Warning.UnusedStringOperation, []},
{Credo.Check.Warning.UnusedTupleOperation, []},
{Credo.Check.Warning.WrongTestFileExtension, []}
],
disabled: [
#
# Checks scheduled for next check update (opt-in for now)
{Credo.Check.Refactor.UtcNowTruncate, []},
#
# Controversial and experimental checks (opt-in, just move the check to `:enabled`
# and be sure to use `mix credo --strict` to see low priority checks)
#
{Credo.Check.Consistency.MultiAliasImportRequireUse, []},
{Credo.Check.Consistency.UnusedVariableNames, []},
{Credo.Check.Design.DuplicatedCode, []},
{Credo.Check.Design.SkipTestWithoutComment, []},
{Credo.Check.Readability.AliasAs, []},
{Credo.Check.Readability.BlockPipe, []},
{Credo.Check.Readability.ImplTrue, []},
{Credo.Check.Readability.MultiAlias, []},
{Credo.Check.Readability.NestedFunctionCalls, []},
{Credo.Check.Readability.OneArityFunctionInPipe, []},
{Credo.Check.Readability.OnePipePerLine, []},
{Credo.Check.Readability.SeparateAliasRequire, []},
{Credo.Check.Readability.SingleFunctionToBlockPipe, []},
{Credo.Check.Readability.SinglePipe, []},
{Credo.Check.Readability.Specs, []},
{Credo.Check.Readability.StrictModuleLayout, []},
{Credo.Check.Readability.WithCustomTaggedTuple, []},
{Credo.Check.Refactor.ABCSize, [max_complexity: 14]},
{Credo.Check.Refactor.AppendSingleItem, []},
{Credo.Check.Refactor.DoubleBooleanNegation, []},
{Credo.Check.Refactor.FilterReject, []},
{Credo.Check.Refactor.IoPuts, []},
{Credo.Check.Refactor.MapMap, []},
{Credo.Check.Refactor.ModuleDependencies, []},
{Credo.Check.Refactor.NegatedIsNil, []},
{Credo.Check.Refactor.PassAsyncInTestCases, []},
{Credo.Check.Refactor.PipeChainStart, []},
{Credo.Check.Refactor.RejectFilter, []},
{Credo.Check.Refactor.VariableRebinding, []},
{Credo.Check.Warning.LazyLogging, []},
{Credo.Check.Warning.LeakyEnvironment, []},
{Credo.Check.Warning.MapGetUnsafePass, []},
{Credo.Check.Warning.MixEnv, []},
{Credo.Check.Warning.UnsafeToAtom, []}
# {Credo.Check.Refactor.MapInto, []},
#
# Custom checks can be created using `mix credo.gen.check`.
#
]
}
}
]
}

View file

@ -9,6 +9,9 @@ locals_without_parens = [
auth_routes_for: 1, auth_routes_for: 1,
auth_routes_for: 2, auth_routes_for: 2,
auth_routes_for: 3, auth_routes_for: 3,
auth_routes: 1,
auth_routes: 2,
auth_routes: 3,
reset_route: 1, reset_route: 1,
set: 2, set: 2,
ash_authentication_live_session: 1, ash_authentication_live_session: 1,

View file

@ -13,3 +13,11 @@ config :ash_authentication, AshAuthentication.Jwt,
signing_secret: "Marty McFly in the past with the Delorean" signing_secret: "Marty McFly in the past with the Delorean"
config :phoenix, :json_library, Jason config :phoenix, :json_library, Jason
config :ash_authentication_phoenix, AshAuthentication.Phoenix.Test.Endpoint,
server: false,
debug_errors: true,
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64)
config :logger, level: :error

View file

@ -14,11 +14,11 @@ defmodule DevWeb.HomePageLive do
<%= if @current_user do %> <%= if @current_user do %>
<h2>Current user: <%= @current_user.email %></h2> <h2>Current user: <%= @current_user.email %></h2>
<.link navigate={Routes.auth_path(@socket, :sign_out)}>Sign out</.link> <.link navigate="/sign-out">Sign out</.link>
<% else %> <% else %>
<h2>Please sign in</h2> <h2>Please sign in</h2>
<.link navigate={Routes.auth_path(@socket, :sign_in)}>Standard sign in</.link> <.link navigate="/auth/sign-in">Standard sign in</.link>
<br /> <br />
<.link navigate={Routes.live_path(@socket, DevWeb.CustomSignInLive)}>Custom sign in</.link> <.link navigate={Routes.live_path(@socket, DevWeb.CustomSignInLive)}>Custom sign in</.link>
<% end %> <% end %>

View file

@ -27,11 +27,17 @@ defmodule DevWeb.Router do
end end
end end
scope "/", DevWeb do scope "/auth", DevWeb do
pipe_through :browser pipe_through :browser
auth_routes_for(Example.Accounts.User, to: AuthController, path: "/auth")
sign_in_route(overrides: [DevWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default])
sign_out_route(AuthController, "/sign-out") sign_out_route(AuthController, "/sign-out")
reset_route() reset_route()
sign_in_route(
path: "/sign-in",
overrides: [DevWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default]
)
auth_routes(AuthController, Example.Accounts.User)
end end
end end

View file

@ -73,21 +73,6 @@ We can make our life easier and the code more consistent by adding formatters to
] ]
``` ```
### Phoenix 1.7 compatibility
For Phoenix 1.7 we need to change `helpers: false` to `helpers: true` in the router section:
**lib/example_web.ex**
```elixir
defmodule ExampleWeb do
# ...
def router do
quote do
use Phoenix.Router, helpers: true # <-- Change this line
# ...
```
### Tailwind ### Tailwind
If you plan on using our default [Tailwind](https://tailwindcss.com/)-based If you plan on using our default [Tailwind](https://tailwindcss.com/)-based
@ -120,25 +105,28 @@ module.exports = {
plugins: [ plugins: [
require("@tailwindcss/forms"), require("@tailwindcss/forms"),
plugin(({ addVariant }) => plugin(({ addVariant }) =>
addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"]) addVariant("phx-no-feedback", [
".phx-no-feedback&",
".phx-no-feedback &",
]),
), ),
plugin(({ addVariant }) => plugin(({ addVariant }) =>
addVariant("phx-click-loading", [ addVariant("phx-click-loading", [
".phx-click-loading&", ".phx-click-loading&",
".phx-click-loading &", ".phx-click-loading &",
]) ]),
), ),
plugin(({ addVariant }) => plugin(({ addVariant }) =>
addVariant("phx-submit-loading", [ addVariant("phx-submit-loading", [
".phx-submit-loading&", ".phx-submit-loading&",
".phx-submit-loading &", ".phx-submit-loading &",
]) ]),
), ),
plugin(({ addVariant }) => plugin(({ addVariant }) =>
addVariant("phx-change-loading", [ addVariant("phx-change-loading", [
".phx-change-loading&", ".phx-change-loading&",
".phx-change-loading &", ".phx-change-loading &",
]) ]),
), ),
], ],
}; };
@ -380,12 +368,17 @@ defmodule ExampleWeb.Router do
get "/", PageController, :home get "/", PageController, :home
# add these lines --> # add these lines -->
# Standard controller-backed routes
auth_routes AuthController, Example.Accounts.User, path: "/auth"
sign_out_route AuthController
# Prebuilt LiveViews for signing in, registration, resetting, etc.
# Leave out `register_path` and `reset_path` if you don't want to support # Leave out `register_path` and `reset_path` if you don't want to support
# user registration and/or password resets respectively. # user registration and/or password resets respectively.
sign_in_route(register_path: "/register", reset_path: "/reset") sign_in_route(register_path: "/register", reset_path: "/reset", auth_routes_prefix: "/auth")
sign_out_route AuthController
auth_routes_for Example.Accounts.User, to: AuthController
reset_route [] reset_route []
# <-- add these lines # <-- add these lines
end end

View file

@ -18,6 +18,32 @@ defmodule AshAuthentication.Phoenix.Components.Helpers do
|> socket.endpoint.config() |> socket.endpoint.config()
end end
def auth_path(socket, subject_name, auth_routes_prefix, strategy, phase, params \\ %{}) do
if auth_routes_prefix do
strategy
|> AshAuthentication.Strategy.routes()
|> Enum.find(&(elem(&1, 1) == phase))
|> elem(0)
|> URI.parse()
|> Map.put(:query, URI.encode_query(params))
|> Map.update!(:path, &Path.join(auth_routes_prefix, &1))
|> URI.to_string()
else
route_helpers = route_helpers(socket)
if Code.ensure_loaded?(route_helpers) do
route_helpers.auth_path(
socket.endpoint,
{subject_name, AshAuthentication.Strategy.name(strategy), phase}
)
else
raise """
Must configure the `auth_routes_prefix`, or enable router helpers.
"""
end
end
end
@doc """ @doc """
The LiveView `Socket` contains a refererence to the Phoenix router, and from The LiveView `Socket` contains a refererence to the Phoenix router, and from
there we can generate the name of the route helpers module. there we can generate the name of the route helpers module.

View file

@ -33,13 +33,14 @@ defmodule AshAuthentication.Phoenix.Components.MagicLink do
alias AshAuthentication.{Info, Phoenix.Components.Password.Input, Strategy} alias AshAuthentication.{Info, Phoenix.Components.Password.Input, Strategy}
alias AshPhoenix.Form alias AshPhoenix.Form
alias Phoenix.LiveView.{Rendered, Socket} alias Phoenix.LiveView.{Rendered, Socket}
import AshAuthentication.Phoenix.Components.Helpers, only: [route_helpers: 1] import AshAuthentication.Phoenix.Components.Helpers, only: [auth_path: 5]
import Slug import Slug
@type props :: %{ @type props :: %{
required(:strategy) => AshAuthentication.Strategy.t(), required(:strategy) => AshAuthentication.Strategy.t(),
optional(:overrides) => [module], optional(:overrides) => [module],
optional(:current_tenant) => String.t() optional(:current_tenant) => String.t(),
optional(:auth_routes_prefix) => String.t()
} }
@doc false @doc false
@ -58,6 +59,7 @@ defmodule AshAuthentication.Phoenix.Components.MagicLink do
|> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end) |> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end)
|> assign_new(:label, fn -> nil end) |> assign_new(:label, fn -> nil end)
|> assign_new(:current_tenant, fn -> nil end) |> assign_new(:current_tenant, fn -> nil end)
|> assign_new(:auth_routes_prefix, fn -> nil end)
{:ok, socket} {:ok, socket}
end end
@ -79,12 +81,7 @@ defmodule AshAuthentication.Phoenix.Components.MagicLink do
phx-submit="submit" phx-submit="submit"
phx-trigger-action={@trigger_action} phx-trigger-action={@trigger_action}
phx-target={@myself} phx-target={@myself}
action={ action={auth_path(@socket, @subject_name, @auth_routes_prefix, @strategy, :request)}
route_helpers(@socket).auth_path(
@socket.endpoint,
{@subject_name, Strategy.name(@strategy), :request}
)
}
method="POST" method="POST"
class={override_for(@overrides, :form_class)} class={override_for(@overrides, :form_class)}
> >

View file

@ -24,13 +24,14 @@ defmodule AshAuthentication.Phoenix.Components.OAuth2 do
use AshAuthentication.Phoenix.Web, :live_component use AshAuthentication.Phoenix.Web, :live_component
alias AshAuthentication.{Info, Strategy} alias AshAuthentication.{Info, Strategy}
alias Phoenix.LiveView.Rendered alias Phoenix.LiveView.Rendered
import AshAuthentication.Phoenix.Components.Helpers, only: [route_helpers: 1] import AshAuthentication.Phoenix.Components.Helpers, only: [auth_path: 5]
import Phoenix.HTML, only: [raw: 1] import Phoenix.HTML, only: [raw: 1]
import PhoenixHTMLHelpers.Form, only: [humanize: 1] import PhoenixHTMLHelpers.Form, only: [humanize: 1]
@type props :: %{ @type props :: %{
required(:strategy) => AshAuthentication.Strategy.t(), required(:strategy) => AshAuthentication.Strategy.t(),
optional(:overrides) => [module] optional(:overrides) => [module],
optional(:auth_routes_prefix) => String.t()
} }
@doc false @doc false
@ -41,16 +42,12 @@ defmodule AshAuthentication.Phoenix.Components.OAuth2 do
assigns assigns
|> assign(:subject_name, Info.authentication_subject_name!(assigns.strategy.resource)) |> assign(:subject_name, Info.authentication_subject_name!(assigns.strategy.resource))
|> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end) |> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end)
|> assign_new(:auth_routes_prefix, fn -> nil end)
~H""" ~H"""
<div class={override_for(@overrides, :root_class)}> <div class={override_for(@overrides, :root_class)}>
<a <a
href={ href={auth_path(@socket, @subject_name, @auth_routes_prefix, @strategy, :request)}
route_helpers(@socket).auth_path(
@socket.endpoint,
{@subject_name, Strategy.name(@strategy), :request}
)
}
class={override_for(@overrides, :link_class)} class={override_for(@overrides, :link_class)}
> >
<.icon icon={@strategy.icon} overrides={@overrides} /> <.icon icon={@strategy.icon} overrides={@overrides} />

View file

@ -143,6 +143,7 @@ defmodule AshAuthentication.Phoenix.Components.Password do
|> assign_new(:reset_path, fn -> nil end) |> assign_new(:reset_path, fn -> nil end)
|> assign_new(:register_path, fn -> nil end) |> assign_new(:register_path, fn -> nil end)
|> assign_new(:current_tenant, fn -> nil end) |> assign_new(:current_tenant, fn -> nil end)
|> assign_new(:auth_routes_prefix, fn -> nil end)
show = show =
if assigns[:live_action] == :sign_in && is_nil(assigns[:reset_path]) && if assigns[:live_action] == :sign_in && is_nil(assigns[:reset_path]) &&
@ -160,6 +161,7 @@ defmodule AshAuthentication.Phoenix.Components.Password do
<.live_component <.live_component
:let={form} :let={form}
module={Password.SignInForm} module={Password.SignInForm}
auth_routes_prefix={@auth_routes_prefix}
id={@sign_in_id} id={@sign_in_id}
strategy={@strategy} strategy={@strategy}
label={false} label={false}
@ -204,6 +206,7 @@ defmodule AshAuthentication.Phoenix.Components.Password do
<.live_component <.live_component
:let={form} :let={form}
module={Password.RegisterForm} module={Password.RegisterForm}
auth_routes_prefix={@auth_routes_prefix}
id={@register_id} id={@register_id}
strategy={@strategy} strategy={@strategy}
label={false} label={false}
@ -245,6 +248,7 @@ defmodule AshAuthentication.Phoenix.Components.Password do
<.live_component <.live_component
:let={form} :let={form}
module={Password.ResetForm} module={Password.ResetForm}
auth_routes_prefix={@auth_routes_prefix}
id={@reset_id} id={@reset_id}
strategy={@strategy} strategy={@strategy}
label={false} label={false}

View file

@ -44,7 +44,8 @@ defmodule AshAuthentication.Phoenix.Components.Password.RegisterForm do
required(:strategy) => AshAuthentication.Strategy.t(), required(:strategy) => AshAuthentication.Strategy.t(),
optional(:overrides) => [module], optional(:overrides) => [module],
optional(:live_action) => :sign_in | :register, optional(:live_action) => :sign_in | :register,
optional(:current_tenant) => String.t() optional(:current_tenant) => String.t(),
optional(:auth_routes_prefix) => String.t()
} }
@doc false @doc false
@ -79,6 +80,7 @@ defmodule AshAuthentication.Phoenix.Components.Password.RegisterForm do
|> assign_new(:inner_block, fn -> nil end) |> assign_new(:inner_block, fn -> nil end)
|> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end) |> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end)
|> assign_new(:current_tenant, fn -> nil end) |> assign_new(:current_tenant, fn -> nil end)
|> assign_new(:auth_routes_prefix, fn -> nil end)
{:ok, socket} {:ok, socket}
end end
@ -102,12 +104,7 @@ defmodule AshAuthentication.Phoenix.Components.Password.RegisterForm do
phx-submit="submit" phx-submit="submit"
phx-trigger-action={@trigger_action} phx-trigger-action={@trigger_action}
phx-target={@myself} phx-target={@myself}
action={ action={auth_path(@socket, @subject_name, @auth_routes_prefix, @strategy, :register)}
route_helpers(@socket).auth_path(
@socket.endpoint,
{@subject_name, Strategy.name(@strategy), :register}
)
}
method="POST" method="POST"
class={override_for(@overrides, :form_class)} class={override_for(@overrides, :form_class)}
> >
@ -166,10 +163,12 @@ defmodule AshAuthentication.Phoenix.Components.Password.RegisterForm do
) do ) do
{:ok, user} -> {:ok, user} ->
validate_sign_in_token_path = validate_sign_in_token_path =
route_helpers(socket).auth_path( auth_path(
socket.endpoint, socket,
{socket.assigns.subject_name, Strategy.name(socket.assigns.strategy), socket.assigns.subject_name,
:sign_in_with_token}, socket.assigns.auth_routes_prefix,
socket.assigns.strategy,
:sign_in_with_token,
token: user.__metadata__.token token: user.__metadata__.token
) )

View file

@ -45,7 +45,8 @@ defmodule AshAuthentication.Phoenix.Components.Password.ResetForm do
required(:strategy) => AshAuthentication.Strategy.t(), required(:strategy) => AshAuthentication.Strategy.t(),
optional(:label) => String.t() | false, optional(:label) => String.t() | false,
optional(:overrides) => [module], optional(:overrides) => [module],
optional(:current_tenant) => String.t() optional(:current_tenant) => String.t(),
optional(:auth_routes_prefix) => String.t()
} }
@doc false @doc false
@ -63,6 +64,7 @@ defmodule AshAuthentication.Phoenix.Components.Password.ResetForm do
|> assign_new(:inner_block, fn -> nil end) |> assign_new(:inner_block, fn -> nil end)
|> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end) |> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end)
|> assign_new(:current_tenant, fn -> nil end) |> assign_new(:current_tenant, fn -> nil end)
|> assign_new(:auth_routes_prefix, fn -> nil end)
{:ok, socket} {:ok, socket}
end end
@ -85,12 +87,7 @@ defmodule AshAuthentication.Phoenix.Components.Password.ResetForm do
phx-submit="submit" phx-submit="submit"
phx-change="change" phx-change="change"
phx-target={@myself} phx-target={@myself}
action={ action={auth_path(@socket, @subject_name, @auth_routes_prefix, @strategy, :reset_request)}
route_helpers(@socket).auth_path(
@socket.endpoint,
{@subject_name, Strategy.name(@strategy), :reset_request}
)
}
method="POST" method="POST"
class={override_for(@overrides, :form_class)} class={override_for(@overrides, :form_class)}
> >

View file

@ -37,7 +37,7 @@ defmodule AshAuthentication.Phoenix.Components.Password.SignInForm do
alias Phoenix.LiveView.{Rendered, Socket} alias Phoenix.LiveView.{Rendered, Socket}
import AshAuthentication.Phoenix.Components.Helpers, import AshAuthentication.Phoenix.Components.Helpers,
only: [route_helpers: 1] only: [auth_path: 5, auth_path: 6]
import PhoenixHTMLHelpers.Form import PhoenixHTMLHelpers.Form
import Slug import Slug
@ -46,7 +46,8 @@ defmodule AshAuthentication.Phoenix.Components.Password.SignInForm do
required(:strategy) => AshAuthentication.Strategy.t(), required(:strategy) => AshAuthentication.Strategy.t(),
optional(:label) => String.t() | false, optional(:label) => String.t() | false,
optional(:overrides) => [module], optional(:overrides) => [module],
optional(:current_tenant) => String.t() optional(:current_tenant) => String.t(),
optional(:auth_routes_prefix) => String.t()
} }
@doc false @doc false
@ -77,6 +78,7 @@ defmodule AshAuthentication.Phoenix.Components.Password.SignInForm do
|> assign_new(:inner_block, fn -> nil end) |> assign_new(:inner_block, fn -> nil end)
|> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end) |> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end)
|> assign_new(:current_tenant, fn -> nil end) |> assign_new(:current_tenant, fn -> nil end)
|> assign_new(:auth_routes_prefix, fn -> nil end)
{:ok, socket} {:ok, socket}
end end
@ -98,12 +100,7 @@ defmodule AshAuthentication.Phoenix.Components.Password.SignInForm do
phx-submit="submit" phx-submit="submit"
phx-trigger-action={@trigger_action} phx-trigger-action={@trigger_action}
phx-target={@myself} phx-target={@myself}
action={ action={auth_path(@socket, @subject_name, @auth_routes_prefix, @strategy, :sign_in)}
route_helpers(@socket).auth_path(
@socket.endpoint,
{@subject_name, Strategy.name(@strategy), :sign_in}
)
}
method="POST" method="POST"
class={override_for(@overrides, :form_class)} class={override_for(@overrides, :form_class)}
> >
@ -158,10 +155,12 @@ defmodule AshAuthentication.Phoenix.Components.Password.SignInForm do
) do ) do
{:ok, user} -> {:ok, user} ->
validate_sign_in_token_path = validate_sign_in_token_path =
route_helpers(socket).auth_path( auth_path(
socket.endpoint, socket,
{socket.assigns.subject_name, Strategy.name(socket.assigns.strategy), socket.assigns.subject_name,
:sign_in_with_token}, socket.assigns.auth_routes_prefix,
socket.assigns.strategy,
:sign_in_with_token,
token: user.__metadata__.token token: user.__metadata__.token
) )

View file

@ -53,6 +53,7 @@ defmodule AshAuthentication.Phoenix.Components.Reset do
socket socket
|> assign(strategies: strategies) |> assign(strategies: strategies)
|> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end) |> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end)
|> assign_new(:auth_routes_prefix, fn -> nil end)
{:ok, socket} {:ok, socket}
end end
@ -71,6 +72,7 @@ defmodule AshAuthentication.Phoenix.Components.Reset do
<div class={override_for(@overrides, :strategy_class)}> <div class={override_for(@overrides, :strategy_class)}>
<.live_component <.live_component
module={Components.Reset.Form} module={Components.Reset.Form}
auth_routes_prefix={@auth_routes_prefix}
strategy={strategy} strategy={strategy}
token={@token} token={@token}
id="reset-form" id="reset-form"

View file

@ -38,7 +38,7 @@ defmodule AshAuthentication.Phoenix.Components.Reset.Form do
alias AshAuthentication.{Info, Phoenix.Components.Password.Input, Strategy} alias AshAuthentication.{Info, Phoenix.Components.Password.Input, Strategy}
alias AshPhoenix.Form alias AshPhoenix.Form
alias Phoenix.LiveView.{Rendered, Socket} alias Phoenix.LiveView.{Rendered, Socket}
import AshAuthentication.Phoenix.Components.Helpers, only: [route_helpers: 1] import AshAuthentication.Phoenix.Components.Helpers, only: [auth_path: 5]
import PhoenixHTMLHelpers.Form import PhoenixHTMLHelpers.Form
import Slug import Slug
@ -47,7 +47,8 @@ defmodule AshAuthentication.Phoenix.Components.Reset.Form do
required(:strategy) => AshAuthentication.Strategy.t(), required(:strategy) => AshAuthentication.Strategy.t(),
required(:token) => String.t(), required(:token) => String.t(),
optional(:label) => String.t() | false, optional(:label) => String.t() | false,
optional(:overrides) => [module] optional(:overrides) => [module],
optional(:auth_routes_prefix) => String.t()
} }
@doc false @doc false
@ -82,6 +83,7 @@ defmodule AshAuthentication.Phoenix.Components.Reset.Form do
) )
|> assign_new(:label, fn -> humanize(resettable.password_reset_action_name) end) |> assign_new(:label, fn -> humanize(resettable.password_reset_action_name) end)
|> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end) |> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end)
|> assign_new(:auth_routes_prefix, fn -> nil end)
{:ok, socket} {:ok, socket}
end end
@ -103,12 +105,7 @@ defmodule AshAuthentication.Phoenix.Components.Reset.Form do
phx-submit="submit" phx-submit="submit"
phx-trigger-action={@trigger_action} phx-trigger-action={@trigger_action}
phx-target={@myself} phx-target={@myself}
action={ action={auth_path(@socket, @subject_name, @auth_routes_prefix, @strategy, :reset)}
route_helpers(@socket).auth_path(
@socket.endpoint,
{@subject_name, Strategy.name(@strategy), :reset}
)
}
method="POST" method="POST"
class={override_for(@overrides, :form_class)} class={override_for(@overrides, :form_class)}
> >

View file

@ -81,6 +81,7 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do
|> assign_new(:reset_path, fn -> nil end) |> assign_new(:reset_path, fn -> nil end)
|> assign_new(:register_path, fn -> nil end) |> assign_new(:register_path, fn -> nil end)
|> assign_new(:current_tenant, fn -> nil end) |> assign_new(:current_tenant, fn -> nil end)
|> assign_new(:auth_routes_prefix, fn -> nil end)
{:ok, socket} {:ok, socket}
end end
@ -103,6 +104,7 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do
live_action={@live_action} live_action={@live_action}
strategy={strategy} strategy={strategy}
path={@path} path={@path}
auth_routes_prefix={@auth_routes_prefix}
reset_path={@reset_path} reset_path={@reset_path}
register_path={@register_path} register_path={@register_path}
overrides={@overrides} overrides={@overrides}
@ -125,6 +127,7 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do
component={component_for_strategy(strategy)} component={component_for_strategy(strategy)}
live_action={@live_action} live_action={@live_action}
strategy={strategy} strategy={strategy}
auth_routes_prefix={@auth_routes_prefix}
path={@path} path={@path}
reset_path={@reset_path} reset_path={@reset_path}
register_path={@register_path} register_path={@register_path}
@ -145,6 +148,7 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do
module={@component} module={@component}
id={strategy_id(@strategy)} id={strategy_id(@strategy)}
strategy={@strategy} strategy={@strategy}
auth_routes_prefix={@auth_routes_prefix}
path={@path} path={@path}
reset_path={@reset_path} reset_path={@reset_path}
register_path={@register_path} register_path={@register_path}

View file

@ -37,6 +37,13 @@ defmodule AshAuthentication.Phoenix.LiveSession do
defmacro ash_authentication_live_session(session_name \\ :ash_authentication, opts \\ [], defmacro ash_authentication_live_session(session_name \\ :ash_authentication, opts \\ [],
do: block do: block
) do ) do
opts =
if Macro.quoted_literal?(opts) do
Macro.prewalk(opts, &expand_alias(&1, __CALLER__))
else
opts
end
quote do quote do
on_mount = [LiveSession] on_mount = [LiveSession]
@ -64,6 +71,11 @@ defmodule AshAuthentication.Phoenix.LiveSession do
end end
end end
defp expand_alias({:__aliases__, _, _} = alias, env),
do: Macro.expand(alias, %{env | function: {:mount, 3}})
defp expand_alias(other, _env), do: other
@doc """ @doc """
Inspects the incoming session for any subject_name -> subject values and loads Inspects the incoming session for any subject_name -> subject values and loads
them into the socket's assigns. them into the socket's assigns.

View file

@ -119,6 +119,60 @@ defmodule AshAuthentication.Phoenix.Router do
end end
end end
@doc """
Generates the routes needed for the various strategies for a given
AshAuthentication resource.
This matches *all* routes at the provided `path`, which defaults to `/auth`. This means that
if you have any other routes that begin with `/auth`, you will need to make sure this
appears after them.
## Upgrading from `auth_routes_for/2`
If you are using route helpers anywhere in your application, typically looks like `Routes.auth_path/3`
or `Helpers.auth_path/3` you will need to update them to use verified routes. To see what routes are
available to you, use `mix ash_authentication.phoenix.routes`.
If you are using any of the components provided by `AshAuthenticationPhoenix`, you will need to supply
them with the `auth_routes_prefix` assign, set to the `path` you provide here (set to `/auth` by default).
## Options
* `path` - the path to mount auth routes at. Defaults to `/auth`. If changed, you will also want
to change the `auth_routes_prefix` option in `sign_in_route` to match.
routes.
* `not_found_plug` - a plug to call if no route is found. By default, it renders a simple JSON
response with a 404 status code.
* `as` - the alias to use for the generated scope. Defaults to `:auth`.
"""
@spec auth_routes(
auth_controller :: module(),
Ash.Resource.t() | list(Ash.Resource.t()),
auth_route_options
) :: Macro.t()
defmacro auth_routes(auth_controller, resource_or_resources, opts \\ []) when is_list(opts) do
resource_or_resources =
resource_or_resources
|> List.wrap()
|> Enum.map(&Macro.expand_once(&1, %{__CALLER__ | function: {:auth_routes, 2}}))
quote location: :keep do
opts = unquote(opts)
path = Keyword.get(opts, :path, "/auth")
not_found_plug = Keyword.get(opts, :not_found_plug)
controller = Phoenix.Router.scoped_alias(__MODULE__, unquote(auth_controller))
scope "/", alias: false do
forward path, AshAuthentication.Phoenix.StrategyRouter,
path: Phoenix.Router.scoped_path(__MODULE__, path),
as: opts[:as] || :auth,
controller: controller,
not_found_plug: not_found_plug,
resources: List.wrap(unquote(resource_or_resources))
end
end
end
@doc """ @doc """
Generates a generic, white-label sign-in page using LiveView and the Generates a generic, white-label sign-in page using LiveView and the
components in `AshAuthentication.Phoenix.Components`. components in `AshAuthentication.Phoenix.Components`.
@ -128,12 +182,15 @@ defmodule AshAuthentication.Phoenix.Router do
Available options are: 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"`.
* `auth_routes_prefix` if set, this will be used instead of route helpers when determining routes.
Allows disabling `helpers: true`.
* `register_path` - the path under which to mount the password strategy's registration live-view. * `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 not set, and registration is supported, registration will use a dynamic toggle and will not be routeable to.
* `reset_path` - the path under which to mount the password strategy's password reset live-view. * `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 not set, and password reset is supported, password reset will use a dynamic toggle and will not be routeable to.
* `live_view` the name of the live view to render. Defaults to * `live_view` the name of the live view to render. Defaults to
`AshAuthentication.Phoenix.SignInLive`. `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 passed to the generated `live` route. Defaults to `:auth`.
* `otp_app` the otp app or apps to find authentication resources in. Pulls from the socket by default. * `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 * `overrides` specify any override modules for customisation. See
@ -160,6 +217,7 @@ defmodule AshAuthentication.Phoenix.Router do
{on_mount, opts} = Keyword.pop(opts, :on_mount) {on_mount, opts} = Keyword.pop(opts, :on_mount)
{reset_path, opts} = Keyword.pop(opts, :reset_path) {reset_path, opts} = Keyword.pop(opts, :reset_path)
{register_path, opts} = Keyword.pop(opts, :register_path) {register_path, opts} = Keyword.pop(opts, :register_path)
{auth_routes_prefix, opts} = Keyword.pop(opts, :auth_routes_prefix)
{overrides, opts} = {overrides, opts} =
Keyword.pop(opts, :overrides, [AshAuthentication.Phoenix.Overrides.Default]) Keyword.pop(opts, :overrides, [AshAuthentication.Phoenix.Overrides.Default])
@ -174,7 +232,7 @@ defmodule AshAuthentication.Phoenix.Router do
on_mount = on_mount =
[ [
AshAuthenticationPhoenix.Router.OnLiveViewMount, AshAuthentication.Phoenix.Router.OnLiveViewMount,
AshAuthentication.Phoenix.LiveSession | unquote(on_mount || []) AshAuthentication.Phoenix.LiveSession | unquote(on_mount || [])
] ]
|> Enum.uniq_by(fn |> Enum.uniq_by(fn
@ -182,12 +240,30 @@ defmodule AshAuthentication.Phoenix.Router do
mod -> mod mod -> mod
end) end)
register_path =
case unquote(register_path) do
nil -> nil
{:unscoped, value} -> value
value -> Phoenix.Router.scoped_path(__MODULE__, value)
end
reset_path =
case unquote(reset_path) do
nil -> nil
{:unscoped, value} -> value
value -> Phoenix.Router.scoped_path(__MODULE__, value)
end
unquote(register_path) &&
Phoenix.Router.scoped_path(__MODULE__, unquote(register_path))
live_session_opts = [ live_session_opts = [
session: session:
{AshAuthentication.Phoenix.Router, :generate_session, {AshAuthentication.Phoenix.Router, :generate_session,
[ [
%{ %{
"overrides" => unquote(overrides), "overrides" => unquote(overrides),
"auth_routes_prefix" => unquote(auth_routes_prefix),
"otp_app" => unquote(otp_app), "otp_app" => unquote(otp_app),
"path" => Phoenix.Router.scoped_path(__MODULE__, unquote(path)), "path" => Phoenix.Router.scoped_path(__MODULE__, unquote(path)),
"reset_path" => "reset_path" =>
@ -294,7 +370,7 @@ defmodule AshAuthentication.Phoenix.Router do
on_mount = on_mount =
[ [
AshAuthenticationPhoenix.Router.OnLiveViewMount, AshAuthentication.Phoenix.Router.OnLiveViewMount,
AshAuthentication.Phoenix.LiveSession | unquote(on_mount || []) AshAuthentication.Phoenix.LiveSession | unquote(on_mount || [])
] ]
|> Enum.uniq_by(fn |> Enum.uniq_by(fn

View file

@ -0,0 +1,103 @@
defmodule AshAuthentication.Phoenix.Router.ConsoleFormatter do
@moduledoc false
@doc """
Format the routes for printing.
This was copied from Phoenix and adapted for our case.
"""
def format(router, endpoint \\ nil) do
routes = Phoenix.Router.routes(router)
column_widths = calculate_column_widths(router, routes, endpoint)
routes
|> Enum.map(&format_route(&1, router, column_widths))
|> Enum.filter(& &1)
|> Enum.join("")
end
defp calculate_column_widths(router, routes, endpoint) do
sockets = (endpoint && endpoint.__sockets__()) || []
widths =
Enum.reduce(routes, {0, 0, 0}, fn route, acc ->
%{verb: verb, path: path, helper: helper} = route
verb = verb_name(verb)
{verb_len, path_len, route_name_len} = acc
route_name = route_name(router, helper)
{max(verb_len, String.length(verb)), max(path_len, String.length(path)),
max(route_name_len, String.length(route_name))}
end)
Enum.reduce(sockets, widths, fn {path, _mod, _opts}, acc ->
{verb_len, path_len, route_name_len} = acc
prefix = if router.__helpers__(), do: "websocket", else: ""
{verb_len, max(path_len, String.length(path <> "/websocket")),
max(route_name_len, String.length(prefix))}
end)
end
defp format_route(
%{
verb: :*,
plug: AshAuthentication.Phoenix.StrategyRouter,
path: plug_path,
plug_opts: plug_opts,
helper: helper
},
router,
column_widths
) do
plug_opts[:resources]
|> List.wrap()
|> resource_routes()
|> Enum.map(fn {strategy, path, phase} ->
verb = verb_name(AshAuthentication.Strategy.method_for_phase(strategy, phase))
route_name = route_name(router, helper)
{verb_len, path_len, route_name_len} = column_widths
log_module = strategy.__struct__
path = Path.join(plug_path || "/", path)
String.pad_leading(route_name, route_name_len) <>
" " <>
String.pad_trailing(verb, verb_len) <>
" " <>
String.pad_trailing(path, path_len) <>
" " <>
"#{inspect(log_module)}\n"
end)
end
defp format_route(_, _, _), do: nil
defp resource_routes(resources) do
Stream.flat_map(resources, fn resource ->
resource
|> AshAuthentication.Info.authentication_add_ons()
|> Enum.concat(AshAuthentication.Info.authentication_strategies(resource))
|> strategy_routes()
end)
end
defp strategy_routes(strategies) do
Stream.flat_map(strategies, fn strategy ->
strategy
|> AshAuthentication.Strategy.routes()
|> Stream.map(fn {path, phase} -> {strategy, path, phase} end)
end)
end
defp route_name(_router, nil), do: ""
defp route_name(router, name) do
if router.__helpers__() do
name <> "_path"
else
""
end
end
defp verb_name(verb), do: verb |> to_string() |> String.upcase()
end

View file

@ -1,4 +1,4 @@
defmodule AshAuthenticationPhoenix.Router.OnLiveViewMount do defmodule AshAuthentication.Phoenix.Router.OnLiveViewMount do
@moduledoc false @moduledoc false
import Phoenix.Component import Phoenix.Component

View file

@ -37,6 +37,7 @@ defmodule AshAuthentication.Phoenix.SignInLive do
|> assign(:reset_path, session["reset_path"]) |> assign(:reset_path, session["reset_path"])
|> assign(:register_path, session["register_path"]) |> assign(:register_path, session["register_path"])
|> assign(:current_tenant, session["tenant"]) |> assign(:current_tenant, session["tenant"])
|> assign(:auth_routes_prefix, session["auth_routes_prefix"])
{:ok, socket} {:ok, socket}
end end
@ -57,6 +58,7 @@ defmodule AshAuthentication.Phoenix.SignInLive do
otp_app={@otp_app} otp_app={@otp_app}
live_action={@live_action} live_action={@live_action}
path={@path} path={@path}
auth_routes_prefix={@auth_routes_prefix}
reset_path={@reset_path} reset_path={@reset_path}
register_path={@register_path} register_path={@register_path}
id={override_for(@overrides, :sign_in_id, "sign-in")} id={override_for(@overrides, :sign_in_id, "sign-in")}

View file

@ -0,0 +1,81 @@
defmodule AshAuthentication.Phoenix.StrategyRouter do
@moduledoc false
@behaviour Plug
@impl true
def init(opts), do: opts
@impl true
def call(conn, opts) do
opts
|> routes()
|> Enum.reduce_while(
{:not_found, conn},
fn {resource, strategy, path, phase}, {:not_found, conn} ->
strategy_path_split = Path.split(String.trim_leading(path, "/"))
if paths_match?(strategy_path_split, conn.path_info) do
{:halt, {:found, resource, strategy, path, phase}}
else
{:cont, {:not_found, conn}}
end
end
)
|> case do
{:found, resource, strategy, _path, phase} ->
subject_name = AshAuthentication.Info.authentication_subject_name!(resource)
conn
|> Plug.Conn.put_private(:strategy, strategy)
|> opts[:controller].call(
{subject_name, AshAuthentication.Strategy.name(strategy), phase}
)
{:not_found, conn} ->
not_found(conn, opts)
end
end
defp not_found(conn, opts) do
if plug = opts[:not_found_plug] do
plug.call(conn, opts)
else
conn
|> Plug.Conn.put_status(:not_found)
|> Phoenix.Controller.json(%{error: "Not Found"})
|> Plug.Conn.halt()
end
end
defp paths_match?([], []), do: true
defp paths_match?([":" <> _ | strategy_rest], [_ | actual_rest]) do
paths_match?(strategy_rest, actual_rest)
end
defp paths_match?([":*"], _), do: true
defp paths_match?([item | strategy_rest], [item | actual_rest]) do
paths_match?(strategy_rest, actual_rest)
end
defp paths_match?(_, _), do: false
defp routes(opts) do
opts[:resources]
|> Stream.flat_map(fn resource ->
resource
|> AshAuthentication.Info.authentication_add_ons()
|> Enum.concat(AshAuthentication.Info.authentication_strategies(resource))
|> Stream.flat_map(&strategy_routes(resource, &1))
end)
end
defp strategy_routes(resource, strategy) do
strategy
|> AshAuthentication.Strategy.routes()
|> Stream.map(fn {path, phase} ->
{resource, strategy, path, phase}
end)
end
end

View file

@ -0,0 +1,143 @@
defmodule Mix.Tasks.AshAuthentication.Phoenix.Routes do
use Mix.Task
alias AshAuthentication.Phoenix.Router.ConsoleFormatter
@moduledoc """
Prints all routes pertaining to AshAuthenticationPhoenix for the default or a given router.
This task can be called directly, accepting the same options as `mix phx.routes`, except for `--info`.
Alternatively, you can modify your aliases task to run them back to back it.
```elixir
aliases: ["phx.routes": ["do", "phx.routes,", "ash_authentication.phx.routes"]]
```
"""
@shortdoc "Prints all routes generated by AshAuthentication Phoenix"
@impl true
def run(args, base \\ Mix.Phoenix.base()) do
Mix.Task.run("compile", args)
Mix.Task.reenable("ash_authentication.phoenix.routes")
{opts, args, _} =
OptionParser.parse(args, switches: [endpoint: :string, router: :string, info: :string])
{router_mod, endpoint_mod} =
case args do
[passed_router] -> {router(passed_router, base), opts[:endpoint]}
[] -> {router(opts[:router], base), endpoint(opts[:endpoint], base)}
end
case Keyword.fetch(opts, :info) do
{:ok, url} ->
get_url_info(url, {router_mod, opts})
:error ->
router_mod
|> ConsoleFormatter.format(endpoint_mod)
|> Mix.shell().info()
end
end
defp router(nil, base) do
if Mix.Project.umbrella?() do
Mix.raise("""
umbrella applications require an explicit router to be given to phx.routes, for example:
$ mix ash_authentication.phoenix.routes MyAppWeb.Router
An alias can be added to mix.exs aliases to automate this:
"ash_authentication.phoenix.routes": "ash_authentication.phoenix.routes MyAppWeb.Router"
""")
end
web_router = web_mod(base, "Router")
old_router = app_mod(base, "Router")
loaded(web_router) || loaded(old_router) ||
Mix.raise("""
no router found at #{inspect(web_router)} or #{inspect(old_router)}.
An explicit router module may be given to ash_authentication.phoenix.routes, for example:
$ mix ash_authentication.phoenix.routes MyAppWeb.Router
An alias can be added to mix.exs aliases to automate this:
"ash_authentication.phoenix.routes": "ash_authentication.phoenix.routes MyAppWeb.Router"
""")
end
defp router(router_name, _base) do
arg_router = Module.concat([router_name])
loaded(arg_router) || Mix.raise("the provided router, #{inspect(arg_router)}, does not exist")
end
defp endpoint(nil, base) do
loaded(web_mod(base, "Endpoint"))
end
defp endpoint(module, _base) do
loaded(Module.concat([module]))
end
defp app_mod(base, name), do: Module.concat([base, name])
defp web_mod(base, name), do: Module.concat(["#{base}Web", name])
defp loaded(module) do
if Code.ensure_loaded?(module), do: module
end
def get_url_info(url, {router_mod, _opts}) do
%{path: path} = URI.parse(url)
meta = Phoenix.Router.route_info(router_mod, "GET", path, "")
%{plug: plug, plug_opts: plug_opts} = meta
{module, func_name} =
if log_mod = meta[:log_module] do
{log_mod, meta[:log_function]}
else
{plug, plug_opts}
end
Mix.shell().info("Module: #{inspect(module)}")
if func_name, do: Mix.shell().info("Function: #{inspect(func_name)}")
file_path = get_file_path(module)
if line = get_line_number(module, func_name) do
Mix.shell().info("#{file_path}:#{line}")
else
Mix.shell().info("#{file_path}")
end
end
defp get_file_path(module_name) do
[compile_infos] = Keyword.get_values(module_name.module_info(), :compile)
[source] = Keyword.get_values(compile_infos, :source)
source
end
defp get_line_number(_, nil), do: nil
defp get_line_number(module, function_name) do
{_, _, _, _, _, _, functions_list} = Code.fetch_docs(module)
function_infos =
functions_list
|> Enum.find(fn {{type, name, _}, _, _, _, _} ->
type == :function and name == function_name
end)
case function_infos do
{_, line, _, _, _} -> line
nil -> nil
end
end
end

View file

@ -132,7 +132,8 @@ defmodule AshAuthentication.Phoenix.MixProject do
{:mimic, "~> 1.7", only: [:dev, :test]}, {:mimic, "~> 1.7", only: [:dev, :test]},
{:mix_audit, "~> 2.1", only: [:dev, :test]}, {:mix_audit, "~> 2.1", only: [:dev, :test]},
{:plug_cowboy, "~> 2.5", only: [:dev, :test]}, {:plug_cowboy, "~> 2.5", only: [:dev, :test]},
{:sobelow, "~> 0.13", only: [:dev, :test]} {:sobelow, "~> 0.13", only: [:dev, :test]},
{:floki, ">= 0.30.0", only: :test}
] ]
end end

View file

@ -25,6 +25,7 @@
"faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"}, "faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"},
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
"floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"}, "git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
"git_ops": {:hex, :git_ops, "2.6.1", "cc7799a68c26cf814d6d1a5121415b4f5bf813de200908f930b27a2f1fe9dad5", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "ce62d07e41fe993ec22c35d5edb11cf333a21ddaead6f5d9868fcb607d42039e"}, "git_ops": {:hex, :git_ops, "2.6.1", "cc7799a68c26cf814d6d1a5121415b4f5bf813de200908f930b27a2f1fe9dad5", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "ce62d07e41fe993ec22c35d5edb11cf333a21ddaead6f5d9868fcb607d42039e"},
"glob_ex": {:hex, :glob_ex, "0.1.8", "f7ef872877ca2ae7a792ab1f9ff73d9c16bf46ecb028603a8a3c5283016adc07", [:mix], [], "hexpm", "9e39d01729419a60a937c9260a43981440c43aa4cadd1fa6672fecd58241c464"}, "glob_ex": {:hex, :glob_ex, "0.1.8", "f7ef872877ca2ae7a792ab1f9ff73d9c16bf46ecb028603a8a3c5283016adc07", [:mix], [], "hexpm", "9e39d01729419a60a937c9260a43981440c43aa4cadd1fa6672fecd58241c464"},

26
test/router_test.exs Normal file
View file

@ -0,0 +1,26 @@
defmodule AshAuthentication.Phoenix.RouterTest do
@moduledoc false
use ExUnit.Case
test "sign_in_routes adds a route according to its scope" do
route =
AshAuthentication.Phoenix.Test.Router
|> Phoenix.Router.routes()
|> Enum.find(&(&1.path == "/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" => "/sign-in",
"register_path" => "/register",
"reset_path" => "/reset"
}
]}
end
end

18
test/sign_in_test.exs Normal file
View file

@ -0,0 +1,18 @@
defmodule AshAuthentication.Phoenix.SignInTest do
@moduledoc false
use ExUnit.Case
import Phoenix.ConnTest
import Phoenix.LiveViewTest
@endpoint AshAuthentication.Phoenix.Test.Endpoint
setup do
# foo
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
test "sign_in routes liveview renders the sign in page", %{conn: conn} do
conn = get(conn, "/sign-in")
assert {:ok, _view, html} = live(conn)
assert html =~ "Sign in"
end
end

View file

@ -0,0 +1,32 @@
defmodule AshAuthentication.Phoenix.Test.AuthController do
@moduledoc false
use DevWeb, :controller
use AshAuthentication.Phoenix.Controller
@doc false
@impl true
def success(conn, _activity, user, _token) do
conn
|> store_in_session(user)
|> assign(:current_user, user)
|> put_status(200)
|> render("success.html")
end
@doc false
@impl true
def failure(conn, _activity, reason) do
conn
|> assign(:failure_reason, reason)
|> redirect(to: "/sign-in")
end
@doc false
@impl true
def sign_out(conn, _params) do
conn
|> clear_session()
|> render("sign_out.html")
end
end

105
test/support/phoenix.ex Normal file
View file

@ -0,0 +1,105 @@
defmodule AshAuthentication.Phoenix.Test.ErrorView do
@moduledoc false
@doc false
def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end
defmodule AshAuthentication.Phoenix.Test.HomeLive do
@moduledoc false
use Phoenix.LiveView, layout: {__MODULE__, :live}
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :count, 0)}
end
defp phx_vsn, do: Application.spec(:phoenix, :vsn)
defp lv_vsn, do: Application.spec(:phoenix_live_view, :vsn)
@doc false
def render("live.html", assigns) do
~H"""
<script src={"https://cdn.jsdelivr.net/npm/phoenix@#{phx_vsn()}/priv/static/phoenix.min.js"}>
</script>
<script
src={"https://cdn.jsdelivr.net/npm/phoenix_live_view@#{lv_vsn()}/priv/static/phoenix_live_view.min.js"}
>
</script>
<script>
let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
liveSocket.connect()
</script>
<style>
* { font-size: 1.1em; }
</style>
<%= @inner_content %>
"""
end
@impl true
def render(assigns) do
~H"""
<%= @count %>
<button phx-click="inc">+</button>
<button phx-click="dec">-</button>
"""
end
@impl true
def handle_event("inc", _params, socket) do
{:noreply, assign(socket, :count, socket.assigns.count + 1)}
end
def handle_event("dec", _params, socket) do
{:noreply, assign(socket, :count, socket.assigns.count - 1)}
end
end
defmodule AshAuthentication.Phoenix.Test.Router do
@moduledoc false
use Phoenix.Router
import Phoenix.LiveView.Router
use AshAuthentication.Phoenix.Router
pipeline :browser do
plug(:accepts, ["html"])
plug :fetch_session
plug :fetch_live_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :load_from_session
end
scope "/", AshAuthentication.Phoenix.Test do
pipe_through :browser
sign_in_route register_path: "/register", reset_path: "/reset", auth_routes_prefix: "/auth"
sign_out_route AuthController
reset_route []
auth_routes AuthController, Example.Accounts.User, path: "/auth"
end
scope "/", AshAuthentication.Phoenix.Test do
pipe_through(:browser)
live("/", HomeLive, :index)
end
end
defmodule AshAuthentication.Phoenix.Test.Endpoint do
@moduledoc false
use Phoenix.Endpoint, otp_app: :ash_authentication_phoenix
@session_options [
store: :cookie,
key: "_webuilt_key",
signing_salt: "c911QDW5",
same_site: "Lax"
]
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
plug Plug.Session, @session_options
plug(AshAuthentication.Phoenix.Test.Router)
end

View file

@ -1 +1,3 @@
ExUnit.start() ExUnit.start()
AshAuthentication.Phoenix.Test.Endpoint.start_link()