Updating particular column in the table using update_user

Am I doing anything wrong with the pattern matching?

with {:ok, %User{} = user} <- Accounts.update_user(user, %{verify_email: true}) do
  text conn, "Email verified!"
end

or is it the update_user function?

def update_user(%User{} = user, attrs) do
  user
  |> User.changeset(attrs)
  |> Repo.update()
end

Why do all the fields get updated though only verify_email is set to true? Thus, the password is also updated into a new hashed phrase, making the users unable to log in every time after verifying the email. Or do I have to create a separate changeset for verify_email?

[debug] QUERY OK db=2.9ms
UPDATE "users" SET "password" = $1, "verify_email" = $2, "updated_at" = $3 WHERE "id" = $4 
["$pbkdf2-sha512$12345$SoMeNeWlYuPdAtEdHaShedKeY", true, {{2018, 7, 6}, {19, 36, 0, 301940}}, 171]

:wave:

What’s in User.changeset/2? You might have added some custom function which always computes and updates the password hash.

2 Likes
def changeset(%User{} = user, attrs) do
  user
  |> cast(attrs, [:username, :password, :email, :bio, :image_url,
    :token, :uid, :name, :verify_email])
  |> validate_required([:username, :password, :email])
  |> validate_format(:email, ~r/@/)
  |> validate_length(:password, min: 6, max: 100)
  |> hash_password
  |> unique_constraint(:email)
  |> unique_constraint(:username)
end

And what does this function do?

It hashes password using Pbkdf2 with salt.

defp hash_password(%{valid?: false} = changeset), do: changeset
defp hash_password(%{valid?: true} = changeset) do
  password =
    changeset
    |> get_field(:password)
    |> Pbkdf2.hashpwsalt()
  put_change(changeset, :password, password)
end

put_change(changeset, :password, password) – adds a “password” change into for every “valid” changeset, thus resulting in the UPDATE sql query you posted in OP.

Try checking if the password has been changed before hashing it again.

defp hash_password(%{valid?: true, changes: %{password: new_password}} = changeset) do
  put_change(changeset, :password, Pbkdf2.hashpwsalt(new_password))
end
defp hash_password(changeset), do: changeset

Off-topic: maybe it’s time to switch from pbkdf2 to bcrypt or even argon. And maybe don’t handle both hashed and unhashed passwords under the same name (:password, in your case) – you might not notice some small change in your code base, and boom! some of the password in your database are stored in plaintext.

5 Likes

Ah, yea, right. That could be confusing.

no function clause matching in Monitor.Accounts.User.hash_password/1

Called with 1 arguments
#Ecto.Changeset<action: nil, changes: %{verify_email: true}, errors: [], data: #Monitor.Accounts.User<>, valid?: true>

It returns this error.

Maybe, I think I will just create a separate changeset and custom function for updating verify_email.

You need to place a catch-all clause at the end, like

# ...
defp hash_password(changeset), do: changeset
1 Like

You can use Ecto.changeset.change/2 to add changes to a changeset.
In your case, to update the specific column you can write as

def update_user(%User{} = user, attrs) do
    user
    |> change(attrs)
    |> Repo.update()
end

instead of going through the User.changeset/2

1 Like

This potentially opens up your app to “hacker” attacks – updating “private” values that shouldn’t be updated (casted), like foreign keys. Especially so if you “blindly” pass the controller’s “params” into this function as “attrs”. Changesets at least mildly protect from that by using selective cast.

4 Likes
[debug] QUERY OK db=9.3ms
UPDATE "users" SET "verify_email" = $1, "updated_at" = $2 WHERE "id" = $3 [true, {{2018, 7, 
10}, {4, 42, 45, 52596}}, 180]

Gotcha! Thanks!