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

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

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

217
.credo.exs Normal file
View file

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

View file

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

View file

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

View file

@ -14,11 +14,11 @@ defmodule DevWeb.HomePageLive do
<%= if @current_user do %>
<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 %>

View file

@ -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

View file

@ -73,21 +73,6 @@ We can make our life easier and the code more consistent by adding formatters to
]
```
### Phoenix 1.7 compatibility
For Phoenix 1.7 we need to change `helpers: false` to `helpers: true` in the router section:
**lib/example_web.ex**
```elixir
defmodule ExampleWeb do
# ...
def router do
quote do
use Phoenix.Router, helpers: true # <-- Change this line
# ...
```
### Tailwind
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

View file

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

View file

@ -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)}
>

View file

@ -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} />

View file

@ -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}

View file

@ -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
)

View file

@ -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)}
>

View file

@ -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
)

View file

@ -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"

View file

@ -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)}
>

View file

@ -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}

View file

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

View file

@ -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

View file

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

View file

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

View file

@ -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")}

View file

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

View file

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

View file

@ -132,7 +132,8 @@ defmodule AshAuthentication.Phoenix.MixProject do
{:mimic, "~> 1.7", only: [:dev, :test]},
{: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

View file

@ -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
View file

@ -0,0 +1,26 @@
defmodule AshAuthentication.Phoenix.RouterTest do
@moduledoc false
use ExUnit.Case
test "sign_in_routes adds a route according to its scope" do
route =
AshAuthentication.Phoenix.Test.Router
|> Phoenix.Router.routes()
|> Enum.find(&(&1.path == "/sign-in"))
{_, _, _, %{extra: %{session: session}}} = route.metadata.phoenix_live_view
assert session ==
{AshAuthentication.Phoenix.Router, :generate_session,
[
%{
"auth_routes_prefix" => "/auth",
"otp_app" => nil,
"overrides" => [AshAuthentication.Phoenix.Overrides.Default],
"path" => "/sign-in",
"register_path" => "/register",
"reset_path" => "/reset"
}
]}
end
end

18
test/sign_in_test.exs Normal file
View file

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

View file

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

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

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

View file

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