Files
Claper/lib/claper/accounts/user.ex
2024-08-11 11:16:34 +02:00

135 lines
3.6 KiB
Elixir

defmodule Claper.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
@type t :: %__MODULE__{
id: integer(),
uuid: Ecto.UUID.t(),
email: String.t(),
password: String.t() | nil,
hashed_password: String.t(),
is_randomized_password: boolean(),
confirmed_at: NaiveDateTime.t() | nil,
locale: String.t() | nil,
events: [Claper.Events.Event.t()] | nil,
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
}
schema "users" do
field :uuid, :binary_id
field :email, :string
field :password, :string, virtual: true, redact: true
field :hashed_password, :string, redact: true
field :is_randomized_password, :boolean
field :confirmed_at, :naive_datetime
field :locale, :string
has_many :events, Claper.Events.Event
timestamps()
end
def registration_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:email, :confirmed_at, :password, :is_randomized_password])
|> validate_email()
|> validate_password(opts)
end
def preferences_changeset(user, attrs) do
user
|> cast(attrs, [:locale])
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, Claper.Repo)
|> unique_constraint(:email)
end
defp validate_password(changeset, opts) do
changeset
|> validate_required([:password])
|> validate_length(:password, min: 6, max: 72)
|> maybe_hash_password(opts)
end
defp maybe_hash_password(changeset, opts) do
hash_password? = Keyword.get(opts, :hash_password, true)
password = get_change(changeset, :password)
if hash_password? && password && changeset.valid? do
changeset
|> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password))
|> delete_change(:password)
else
changeset
end
end
@doc """
A user changeset for changing the email.
It requires the email to change otherwise an error is added.
"""
def email_changeset(user, attrs) do
user
|> cast(attrs, [:email])
|> validate_email()
|> case do
%{changes: %{email: _}} = changeset -> changeset
%{} = changeset -> add_error(changeset, :email, "did not change")
end
end
@doc """
A user changeset for changing the password.
"""
def password_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:password, :is_randomized_password])
|> validate_confirmation(:password)
|> validate_password(opts)
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?(%Claper.Accounts.User{hashed_password: hashed_password}, password)
when is_binary(hashed_password) and byte_size(password) > 0 do
Bcrypt.verify_pass(password, hashed_password)
end
def valid_password?(_, _) do
Bcrypt.no_user_verify()
false
end
@doc """
Confirms the account by setting `confirmed_at`.
"""
def confirm_changeset(user) do
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
change(user, confirmed_at: now)
end
@doc """
Validates the current password otherwise adds an error to the changeset.
"""
def validate_current_password(changeset, password) do
if valid_password?(changeset.data, password) do
changeset
else
add_error(changeset, :current_password, "is not valid")
end
end
end