mirror of
https://github.com/team-alembic/ash_authentication_phoenix.git
synced 2024-09-19 04:53:56 +12:00
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:
parent
29364dcf4d
commit
9f5feedc7d
30 changed files with 936 additions and 85 deletions
217
.credo.exs
Normal file
217
.credo.exs
Normal 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`.
|
||||
#
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -9,6 +9,9 @@ locals_without_parens = [
|
|||
auth_routes_for: 1,
|
||||
auth_routes_for: 2,
|
||||
auth_routes_for: 3,
|
||||
auth_routes: 1,
|
||||
auth_routes: 2,
|
||||
auth_routes: 3,
|
||||
reset_route: 1,
|
||||
set: 2,
|
||||
ash_authentication_live_session: 1,
|
||||
|
|
|
@ -13,3 +13,11 @@ config :ash_authentication, AshAuthentication.Jwt,
|
|||
signing_secret: "Marty McFly in the past with the Delorean"
|
||||
|
||||
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
|
||||
|
|
|
@ -14,11 +14,11 @@ defmodule DevWeb.HomePageLive do
|
|||
<%= if @current_user do %>
|
||||
<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 %>
|
||||
<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 />
|
||||
<.link navigate={Routes.live_path(@socket, DevWeb.CustomSignInLive)}>Custom sign in</.link>
|
||||
<% end %>
|
||||
|
|
|
@ -27,11 +27,17 @@ defmodule DevWeb.Router do
|
|||
end
|
||||
end
|
||||
|
||||
scope "/", DevWeb do
|
||||
scope "/auth", DevWeb do
|
||||
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")
|
||||
reset_route()
|
||||
|
||||
sign_in_route(
|
||||
path: "/sign-in",
|
||||
overrides: [DevWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default]
|
||||
)
|
||||
|
||||
auth_routes(AuthController, Example.Accounts.User)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
||||
If you plan on using our default [Tailwind](https://tailwindcss.com/)-based
|
||||
|
@ -120,25 +105,28 @@ module.exports = {
|
|||
plugins: [
|
||||
require("@tailwindcss/forms"),
|
||||
plugin(({ addVariant }) =>
|
||||
addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])
|
||||
addVariant("phx-no-feedback", [
|
||||
".phx-no-feedback&",
|
||||
".phx-no-feedback &",
|
||||
]),
|
||||
),
|
||||
plugin(({ addVariant }) =>
|
||||
addVariant("phx-click-loading", [
|
||||
".phx-click-loading&",
|
||||
".phx-click-loading &",
|
||||
])
|
||||
]),
|
||||
),
|
||||
plugin(({ addVariant }) =>
|
||||
addVariant("phx-submit-loading", [
|
||||
".phx-submit-loading&",
|
||||
".phx-submit-loading &",
|
||||
])
|
||||
]),
|
||||
),
|
||||
plugin(({ addVariant }) =>
|
||||
addVariant("phx-change-loading", [
|
||||
".phx-change-loading&",
|
||||
".phx-change-loading &",
|
||||
])
|
||||
]),
|
||||
),
|
||||
],
|
||||
};
|
||||
|
@ -380,12 +368,17 @@ defmodule ExampleWeb.Router do
|
|||
get "/", PageController, :home
|
||||
|
||||
# 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
|
||||
# user registration and/or password resets respectively.
|
||||
sign_in_route(register_path: "/register", reset_path: "/reset")
|
||||
sign_out_route AuthController
|
||||
auth_routes_for Example.Accounts.User, to: AuthController
|
||||
sign_in_route(register_path: "/register", reset_path: "/reset", auth_routes_prefix: "/auth")
|
||||
reset_route []
|
||||
|
||||
# <-- add these lines
|
||||
end
|
||||
|
||||
|
|
|
@ -18,6 +18,32 @@ defmodule AshAuthentication.Phoenix.Components.Helpers do
|
|||
|> socket.endpoint.config()
|
||||
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 """
|
||||
The LiveView `Socket` contains a refererence to the Phoenix router, and from
|
||||
there we can generate the name of the route helpers module.
|
||||
|
|
|
@ -33,13 +33,14 @@ defmodule AshAuthentication.Phoenix.Components.MagicLink do
|
|||
alias AshAuthentication.{Info, Phoenix.Components.Password.Input, Strategy}
|
||||
alias AshPhoenix.Form
|
||||
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
|
||||
|
||||
@type props :: %{
|
||||
required(:strategy) => AshAuthentication.Strategy.t(),
|
||||
optional(:overrides) => [module],
|
||||
optional(:current_tenant) => String.t()
|
||||
optional(:current_tenant) => String.t(),
|
||||
optional(:auth_routes_prefix) => String.t()
|
||||
}
|
||||
|
||||
@doc false
|
||||
|
@ -58,6 +59,7 @@ defmodule AshAuthentication.Phoenix.Components.MagicLink do
|
|||
|> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end)
|
||||
|> assign_new(:label, fn -> nil end)
|
||||
|> assign_new(:current_tenant, fn -> nil end)
|
||||
|> assign_new(:auth_routes_prefix, fn -> nil end)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
@ -79,12 +81,7 @@ defmodule AshAuthentication.Phoenix.Components.MagicLink do
|
|||
phx-submit="submit"
|
||||
phx-trigger-action={@trigger_action}
|
||||
phx-target={@myself}
|
||||
action={
|
||||
route_helpers(@socket).auth_path(
|
||||
@socket.endpoint,
|
||||
{@subject_name, Strategy.name(@strategy), :request}
|
||||
)
|
||||
}
|
||||
action={auth_path(@socket, @subject_name, @auth_routes_prefix, @strategy, :request)}
|
||||
method="POST"
|
||||
class={override_for(@overrides, :form_class)}
|
||||
>
|
||||
|
|
|
@ -24,13 +24,14 @@ defmodule AshAuthentication.Phoenix.Components.OAuth2 do
|
|||
use AshAuthentication.Phoenix.Web, :live_component
|
||||
alias AshAuthentication.{Info, Strategy}
|
||||
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 PhoenixHTMLHelpers.Form, only: [humanize: 1]
|
||||
|
||||
@type props :: %{
|
||||
required(:strategy) => AshAuthentication.Strategy.t(),
|
||||
optional(:overrides) => [module]
|
||||
optional(:overrides) => [module],
|
||||
optional(:auth_routes_prefix) => String.t()
|
||||
}
|
||||
|
||||
@doc false
|
||||
|
@ -41,16 +42,12 @@ defmodule AshAuthentication.Phoenix.Components.OAuth2 do
|
|||
assigns
|
||||
|> assign(:subject_name, Info.authentication_subject_name!(assigns.strategy.resource))
|
||||
|> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end)
|
||||
|> assign_new(:auth_routes_prefix, fn -> nil end)
|
||||
|
||||
~H"""
|
||||
<div class={override_for(@overrides, :root_class)}>
|
||||
<a
|
||||
href={
|
||||
route_helpers(@socket).auth_path(
|
||||
@socket.endpoint,
|
||||
{@subject_name, Strategy.name(@strategy), :request}
|
||||
)
|
||||
}
|
||||
href={auth_path(@socket, @subject_name, @auth_routes_prefix, @strategy, :request)}
|
||||
class={override_for(@overrides, :link_class)}
|
||||
>
|
||||
<.icon icon={@strategy.icon} overrides={@overrides} />
|
||||
|
|
|
@ -143,6 +143,7 @@ defmodule AshAuthentication.Phoenix.Components.Password do
|
|||
|> assign_new(:reset_path, fn -> nil end)
|
||||
|> assign_new(:register_path, fn -> nil end)
|
||||
|> assign_new(:current_tenant, fn -> nil end)
|
||||
|> assign_new(:auth_routes_prefix, fn -> nil end)
|
||||
|
||||
show =
|
||||
if assigns[:live_action] == :sign_in && is_nil(assigns[:reset_path]) &&
|
||||
|
@ -160,6 +161,7 @@ defmodule AshAuthentication.Phoenix.Components.Password do
|
|||
<.live_component
|
||||
:let={form}
|
||||
module={Password.SignInForm}
|
||||
auth_routes_prefix={@auth_routes_prefix}
|
||||
id={@sign_in_id}
|
||||
strategy={@strategy}
|
||||
label={false}
|
||||
|
@ -204,6 +206,7 @@ defmodule AshAuthentication.Phoenix.Components.Password do
|
|||
<.live_component
|
||||
:let={form}
|
||||
module={Password.RegisterForm}
|
||||
auth_routes_prefix={@auth_routes_prefix}
|
||||
id={@register_id}
|
||||
strategy={@strategy}
|
||||
label={false}
|
||||
|
@ -245,6 +248,7 @@ defmodule AshAuthentication.Phoenix.Components.Password do
|
|||
<.live_component
|
||||
:let={form}
|
||||
module={Password.ResetForm}
|
||||
auth_routes_prefix={@auth_routes_prefix}
|
||||
id={@reset_id}
|
||||
strategy={@strategy}
|
||||
label={false}
|
||||
|
|
|
@ -44,7 +44,8 @@ defmodule AshAuthentication.Phoenix.Components.Password.RegisterForm do
|
|||
required(:strategy) => AshAuthentication.Strategy.t(),
|
||||
optional(:overrides) => [module],
|
||||
optional(:live_action) => :sign_in | :register,
|
||||
optional(:current_tenant) => String.t()
|
||||
optional(:current_tenant) => String.t(),
|
||||
optional(:auth_routes_prefix) => String.t()
|
||||
}
|
||||
|
||||
@doc false
|
||||
|
@ -79,6 +80,7 @@ defmodule AshAuthentication.Phoenix.Components.Password.RegisterForm do
|
|||
|> assign_new(:inner_block, fn -> nil end)
|
||||
|> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end)
|
||||
|> assign_new(:current_tenant, fn -> nil end)
|
||||
|> assign_new(:auth_routes_prefix, fn -> nil end)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
@ -102,12 +104,7 @@ defmodule AshAuthentication.Phoenix.Components.Password.RegisterForm do
|
|||
phx-submit="submit"
|
||||
phx-trigger-action={@trigger_action}
|
||||
phx-target={@myself}
|
||||
action={
|
||||
route_helpers(@socket).auth_path(
|
||||
@socket.endpoint,
|
||||
{@subject_name, Strategy.name(@strategy), :register}
|
||||
)
|
||||
}
|
||||
action={auth_path(@socket, @subject_name, @auth_routes_prefix, @strategy, :register)}
|
||||
method="POST"
|
||||
class={override_for(@overrides, :form_class)}
|
||||
>
|
||||
|
@ -166,10 +163,12 @@ defmodule AshAuthentication.Phoenix.Components.Password.RegisterForm do
|
|||
) do
|
||||
{:ok, user} ->
|
||||
validate_sign_in_token_path =
|
||||
route_helpers(socket).auth_path(
|
||||
socket.endpoint,
|
||||
{socket.assigns.subject_name, Strategy.name(socket.assigns.strategy),
|
||||
:sign_in_with_token},
|
||||
auth_path(
|
||||
socket,
|
||||
socket.assigns.subject_name,
|
||||
socket.assigns.auth_routes_prefix,
|
||||
socket.assigns.strategy,
|
||||
:sign_in_with_token,
|
||||
token: user.__metadata__.token
|
||||
)
|
||||
|
||||
|
|
|
@ -45,7 +45,8 @@ defmodule AshAuthentication.Phoenix.Components.Password.ResetForm do
|
|||
required(:strategy) => AshAuthentication.Strategy.t(),
|
||||
optional(:label) => String.t() | false,
|
||||
optional(:overrides) => [module],
|
||||
optional(:current_tenant) => String.t()
|
||||
optional(:current_tenant) => String.t(),
|
||||
optional(:auth_routes_prefix) => String.t()
|
||||
}
|
||||
|
||||
@doc false
|
||||
|
@ -63,6 +64,7 @@ defmodule AshAuthentication.Phoenix.Components.Password.ResetForm do
|
|||
|> assign_new(:inner_block, fn -> nil end)
|
||||
|> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end)
|
||||
|> assign_new(:current_tenant, fn -> nil end)
|
||||
|> assign_new(:auth_routes_prefix, fn -> nil end)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
@ -85,12 +87,7 @@ defmodule AshAuthentication.Phoenix.Components.Password.ResetForm do
|
|||
phx-submit="submit"
|
||||
phx-change="change"
|
||||
phx-target={@myself}
|
||||
action={
|
||||
route_helpers(@socket).auth_path(
|
||||
@socket.endpoint,
|
||||
{@subject_name, Strategy.name(@strategy), :reset_request}
|
||||
)
|
||||
}
|
||||
action={auth_path(@socket, @subject_name, @auth_routes_prefix, @strategy, :reset_request)}
|
||||
method="POST"
|
||||
class={override_for(@overrides, :form_class)}
|
||||
>
|
||||
|
|
|
@ -37,7 +37,7 @@ defmodule AshAuthentication.Phoenix.Components.Password.SignInForm do
|
|||
alias Phoenix.LiveView.{Rendered, Socket}
|
||||
|
||||
import AshAuthentication.Phoenix.Components.Helpers,
|
||||
only: [route_helpers: 1]
|
||||
only: [auth_path: 5, auth_path: 6]
|
||||
|
||||
import PhoenixHTMLHelpers.Form
|
||||
import Slug
|
||||
|
@ -46,7 +46,8 @@ defmodule AshAuthentication.Phoenix.Components.Password.SignInForm do
|
|||
required(:strategy) => AshAuthentication.Strategy.t(),
|
||||
optional(:label) => String.t() | false,
|
||||
optional(:overrides) => [module],
|
||||
optional(:current_tenant) => String.t()
|
||||
optional(:current_tenant) => String.t(),
|
||||
optional(:auth_routes_prefix) => String.t()
|
||||
}
|
||||
|
||||
@doc false
|
||||
|
@ -77,6 +78,7 @@ defmodule AshAuthentication.Phoenix.Components.Password.SignInForm do
|
|||
|> assign_new(:inner_block, fn -> nil end)
|
||||
|> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end)
|
||||
|> assign_new(:current_tenant, fn -> nil end)
|
||||
|> assign_new(:auth_routes_prefix, fn -> nil end)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
@ -98,12 +100,7 @@ defmodule AshAuthentication.Phoenix.Components.Password.SignInForm do
|
|||
phx-submit="submit"
|
||||
phx-trigger-action={@trigger_action}
|
||||
phx-target={@myself}
|
||||
action={
|
||||
route_helpers(@socket).auth_path(
|
||||
@socket.endpoint,
|
||||
{@subject_name, Strategy.name(@strategy), :sign_in}
|
||||
)
|
||||
}
|
||||
action={auth_path(@socket, @subject_name, @auth_routes_prefix, @strategy, :sign_in)}
|
||||
method="POST"
|
||||
class={override_for(@overrides, :form_class)}
|
||||
>
|
||||
|
@ -158,10 +155,12 @@ defmodule AshAuthentication.Phoenix.Components.Password.SignInForm do
|
|||
) do
|
||||
{:ok, user} ->
|
||||
validate_sign_in_token_path =
|
||||
route_helpers(socket).auth_path(
|
||||
socket.endpoint,
|
||||
{socket.assigns.subject_name, Strategy.name(socket.assigns.strategy),
|
||||
:sign_in_with_token},
|
||||
auth_path(
|
||||
socket,
|
||||
socket.assigns.subject_name,
|
||||
socket.assigns.auth_routes_prefix,
|
||||
socket.assigns.strategy,
|
||||
:sign_in_with_token,
|
||||
token: user.__metadata__.token
|
||||
)
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ defmodule AshAuthentication.Phoenix.Components.Reset do
|
|||
socket
|
||||
|> assign(strategies: strategies)
|
||||
|> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end)
|
||||
|> assign_new(:auth_routes_prefix, fn -> nil end)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
@ -71,6 +72,7 @@ defmodule AshAuthentication.Phoenix.Components.Reset do
|
|||
<div class={override_for(@overrides, :strategy_class)}>
|
||||
<.live_component
|
||||
module={Components.Reset.Form}
|
||||
auth_routes_prefix={@auth_routes_prefix}
|
||||
strategy={strategy}
|
||||
token={@token}
|
||||
id="reset-form"
|
||||
|
|
|
@ -38,7 +38,7 @@ defmodule AshAuthentication.Phoenix.Components.Reset.Form do
|
|||
alias AshAuthentication.{Info, Phoenix.Components.Password.Input, Strategy}
|
||||
alias AshPhoenix.Form
|
||||
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 Slug
|
||||
|
||||
|
@ -47,7 +47,8 @@ defmodule AshAuthentication.Phoenix.Components.Reset.Form do
|
|||
required(:strategy) => AshAuthentication.Strategy.t(),
|
||||
required(:token) => String.t(),
|
||||
optional(:label) => String.t() | false,
|
||||
optional(:overrides) => [module]
|
||||
optional(:overrides) => [module],
|
||||
optional(:auth_routes_prefix) => String.t()
|
||||
}
|
||||
|
||||
@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(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end)
|
||||
|> assign_new(:auth_routes_prefix, fn -> nil end)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
@ -103,12 +105,7 @@ defmodule AshAuthentication.Phoenix.Components.Reset.Form do
|
|||
phx-submit="submit"
|
||||
phx-trigger-action={@trigger_action}
|
||||
phx-target={@myself}
|
||||
action={
|
||||
route_helpers(@socket).auth_path(
|
||||
@socket.endpoint,
|
||||
{@subject_name, Strategy.name(@strategy), :reset}
|
||||
)
|
||||
}
|
||||
action={auth_path(@socket, @subject_name, @auth_routes_prefix, @strategy, :reset)}
|
||||
method="POST"
|
||||
class={override_for(@overrides, :form_class)}
|
||||
>
|
||||
|
|
|
@ -81,6 +81,7 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do
|
|||
|> assign_new(:reset_path, fn -> nil end)
|
||||
|> assign_new(:register_path, fn -> nil end)
|
||||
|> assign_new(:current_tenant, fn -> nil end)
|
||||
|> assign_new(:auth_routes_prefix, fn -> nil end)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
@ -103,6 +104,7 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do
|
|||
live_action={@live_action}
|
||||
strategy={strategy}
|
||||
path={@path}
|
||||
auth_routes_prefix={@auth_routes_prefix}
|
||||
reset_path={@reset_path}
|
||||
register_path={@register_path}
|
||||
overrides={@overrides}
|
||||
|
@ -125,6 +127,7 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do
|
|||
component={component_for_strategy(strategy)}
|
||||
live_action={@live_action}
|
||||
strategy={strategy}
|
||||
auth_routes_prefix={@auth_routes_prefix}
|
||||
path={@path}
|
||||
reset_path={@reset_path}
|
||||
register_path={@register_path}
|
||||
|
@ -145,6 +148,7 @@ defmodule AshAuthentication.Phoenix.Components.SignIn do
|
|||
module={@component}
|
||||
id={strategy_id(@strategy)}
|
||||
strategy={@strategy}
|
||||
auth_routes_prefix={@auth_routes_prefix}
|
||||
path={@path}
|
||||
reset_path={@reset_path}
|
||||
register_path={@register_path}
|
||||
|
|
|
@ -37,6 +37,13 @@ defmodule AshAuthentication.Phoenix.LiveSession do
|
|||
defmacro ash_authentication_live_session(session_name \\ :ash_authentication, opts \\ [],
|
||||
do: block
|
||||
) do
|
||||
opts =
|
||||
if Macro.quoted_literal?(opts) do
|
||||
Macro.prewalk(opts, &expand_alias(&1, __CALLER__))
|
||||
else
|
||||
opts
|
||||
end
|
||||
|
||||
quote do
|
||||
on_mount = [LiveSession]
|
||||
|
||||
|
@ -64,6 +71,11 @@ defmodule AshAuthentication.Phoenix.LiveSession do
|
|||
end
|
||||
end
|
||||
|
||||
defp expand_alias({:__aliases__, _, _} = alias, env),
|
||||
do: Macro.expand(alias, %{env | function: {:mount, 3}})
|
||||
|
||||
defp expand_alias(other, _env), do: other
|
||||
|
||||
@doc """
|
||||
Inspects the incoming session for any subject_name -> subject values and loads
|
||||
them into the socket's assigns.
|
||||
|
|
|
@ -119,6 +119,60 @@ defmodule AshAuthentication.Phoenix.Router do
|
|||
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 """
|
||||
Generates a generic, white-label sign-in page using LiveView and the
|
||||
components in `AshAuthentication.Phoenix.Components`.
|
||||
|
@ -128,12 +182,15 @@ defmodule AshAuthentication.Phoenix.Router do
|
|||
Available options are:
|
||||
|
||||
* `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.
|
||||
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.
|
||||
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
|
||||
`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`.
|
||||
* `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
|
||||
|
@ -160,6 +217,7 @@ defmodule AshAuthentication.Phoenix.Router do
|
|||
{on_mount, opts} = Keyword.pop(opts, :on_mount)
|
||||
{reset_path, opts} = Keyword.pop(opts, :reset_path)
|
||||
{register_path, opts} = Keyword.pop(opts, :register_path)
|
||||
{auth_routes_prefix, opts} = Keyword.pop(opts, :auth_routes_prefix)
|
||||
|
||||
{overrides, opts} =
|
||||
Keyword.pop(opts, :overrides, [AshAuthentication.Phoenix.Overrides.Default])
|
||||
|
@ -174,7 +232,7 @@ defmodule AshAuthentication.Phoenix.Router do
|
|||
|
||||
on_mount =
|
||||
[
|
||||
AshAuthenticationPhoenix.Router.OnLiveViewMount,
|
||||
AshAuthentication.Phoenix.Router.OnLiveViewMount,
|
||||
AshAuthentication.Phoenix.LiveSession | unquote(on_mount || [])
|
||||
]
|
||||
|> Enum.uniq_by(fn
|
||||
|
@ -182,12 +240,30 @@ defmodule AshAuthentication.Phoenix.Router do
|
|||
mod -> mod
|
||||
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 = [
|
||||
session:
|
||||
{AshAuthentication.Phoenix.Router, :generate_session,
|
||||
[
|
||||
%{
|
||||
"overrides" => unquote(overrides),
|
||||
"auth_routes_prefix" => unquote(auth_routes_prefix),
|
||||
"otp_app" => unquote(otp_app),
|
||||
"path" => Phoenix.Router.scoped_path(__MODULE__, unquote(path)),
|
||||
"reset_path" =>
|
||||
|
@ -294,7 +370,7 @@ defmodule AshAuthentication.Phoenix.Router do
|
|||
|
||||
on_mount =
|
||||
[
|
||||
AshAuthenticationPhoenix.Router.OnLiveViewMount,
|
||||
AshAuthentication.Phoenix.Router.OnLiveViewMount,
|
||||
AshAuthentication.Phoenix.LiveSession | unquote(on_mount || [])
|
||||
]
|
||||
|> Enum.uniq_by(fn
|
||||
|
|
103
lib/ash_authentication_phoenix/router/console_formatter.ex
Normal file
103
lib/ash_authentication_phoenix/router/console_formatter.ex
Normal 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
|
|
@ -1,4 +1,4 @@
|
|||
defmodule AshAuthenticationPhoenix.Router.OnLiveViewMount do
|
||||
defmodule AshAuthentication.Phoenix.Router.OnLiveViewMount do
|
||||
@moduledoc false
|
||||
import Phoenix.Component
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ defmodule AshAuthentication.Phoenix.SignInLive do
|
|||
|> assign(:reset_path, session["reset_path"])
|
||||
|> assign(:register_path, session["register_path"])
|
||||
|> assign(:current_tenant, session["tenant"])
|
||||
|> assign(:auth_routes_prefix, session["auth_routes_prefix"])
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
@ -57,6 +58,7 @@ defmodule AshAuthentication.Phoenix.SignInLive do
|
|||
otp_app={@otp_app}
|
||||
live_action={@live_action}
|
||||
path={@path}
|
||||
auth_routes_prefix={@auth_routes_prefix}
|
||||
reset_path={@reset_path}
|
||||
register_path={@register_path}
|
||||
id={override_for(@overrides, :sign_in_id, "sign-in")}
|
||||
|
|
81
lib/ash_authentication_phoenix/strategy_router.ex
Normal file
81
lib/ash_authentication_phoenix/strategy_router.ex
Normal 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
|
143
lib/mix/tasks/ash_authentication.phoenix.routes.ex
Normal file
143
lib/mix/tasks/ash_authentication.phoenix.routes.ex
Normal 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
|
3
mix.exs
3
mix.exs
|
@ -132,7 +132,8 @@ defmodule AshAuthentication.Phoenix.MixProject do
|
|||
{:mimic, "~> 1.7", only: [:dev, :test]},
|
||||
{:mix_audit, "~> 2.1", 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
|
||||
|
||||
|
|
1
mix.lock
1
mix.lock
|
@ -25,6 +25,7 @@
|
|||
"faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"},
|
||||
"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"},
|
||||
"floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
|
||||
"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"},
|
||||
"glob_ex": {:hex, :glob_ex, "0.1.8", "f7ef872877ca2ae7a792ab1f9ff73d9c16bf46ecb028603a8a3c5283016adc07", [:mix], [], "hexpm", "9e39d01729419a60a937c9260a43981440c43aa4cadd1fa6672fecd58241c464"},
|
||||
|
|
26
test/router_test.exs
Normal file
26
test/router_test.exs
Normal 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
18
test/sign_in_test.exs
Normal 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
|
32
test/support/auth_controller.ex
Normal file
32
test/support/auth_controller.ex
Normal 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
105
test/support/phoenix.ex
Normal 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
|
|
@ -1 +1,3 @@
|
|||
ExUnit.start()
|
||||
|
||||
AshAuthentication.Phoenix.Test.Endpoint.start_link()
|
||||
|
|
Loading…
Reference in a new issue