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