mirror of
https://github.com/team-alembic/ash_authentication.git
synced 2024-09-19 21:03:23 +12:00
improvement: add select_for_senders (#189)
* improvement: add select_for_senders fix: select `hashed_password` on sign in preparation * improvement: include metadata declaration on register action * chore: fix typo
This commit is contained in:
parent
ca3dac3878
commit
a2bba519c0
9 changed files with 106 additions and 1 deletions
|
@ -124,6 +124,7 @@ defmodule AshAuthentication do
|
||||||
),
|
),
|
||||||
transformers: [
|
transformers: [
|
||||||
AshAuthentication.Transformer,
|
AshAuthentication.Transformer,
|
||||||
|
AshAuthentication.Transformer.SetSelectForSenders,
|
||||||
AshAuthentication.Strategy.Custom.Transformer
|
AshAuthentication.Strategy.Custom.Transformer
|
||||||
],
|
],
|
||||||
verifiers: [
|
verifiers: [
|
||||||
|
|
|
@ -80,6 +80,16 @@ defmodule AshAuthentication.Dsl do
|
||||||
action doesn't exist, one will be generated for you.
|
action doesn't exist, one will be generated for you.
|
||||||
""",
|
""",
|
||||||
default: :get_by_subject
|
default: :get_by_subject
|
||||||
|
],
|
||||||
|
select_for_senders: [
|
||||||
|
type: {:list, :atom},
|
||||||
|
doc: """
|
||||||
|
A list of fields that we will ensure are selected whenever a sender will be invoked.
|
||||||
|
This is useful if using something like `ash_graphql` which by default only selects
|
||||||
|
what fields appear in the query, and if you are exposing these actions that way.
|
||||||
|
Defaults to `[:email]` if there is an `:email` attribute on the resource, and `[]`
|
||||||
|
otherwise.
|
||||||
|
"""
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
sections: [
|
sections: [
|
||||||
|
|
|
@ -24,9 +24,13 @@ defmodule AshAuthentication.Strategy.MagicLink.RequestPreparation do
|
||||||
|
|
||||||
identity_field = strategy.identity_field
|
identity_field = strategy.identity_field
|
||||||
identity = Query.get_argument(query, identity_field)
|
identity = Query.get_argument(query, identity_field)
|
||||||
|
select_for_senders = Info.authentication_select_for_senders!(query.resource)
|
||||||
|
|
||||||
query
|
query
|
||||||
|> Query.filter(ref(^identity_field) == ^identity)
|
|> Query.filter(ref(^identity_field) == ^identity)
|
||||||
|
|> Query.before_action(fn query ->
|
||||||
|
Ash.Query.ensure_selected(query, select_for_senders)
|
||||||
|
end)
|
||||||
|> Query.after_action(&after_action(&1, &2, strategy))
|
|> Query.after_action(&after_action(&1, &2, strategy))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -25,9 +25,13 @@ defmodule AshAuthentication.Strategy.Password.RequestPasswordResetPreparation do
|
||||||
if Enum.any?(strategy.resettable) do
|
if Enum.any?(strategy.resettable) do
|
||||||
identity_field = strategy.identity_field
|
identity_field = strategy.identity_field
|
||||||
identity = Query.get_argument(query, identity_field)
|
identity = Query.get_argument(query, identity_field)
|
||||||
|
select_for_senders = Info.authentication_select_for_senders!(query.resource)
|
||||||
|
|
||||||
query
|
query
|
||||||
|> Query.filter(ref(^identity_field) == ^identity)
|
|> Query.filter(ref(^identity_field) == ^identity)
|
||||||
|
|> Query.before_action(fn query ->
|
||||||
|
Ash.Query.ensure_selected(query, select_for_senders)
|
||||||
|
end)
|
||||||
|> Query.after_action(&after_action(&1, &2, strategy))
|
|> Query.after_action(&after_action(&1, &2, strategy))
|
||||||
else
|
else
|
||||||
query
|
query
|
||||||
|
|
|
@ -27,6 +27,9 @@ defmodule AshAuthentication.Strategy.Password.SignInPreparation do
|
||||||
|
|
||||||
query
|
query
|
||||||
|> Query.filter(ref(^identity_field) == ^identity)
|
|> Query.filter(ref(^identity_field) == ^identity)
|
||||||
|
|> Query.before_action(fn query ->
|
||||||
|
Ash.Query.ensure_selected(query, [strategy.hashed_password_field])
|
||||||
|
end)
|
||||||
|> Query.after_action(fn
|
|> Query.after_action(fn
|
||||||
query, [record] when is_binary(:erlang.map_get(strategy.hashed_password_field, record)) ->
|
query, [record] when is_binary(:erlang.map_get(strategy.hashed_password_field, record)) ->
|
||||||
password = Query.get_argument(query, strategy.password_field)
|
password = Query.get_argument(query, strategy.password_field)
|
||||||
|
|
|
@ -85,7 +85,7 @@ defmodule AshAuthentication.Strategy.Password.Transformer do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_register_action(_dsl_state, strategy) do
|
defp build_register_action(dsl_state, strategy) do
|
||||||
password_opts = [
|
password_opts = [
|
||||||
type: Type.String,
|
type: Type.String,
|
||||||
allow_nil?: false,
|
allow_nil?: false,
|
||||||
|
@ -129,10 +129,24 @@ defmodule AshAuthentication.Strategy.Password.Transformer do
|
||||||
)
|
)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
metadata =
|
||||||
|
if AshAuthentication.Info.authentication_tokens_enabled?(dsl_state) do
|
||||||
|
[
|
||||||
|
Transformer.build_entity!(Resource.Dsl, [:actions, :create], :metadata,
|
||||||
|
name: :token,
|
||||||
|
type: :string,
|
||||||
|
allow_nil?: false
|
||||||
|
)
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
Transformer.build_entity(Resource.Dsl, [:actions], :create,
|
Transformer.build_entity(Resource.Dsl, [:actions], :create,
|
||||||
name: strategy.register_action_name,
|
name: strategy.register_action_name,
|
||||||
arguments: arguments,
|
arguments: arguments,
|
||||||
changes: changes,
|
changes: changes,
|
||||||
|
metadata: metadata,
|
||||||
allow_nil_input: [strategy.hashed_password_field]
|
allow_nil_input: [strategy.hashed_password_field]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
34
lib/ash_authentication/transformer/set_select_for_senders.ex
Normal file
34
lib/ash_authentication/transformer/set_select_for_senders.ex
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
defmodule AshAuthentication.Transformer.SetSelectForSenders do
|
||||||
|
@moduledoc """
|
||||||
|
Sets the `select_for_senders` options to its default value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Spark.Dsl.Transformer
|
||||||
|
alias AshAuthentication.Info
|
||||||
|
alias Spark.Dsl.Transformer
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
@impl true
|
||||||
|
@spec after?(any) :: boolean()
|
||||||
|
def after?(_), do: true
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def transform(dsl_state) do
|
||||||
|
dsl_state
|
||||||
|
|> Info.authentication_select_for_senders()
|
||||||
|
|> case do
|
||||||
|
:error ->
|
||||||
|
if Ash.Resource.Info.attribute(dsl_state, :email) do
|
||||||
|
{:ok,
|
||||||
|
Transformer.set_option(dsl_state, [:authentication], :select_for_senders, [
|
||||||
|
:email
|
||||||
|
])}
|
||||||
|
else
|
||||||
|
{:ok, Transformer.set_option(dsl_state, [:authentication], :select_for_senders, [])}
|
||||||
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:ok, dsl_state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -129,6 +129,39 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
|
||||||
assert log =~ ~r/password reset request for user #{user.username}/i
|
assert log =~ ~r/password reset request for user #{user.username}/i
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it selects required fields for senders using `select_for_senders`" do
|
||||||
|
user = build_user()
|
||||||
|
{:ok, strategy} = Info.strategy(Example.User, :password)
|
||||||
|
|
||||||
|
log =
|
||||||
|
capture_log(fn ->
|
||||||
|
params = %{"username" => user.username}
|
||||||
|
options = []
|
||||||
|
api = Info.authentication_api!(strategy.resource)
|
||||||
|
resettable = strategy.resettable |> Enum.at(0)
|
||||||
|
|
||||||
|
result =
|
||||||
|
strategy.resource
|
||||||
|
|> Ash.Query.new()
|
||||||
|
|> Ash.Query.set_context(%{
|
||||||
|
private: %{
|
||||||
|
ash_authentication?: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|> Ash.Query.for_read(resettable.request_password_reset_action_name, params)
|
||||||
|
|> Ash.Query.select([])
|
||||||
|
|> api.read(options)
|
||||||
|
|> case do
|
||||||
|
{:ok, _} -> :ok
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
|
|
||||||
|
assert result == :ok
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert log =~ ~r/password reset request for user #{user.username}/i
|
||||||
|
end
|
||||||
|
|
||||||
test "it doesn't generate a reset token when no matching user exists and the strategy is resettable" do
|
test "it doesn't generate a reset token when no matching user exists and the strategy is resettable" do
|
||||||
{:ok, strategy} = Info.strategy(Example.User, :password)
|
{:ok, strategy} = Info.strategy(Example.User, :password)
|
||||||
|
|
||||||
|
|
|
@ -130,6 +130,8 @@ defmodule Example.User do
|
||||||
authentication do
|
authentication do
|
||||||
api Example
|
api Example
|
||||||
|
|
||||||
|
select_for_senders([:username])
|
||||||
|
|
||||||
tokens do
|
tokens do
|
||||||
enabled? true
|
enabled? true
|
||||||
store_all_tokens? true
|
store_all_tokens? true
|
||||||
|
|
Loading…
Reference in a new issue