daily-tracker

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

user.ex (4219B)


      1 defmodule DailyTracker.Accounts.User do
      2   use Ecto.Schema
      3   import Ecto.Changeset
      4 
      5   schema "users" do
      6     field :email, :string
      7     field :password, :string, virtual: true, redact: true
      8     field :hashed_password, :string, redact: true
      9     field :confirmed_at, :utc_datetime
     10     field :authenticated_at, :utc_datetime, virtual: true
     11 
     12     timestamps(type: :utc_datetime)
     13   end
     14 
     15   @doc """
     16   A user changeset for registering or changing the email.
     17 
     18   It requires the email to change otherwise an error is added.
     19 
     20   ## Options
     21 
     22     * `:validate_unique` - Set to false if you don't want to validate the
     23       uniqueness of the email, useful when displaying live validations.
     24       Defaults to `true`.
     25   """
     26   def email_changeset(user, attrs, opts \\ []) do
     27     user
     28     |> cast(attrs, [:email])
     29     |> validate_email(opts)
     30   end
     31 
     32   defp validate_email(changeset, opts) do
     33     changeset =
     34       changeset
     35       |> validate_required([:email])
     36       |> validate_format(:email, ~r/^[^@,;\s]+@[^@,;\s]+$/,
     37         message: "must have the @ sign and no spaces"
     38       )
     39       |> validate_length(:email, max: 160)
     40 
     41     if Keyword.get(opts, :validate_unique, true) do
     42       changeset
     43       |> unsafe_validate_unique(:email, DailyTracker.Repo)
     44       |> unique_constraint(:email)
     45       |> validate_email_changed()
     46     else
     47       changeset
     48     end
     49   end
     50 
     51   defp validate_email_changed(changeset) do
     52     if get_field(changeset, :email) && get_change(changeset, :email) == nil do
     53       add_error(changeset, :email, "did not change")
     54     else
     55       changeset
     56     end
     57   end
     58 
     59   @doc """
     60   A user changeset for changing the password.
     61 
     62   It is important to validate the length of the password, as long passwords may
     63   be very expensive to hash for certain algorithms.
     64 
     65   ## Options
     66 
     67     * `:hash_password` - Hashes the password so it can be stored securely
     68       in the database and ensures the password field is cleared to prevent
     69       leaks in the logs. If password hashing is not needed and clearing the
     70       password field is not desired (like when using this changeset for
     71       validations on a LiveView form), this option can be set to `false`.
     72       Defaults to `true`.
     73   """
     74   def password_changeset(user, attrs, opts \\ []) do
     75     user
     76     |> cast(attrs, [:password])
     77     |> validate_confirmation(:password, message: "does not match password")
     78     |> validate_password(opts)
     79   end
     80 
     81   defp validate_password(changeset, opts) do
     82     changeset
     83     |> validate_required([:password])
     84     |> validate_length(:password, min: 12, max: 72)
     85     # Examples of additional password validation:
     86     # |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
     87     # |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
     88     # |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
     89     |> maybe_hash_password(opts)
     90   end
     91 
     92   defp maybe_hash_password(changeset, opts) do
     93     hash_password? = Keyword.get(opts, :hash_password, true)
     94     password = get_change(changeset, :password)
     95 
     96     if hash_password? && password && changeset.valid? do
     97       changeset
     98       # If using Bcrypt, then further validate it is at most 72 bytes long
     99       |> validate_length(:password, max: 72, count: :bytes)
    100       # Hashing could be done with `Ecto.Changeset.prepare_changes/2`, but that
    101       # would keep the database transaction open longer and hurt performance.
    102       |> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password))
    103       |> delete_change(:password)
    104     else
    105       changeset
    106     end
    107   end
    108 
    109   @doc """
    110   Confirms the account by setting `confirmed_at`.
    111   """
    112   def confirm_changeset(user) do
    113     now = DateTime.utc_now(:second)
    114     change(user, confirmed_at: now)
    115   end
    116 
    117   @doc """
    118   Verifies the password.
    119 
    120   If there is no user or the user doesn't have a password, we call
    121   `Bcrypt.no_user_verify/0` to avoid timing attacks.
    122   """
    123   def valid_password?(%DailyTracker.Accounts.User{hashed_password: hashed_password}, password)
    124       when is_binary(hashed_password) and byte_size(password) > 0 do
    125     Bcrypt.verify_pass(password, hashed_password)
    126   end
    127 
    128   def valid_password?(_, _) do
    129     Bcrypt.no_user_verify()
    130     false
    131   end
    132 end