mirror of
https://github.com/team-alembic/ash_authentication_phoenix.git
synced 2024-09-19 21:13:52 +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: 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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 %>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
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
|
@moduledoc false
|
||||||
import Phoenix.Component
|
import Phoenix.Component
|
||||||
|
|
||||||
|
|
|
@ -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")}
|
||||||
|
|
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]},
|
{: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
|
||||||
|
|
||||||
|
|
1
mix.lock
1
mix.lock
|
@ -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
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()
|
ExUnit.start()
|
||||||
|
|
||||||
|
AshAuthentication.Phoenix.Test.Endpoint.start_link()
|
||||||
|
|
Loading…
Reference in a new issue