defmodule MyFirstElixirVibeCode.Accounts.User do use Ecto.Schema import Ecto.Changeset schema "users" do field :email, :string field :username, :string field :password, :string, virtual: true, redact: true field :password_hash, :string field :is_admin, :boolean, default: false timestamps(type: :utc_datetime) end @doc false def changeset(user, attrs) do user |> cast(attrs, [:email, :username, :password, :is_admin]) |> validate_required([:password]) |> validate_email_or_username() |> validate_password() |> put_password_hash() end defp validate_email_or_username(changeset) do is_admin = get_field(changeset, :is_admin) changeset |> then(fn cs -> if is_admin do cs |> validate_required([:username]) |> validate_length(:username, min: 3, max: 20) |> unsafe_validate_unique(:username, MyFirstElixirVibeCode.Repo) |> unique_constraint(:username) else cs |> validate_required([:email]) |> validate_email() end end) end defp validate_email(changeset) do changeset |> validate_required([:email]) |> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces") |> validate_length(:email, max: 160) |> unsafe_validate_unique(:email, MyFirstElixirVibeCode.Repo) |> unique_constraint(:email) end defp validate_password(changeset) do changeset |> validate_required([:password]) |> validate_length(:password, min: 6, max: 72) end defp put_password_hash(changeset) do case changeset do %Ecto.Changeset{valid?: true, changes: %{password: password}} -> change(changeset, password_hash: Bcrypt.hash_pwd_salt(password)) _ -> changeset end end @doc """ Verifies the password. If there is no user or the user doesn't have a password, we call `Bcrypt.no_user_verify/0` to avoid timing attacks. """ def valid_password?(%__MODULE__{password_hash: password_hash}, password) when is_binary(password_hash) and byte_size(password) > 0 do Bcrypt.verify_pass(password, password_hash) end def valid_password?(_, _) do Bcrypt.no_user_verify() false end end