mirror of
https://github.com/team-alembic/ash_authentication.git
synced 2024-09-19 12:52:55 +12:00
improvement: Allow the strategy name to be passed for password validations and changes. (#102)
After #89 was merged folks were no longer able to use `AshAuthentication.Strategy.Password.HashPasswordChange` and `AshAuthentication.Strategy.Password.PasswordConfirmationValidation` in their own actions. This change fixes this issue by allowing the name of the strategy to be passed in in the changeset context.
This commit is contained in:
parent
25610d01d2
commit
5ba5e163f3
5 changed files with 190 additions and 5 deletions
|
@ -4,6 +4,27 @@ defmodule AshAuthentication.Strategy.Password.HashPasswordChange do
|
|||
|
||||
Uses the configured `AshAuthentication.HashProvider` to generate a hash of the
|
||||
user's password input and store it in the changeset.
|
||||
|
||||
You can use this change in your actions where you want to change the user's
|
||||
password. If you're not using one of the actions generated by the password
|
||||
strategy then you'll need to manually pass the strategy name in the changeset
|
||||
context. Eg:
|
||||
|
||||
```elixir
|
||||
Changeset.new(user, %{})
|
||||
|> Changeset.set_context(%{strategy_name: :password})
|
||||
|> Changeset.for_update(:update, params)
|
||||
|> Accounts.update()
|
||||
```
|
||||
|
||||
or by adding it statically to your action definition:
|
||||
|
||||
```elixir
|
||||
update :change_password do
|
||||
change set_context(%{strategy_name: :password})
|
||||
change AshAuthentication.Strategy.Password.HashPasswordChange
|
||||
end
|
||||
```
|
||||
"""
|
||||
|
||||
use Ash.Resource.Change
|
||||
|
@ -13,10 +34,10 @@ defmodule AshAuthentication.Strategy.Password.HashPasswordChange do
|
|||
@doc false
|
||||
@impl true
|
||||
@spec change(Changeset.t(), keyword, Change.context()) :: Changeset.t()
|
||||
def change(changeset, _opts, _) do
|
||||
def change(changeset, _opts, context) do
|
||||
changeset
|
||||
|> Changeset.before_action(fn changeset ->
|
||||
with {:ok, strategy} <- Info.strategy_for_action(changeset.resource, changeset.action.name),
|
||||
with {:ok, strategy} <- find_strategy(changeset, context),
|
||||
value when is_binary(value) <-
|
||||
Changeset.get_argument(changeset, strategy.password_field),
|
||||
{:ok, hash} <- strategy.hash_provider.hash(value) do
|
||||
|
@ -30,4 +51,18 @@ defmodule AshAuthentication.Strategy.Password.HashPasswordChange do
|
|||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp find_strategy(changeset, context) do
|
||||
with :error <- Info.strategy_for_action(changeset.resource, changeset.action.name),
|
||||
:error <- Map.fetch(changeset.context, :strategy_name),
|
||||
:error <- Map.fetch(context, :strategy_name) do
|
||||
:error
|
||||
else
|
||||
{:ok, strategy_name} when is_atom(strategy_name) ->
|
||||
Info.strategy(changeset.resource, strategy_name)
|
||||
|
||||
{:ok, strategy} ->
|
||||
{:ok, strategy}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,29 @@ defmodule AshAuthentication.Strategy.Password.PasswordConfirmationValidation do
|
|||
@moduledoc """
|
||||
Validate that the password and password confirmation match.
|
||||
|
||||
This check is only performed when the `confirmation_required?` DSL option is set to `true`.
|
||||
This check is only performed when the `confirmation_required?` DSL option is
|
||||
set to `true`.
|
||||
|
||||
You can use this validation in your own actions where you want to validate
|
||||
that the password and the password confirmation arguments match. If you're
|
||||
not using one of the actions generated by the password strategy then you'll
|
||||
need to manually pass the strategy name in the changeset context. Eg:
|
||||
|
||||
```elixir
|
||||
Changeset.new(user, %{})
|
||||
|> Changeset.set_context(%{strategy_name: :password})
|
||||
|> Changeset.for_update(:change_password, params)
|
||||
|> Accounts.update()
|
||||
```
|
||||
|
||||
or by adding it statically in your action definition:
|
||||
|
||||
```elixir
|
||||
update :change_password do
|
||||
change set_context(%{strategy_name: :password})
|
||||
change AshAuthentication.Strategy.Password.HashPasswordChange
|
||||
end
|
||||
```
|
||||
"""
|
||||
|
||||
use Ash.Resource.Validation
|
||||
|
@ -15,8 +37,8 @@ defmodule AshAuthentication.Strategy.Password.PasswordConfirmationValidation do
|
|||
"""
|
||||
@impl true
|
||||
@spec validate(Changeset.t(), keyword) :: :ok | {:error, String.t() | Exception.t()}
|
||||
def validate(changeset, _) do
|
||||
case Info.strategy_for_action(changeset.resource, changeset.action.name) do
|
||||
def validate(changeset, _options) do
|
||||
case find_strategy(changeset) do
|
||||
{:ok, %{confirmation_required?: true} = strategy} ->
|
||||
validate_password_confirmation(changeset, strategy)
|
||||
|
||||
|
@ -45,4 +67,17 @@ defmodule AshAuthentication.Strategy.Password.PasswordConfirmationValidation do
|
|||
)}
|
||||
end
|
||||
end
|
||||
|
||||
defp find_strategy(changeset) do
|
||||
with :error <- Info.strategy_for_action(changeset.resource, changeset.action.name),
|
||||
:error <- Map.fetch(changeset.context, :strategy_name) do
|
||||
:error
|
||||
else
|
||||
{:ok, strategy_name} when is_atom(strategy_name) ->
|
||||
Info.strategy(changeset.resource, strategy_name)
|
||||
|
||||
{:ok, strategy} ->
|
||||
{:ok, strategy}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
defmodule AshAuthentication.Strategy.Password.HashPasswordChangeTest do
|
||||
use DataCase, async: true
|
||||
alias Ash.Changeset
|
||||
alias AshAuthentication.{Info, Strategy.Password.HashPasswordChange}
|
||||
|
||||
describe "change/3" do
|
||||
test "when the action is associated with a strategy, it can hash the password" do
|
||||
strategy = Info.strategy!(Example.User, :password)
|
||||
username = username()
|
||||
password = password()
|
||||
|
||||
attrs = %{
|
||||
to_string(strategy.identity_field) => username,
|
||||
to_string(strategy.password_field) => password,
|
||||
to_string(strategy.password_confirmation_field) => password
|
||||
}
|
||||
|
||||
{:ok, _user, _changeset, _} =
|
||||
Changeset.new(strategy.resource, %{})
|
||||
|> Changeset.for_create(strategy.register_action_name, attrs)
|
||||
|> HashPasswordChange.change([], %{})
|
||||
|> Changeset.with_hooks(fn changeset ->
|
||||
assert strategy.hash_provider.valid?(password, changeset.attributes.hashed_password)
|
||||
|
||||
{:ok, struct(strategy.resource)}
|
||||
end)
|
||||
end
|
||||
|
||||
test "when the action is not associated with a strategy, but is provided a strategy name in the changeset context, it can hash the password" do
|
||||
strategy = Info.strategy!(Example.User, :password)
|
||||
user = build_user()
|
||||
password = password()
|
||||
|
||||
attrs = %{
|
||||
to_string(strategy.password_field) => password,
|
||||
to_string(strategy.password_confirmation_field) => password
|
||||
}
|
||||
|
||||
{:ok, _user, _changeset, _} =
|
||||
Changeset.new(user, %{})
|
||||
|> Changeset.set_context(%{strategy_name: strategy.name})
|
||||
|> Changeset.for_update(:update, attrs)
|
||||
|> HashPasswordChange.change([], %{})
|
||||
|> Changeset.with_hooks(fn changeset ->
|
||||
assert strategy.hash_provider.valid?(password, changeset.attributes.hashed_password)
|
||||
|
||||
{:ok, struct(strategy.resource)}
|
||||
end)
|
||||
end
|
||||
|
||||
test "when the action is not associated with a strategy, but is provided a strategy name in the action cotnext, it can hash the password" do
|
||||
strategy = Info.strategy!(Example.User, :password)
|
||||
user = build_user()
|
||||
password = password()
|
||||
|
||||
attrs = %{
|
||||
to_string(strategy.password_field) => password,
|
||||
to_string(strategy.password_confirmation_field) => password
|
||||
}
|
||||
|
||||
{:ok, _user, _changeset, _} =
|
||||
Changeset.new(user, %{})
|
||||
|> Changeset.for_update(:update, attrs)
|
||||
|> HashPasswordChange.change([], %{strategy_name: strategy.name})
|
||||
|> Changeset.with_hooks(fn changeset ->
|
||||
assert strategy.hash_provider.valid?(password, changeset.attributes.hashed_password)
|
||||
|
||||
{:ok, struct(strategy.resource)}
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
defmodule AshAuthentication.Strategy.Password.PasswordConfirmationValidationTest do
|
||||
use DataCase, async: true
|
||||
alias Ash.{Changeset, Error.Changes.InvalidArgument}
|
||||
alias AshAuthentication.{Info, Strategy.Password.PasswordConfirmationValidation}
|
||||
|
||||
describe "validate/2" do
|
||||
test "when the action is associated with a strategy, it can validate the password confirmation" do
|
||||
strategy = Info.strategy!(Example.User, :password)
|
||||
username = username()
|
||||
password = password()
|
||||
|
||||
attrs = %{
|
||||
to_string(strategy.identity_field) => username,
|
||||
to_string(strategy.password_field) => password,
|
||||
to_string(strategy.password_confirmation_field) => password <> "123"
|
||||
}
|
||||
|
||||
assert {:error, %InvalidArgument{field: :password_confirmation}} =
|
||||
Changeset.new(strategy.resource, %{})
|
||||
|> Changeset.for_create(strategy.register_action_name, attrs)
|
||||
|> PasswordConfirmationValidation.validate([])
|
||||
end
|
||||
end
|
||||
|
||||
test "when the action is not associated with a strategy, but is provided a strategy name in the changeset context" do
|
||||
strategy = Info.strategy!(Example.User, :password)
|
||||
user = build_user()
|
||||
password = password()
|
||||
|
||||
attrs = %{
|
||||
to_string(strategy.password_field) => password,
|
||||
to_string(strategy.password_confirmation_field) => password <> "123"
|
||||
}
|
||||
|
||||
assert {:error, %InvalidArgument{field: :password_confirmation}} =
|
||||
Changeset.new(user, %{})
|
||||
|> Changeset.set_context(%{strategy_name: strategy.name})
|
||||
|> Changeset.for_update(:update, attrs)
|
||||
|> PasswordConfirmationValidation.validate([])
|
||||
end
|
||||
end
|
|
@ -43,6 +43,8 @@ defmodule Example.User do
|
|||
end
|
||||
|
||||
update :update do
|
||||
argument :password, :string, allow_nil?: true, sensitive?: true
|
||||
argument :password_confirmation, :string, allow_nil?: true, sensitive?: true
|
||||
primary? true
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue