Hello,
I need Help!
I am totally new with Ash and I want to do a login action with a password verification,
Should I do an action or a function in my module ?
Hello,
I need Help!
I am totally new with Ash and I want to do a login action with a password verification,
Should I do an action or a function in my module ?
There are numerous examples in the ash documentation on defining custom actions, please take a look at those. As for authentication specifically, see README — ash_authentication v4.4.1
Yes for Ash authentication, but for now I like to understand what I am I doing for learning purpose.
So I made user ressource and a custom register function with Argon2 and it’s work fine.
My question is about login, should I make a custom read action?
For now I did something dirty but working :
defmodule VsAppWeb.Accounts.User.UserLoginLive do
use VsAppWeb, :live_view
alias AshPhoenix.Form
alias VsApp.Accounts
def mount(_params, _session, socket) do
# Créer le formulaire de login
form = Accounts.User.form_to_login() |> to_form()
{:ok, assign(socket, form: form, error_message: nil)}
end
def render(assigns) do
~H"""
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<.form for={@form} phx-change="validate" phx-submit="login" class="space-y-6">
<div>
<.input field={@form[:email]} type="email" required label="Email" />
</div>
<div>
<.input field={@form[:password]} type="password" required label="Password" />
</div>
<%= if @error_message do %>
<div class="text-red-600 text-sm" role="alert">
{@error_message}
</div>
<% end %>
<div>
<.button phx-disable-with="Signing in..." class="w-full">
Sign in
</.button>
</div>
</.form>
</div>
</div>
"""
end
def handle_event("validate", %{"form" => form_params}, socket) do
form = socket.assigns.form
form =
form
|> Form.validate(form_params)
{:noreply, assign(socket, form: form)}
end
def handle_event("login", %{"form" => %{"email" => email, "password" => password}}, socket) do
case Accounts.Verifier.authenticate_user(email, password) do
{:ok, _} ->
{:noreply,
socket
|> clear_flash()
|> assign(error_message: nil)
|> redirect(to: "/")}
{:error, :invalid_credentials} ->
{:noreply,
socket
|> assign(error_message: "Invalid email or password")}
end
end
end
Again, I don’t want to use Ash_Authentication for now, this is the way I learn
Understood. Your original question had very little context. If you clarify more about your question up front you’ll get clearer answers in general.
In AshAuthentication what we do is make a read action with a preparation that adds an after action hook with Ash.Query.after_action
if a user is returned then we check the password. If it doesn’t match then we set the result to {:ok, []}
otherwise we return the result as is. When no users are returned we do a dummy password check to protect against timing attacks.
With that read action, if you pass in the username and password (which will be an argument) and a user is returned, you know that it’s safe to log them in.
Yes, sorry about the context, I will try to be more explicit next time
defmodule VsApp.Accounts.Validations.ValidLogin do
@moduledoc """
Login validation preparation
"""
use Ash.Resource.Preparation
require Logger
require Ash.Query
@impl true
def prepare(query, _opts, _context) do
password = Ash.Query.get_argument(query, :password)
email = Ash.Query.get_argument(query, :email)
query
|> Ash.Query.filter(email == ^email)
|> Ash.Query.load([:hashed_password])
|> Ash.Query.after_action(fn _, user ->
case user do
[user] ->
if Argon2.verify_pass(password, user.hashed_password) do
{:ok, [user]}
else
{:ok, []}
end
[] ->
Argon2.no_user_verify()
{:ok, []}
_ ->
{:ok, []}
end
end)
end
end
This one work for login, but now I try to add the validation like the preparation documentation and I don’t see any error in my form.
Please, Can you give me a validation example in the preparation ?
If you want to see an error in your form, return an error with a field
on it
prepare fn query, _ ->
Ash.Query.add_error(query, field: :email, message: "is invalid")
end
I love the work you did and I am sure for “normal” crud operation it’s so powerful.
But for a simple read action, it’s too much frustration…
If I do Ash.Query.add_error(query, field: :email, message: "is invalid")
:
[warning] Unhandled error in form submission for VsApp.Accounts.User.login
This error was unhandled because Ash.Error.Unknown.UnknownError does not implement the `AshPhoenix.FormData.Error` protocol.
** (Ash.Error.Unknown.UnknownError)
Maybe I will wait the documentation is more mature, because it’s so hard to too do simple things like a simple read action with validation.
My suggestion is to have exactly the same options for read as the other actions, if I do create :login
my form worked fine and also my login action. Just I don’t like to make a create action with no mutation …
I am slower than when I dev in Rust
Definitely not a suggestion to make a create action with no mutation. It wouldn’t work anyway
I understand if you’d rather not use it, but my suggestion is not to let small hurdles like this put you off We work every day based on conversations like this one to make the docs and UX better. Despite how it may seem, Ash is for far more than “normal” crud
It is a very expansive framework and there are lots of places that need some docs love, but its not an indicator of the utility of the framework itself
The common theme among our users is slow to get started, massively productive down the road. If you don’t have time to learn a new thing w/ new patterns then it may not be a good idea to use Ash. It isn’t for everyone, some users don’t enjoy working this way, to each their own, no judgement
In this case, we added shorthand for creating errors on actions in the way that I described, I was just mistaken that the shorthand had also been applied to read actions.
I’ve released v3.4.56
addressing this, and now the example call to Ash.Query.add_error/2
should work.
I’m fully aware of the work you’ve accomplished, and I believe I’ve gone through almost all your talks on YouTube. I’m going to give myself a little more time, about two more days, before making a final decision. I’m not saying this out of ego, but because I need to make a decisive choice for the company where I work as CTO, which leans towards a more full TypeScript/Node.js stack.
What reassures me is your availability and, of course, all the hard work you’ve put in so far. However, to be completely transparent and honest, I genuinely think the documentation has gaps. I won’t hesitate to contribute once I’m ready myself.
Thank you again for your response!
I will switch to it by tomorrow
Definitely. No denying it We’ll get it sorted, but likely not in the next few days
The book has been released into beta, so that’s also a good resource to learn.
Where I can buy the book ?
Quick question, I tested with
And it work, but something bother me as I need to duplicate the global validations.
Do you have a hint to reuse some validations from resource
in another module ?
Maybe it’s more relative to Elixir and my level but I will appreciate if you can help me on this one
My frustration got away :
create :register do
argument :password, :string, allow_nil?: false
accept [:email]
validate present(:email), message: "Email must be present"
validate present(:password), message: "Password must be present"
change fn changeset, _ ->
changeset |> maybe_hash_password()
end
end
end
defp maybe_hash_password(changeset) do
password = Ash.Changeset.get_argument(changeset, :password)
if password && changeset.valid? do
changeset
|> Ash.Changeset.change_attribute(:hashed_password, Argon2.hash_pwd_salt(password))
|> Ash.Changeset.clear_change(:password)
else
changeset
end
end
Both Login and register work
The book is available here: Ash Framework: Create Declarative Elixir Web Apps by Rebecca Le and Zach Daniel
Ok I will buy it asap.
Can you help me please ? :
Anonymous functions are just one way to define changes/preparations. You can use modules that implement the appropriate behavior.
prepare PreparationModule
....
validate ValidationModule
...
change ChangeModule
In all of those cases you can also add opts to reuse the behavior but customize per use.
change {SomeChange, opt: :value}
You can’t use a change as a preparation or a validation etc. so within those modules if you have to share logic you do it with “regular functions”. I.e if you want a function that takes a string and tells you if it’s a valid email, you can put that in a module alongside your resource and call into it from the resource.
Each of the ways of using those types of modules I mentioned above is explained in its respective guide: Changes — ash v3.4.56
The book also goes into this and how to use/think about it in detail.
I ended up by:
defmodule VsApp.Accounts.Constants do
@moduledoc """
Centralized constants for account-related validations and messages.
"""
@validation_rules %{
email: %{
regex: ~r/^[\w.+-]+@\w+\.\w+$/,
max_length: 160,
messages: %{
format: "Invalid email format",
length: "Email should have max length of 160",
presence: "Email must be present",
unique: "Email should be unique"
}
},
password: %{
min_length: 6,
messages: %{
length: "Password should have min length of 6",
presence: "Password must be present"
}
}
}
# Getters for validations
def email_regex, do: get_in(@validation_rules, [:email, :regex])
def email_max_length, do: get_in(@validation_rules, [:email, :max_length])
def password_min_length, do: get_in(@validation_rules, [:password, :min_length])
# Getters for messages
def message(:email, key), do: get_in(@validation_rules, [:email, :messages, key])
def message(:password, key), do: get_in(@validation_rules, [:password, :messages, key])
# Validation helper
def valid_email?(email) do
email && is_binary(email) && Regex.match?(email_regex(), email)
end
end
And:
defmodule VsApp.Accounts.Validations.ValidLoginPreparation do
use Ash.Resource.Preparation
require Logger
require Ash.Query
alias VsApp.Accounts.Constants
@impl true
def init(opts), do: {:ok, opts}
@impl true
def prepare(query, _opts, _context) do
password = Ash.Query.get_argument(query, :password)
email = Ash.Query.get_argument(query, :email)
cond do
is_nil(email) or not is_binary(email) ->
Ash.Query.add_error(query, field: :email, message: Constants.message(:email, :presence))
not Regex.match?(Constants.email_regex(), email) ->
Ash.Query.add_error(query, field: :email, message: Constants.message(:email, :format))
String.length(email) > Constants.email_max_length() ->
Ash.Query.add_error(query, field: :email, message: Constants.message(:email, :length))
is_nil(password) or not is_binary(password) ->
Ash.Query.add_error(query,
field: :password,
message: Constants.message(:password, :presence)
)
String.length(password) < Constants.password_min_length() ->
Ash.Query.add_error(query,
field: :password,
message: Constants.message(:password, :length)
)
true ->
query
|> Ash.Query.filter(email == ^email)
|> Ash.Query.load([:hashed_password])
|> Ash.Query.after_action(fn _, user ->
case user do
[user] ->
if Argon2.verify_pass(password, user.hashed_password) do
{:ok, [user]}
else
{:ok, []}
end
[] ->
Argon2.no_user_verify()
{:ok, []}
_ ->
{:ok, []}
end
end)
end
end
end
It is on the short list to allow validations to be run against queries, which will make this experience much nicer. i.e validate match(:email, ~r//), message: "..."
For now, however, preparations is the only way. Sorry about that!