Current section

Files

Jump to
auth_n lib ecto user_changesets.ex
Raw

lib/ecto/user_changesets.ex

defmodule AuthN.Ecto.UserChangesets do
@callback registration_changeset(struct, map, struct) :: atom
@callback email_changeset(struct, map, struct) :: atom
@callback password_changeset(struct, map) :: atom
@callback confirm_changeset(struct) :: atom
@callback validate_current_password(struct, String.t) :: atom
defmacro __using__(opts) do
fields = Keyword.get(opts, :fields, [])
email_field = Keyword.get(fields, :email, :email)
hashed_password_field = Keyword.get(fields, :hashed_password, :hashed_password)
clear_password_field = Keyword.get(fields, :clear_password, :clear_password)
confirmed_at_field = Keyword.get(fields, :confirmed_at, :confirmed_at)
hashing_module = Keyword.get(opts, :hashing_module, Argon2)
this_module = __MODULE__
quote do
import Ecto.Changeset
@behaviour unquote(this_module)
@doc """
A user changeset for registration.
It is important to validate the length of both e-mail and password.
Otherwise databases may truncate the e-mail without warnings, which
could lead to unpredictable or insecure behaviour. Long passwords may
also be very expensive to hash for certain algorithms.
"""
def registration_changeset(user, attrs, ecto_repo) do
user
|> cast(attrs, [unquote(email_field), unquote(clear_password_field)])
|> validate_email(ecto_repo)
|> validate_password()
end
defp validate_email(changeset, ecto_repo) do
changeset
|> validate_required([unquote(email_field)])
|> validate_format(unquote(email_field), ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
|> validate_length(unquote(email_field), max: 160)
|> unsafe_validate_unique(unquote(email_field), ecto_repo)
|> unique_constraint(unquote(email_field))
end
defp validate_password(changeset) do
changeset
|> validate_required([unquote(clear_password_field)])
|> validate_length(unquote(clear_password_field), min: 10, max: 80)
# |> validate_format(unquote(clear_password_field), ~r/[a-z]/, message: "at least one lower case character")
# |> validate_format(unquote(clear_password_field), ~r/[A-Z]/, message: "at least one upper case character")
# |> validate_format(unquote(clear_password_field), ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
|> prepare_changes(&maybe_hash_password/1)
end
defp maybe_hash_password(changeset) do
if password = get_change(changeset, unquote(clear_password_field)) do
changeset
|> put_change(unquote(hashed_password_field), unquote(hashing_module).hash_pwd_salt(password))
|> delete_change(unquote(clear_password_field))
else
changeset
end
end
@doc """
A user changeset for changing the e-mail.
It requires the e-mail to change otherwise an error is added.
"""
def email_changeset(user, attrs, ecto_repo) do
user
|> cast(attrs, [unquote(email_field)])
|> validate_email(ecto_repo)
|> case do
%{changes: %{unquote(email_field) => _}} = changeset -> changeset
%{} = changeset -> add_error(changeset, unquote(email_field), "did not change")
end
end
@doc """
A user changeset for changing the password.
"""
def password_changeset(user, attrs) do
user
|> cast(attrs, [unquote(clear_password_field)])
|> validate_confirmation(unquote(clear_password_field), message: "does not match password")
|> validate_password()
end
@doc """
Confirms the account by setting `confirmed_at`.
"""
def confirm_changeset(user) do
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
change(user, [{unquote(confirmed_at_field), now}])
end
@doc """
Validates the current password otherwise adds an error to the changeset.
"""
def validate_current_password(changeset, password) do
if AuthN.valid_password?(changeset.data, unquote(hashed_password_field), password) do
changeset
else
add_error(changeset, :current_password, "is not valid")
end
end
defoverridable confirm_changeset: 1,
email_changeset: 3,
password_changeset: 2,
registration_changeset: 3,
validate_current_password: 2
end
end
end