Using legacy Devise records in Phoenix
If you, like me, are having fun with rebuilding a Rails app in Phoenix then you might also have to deal with User records made with devise. Here’s how to use them with no update to the data required.
First we have our user.ex
schema file:
defmodule MyApp.User do
use MyApp.Web, :model
schema "users" do
field :email, :string
field :encrypted_password, :string
field :password, :string, virtual: true
field :password_confirmation, :string, virtual: true
timestamps inserted_at: :created_at
end
@allowed [:email, :password, :password_confirmation]
@required [:email, :password, :password_confirmation]
def changeset(model attrs \\ %{}) do
model
|> cast(attrs, @allowed)
|> validate_required(@required)
|> MyApp.Crypto.encrypt_password
end
end
Notice the two virtual fields and the last function in the changeset pipeline. This will encrypt whatever’s in password
and save it to encrypted_password
. Let’s see how it looks in web/crypto.ex
.
defmodule MyApp.Crypto
import Ecto.Changeset, only: [put_change: 3]
def encrypt_password(changeset) do
password = changeset.data.password || changeset.chages.password
put_change(changeset, :encrypted_password, encrypt(password))
end
defp encrypt(password) do
pepper = Application.get_env(:my_app, :pepper)
Comeonin.Bcrypt.haspwsalt(password <> pepper)
end
end
A few things: Devise uses both a salt and a pepper. We use Comeonin for actually bcrypting the string.
Now to authenticate users when they log in we make a Session
module in web/session.ex
.
defmoudle MyApp.Session do
alias MyApp.Repo
def authenticate(schema, %{email: email, password: password}) do
case get_resource(schema, email) do
{:ok, resource} -> check_password(resource, password)
{:error, _} -> {:error, nil}
end
end
defp check_password(resource, password) do
case Comeonin.Bcrypt.checkpw(password <> pepper, resource.encrypted_password)
true -> {:ok, resource}
_ -> {:error, nil}
end
end
defp pepper do
Applicaion.get_env(:my_app, :pepper)
end
defp get_resource(schema, email) do
case Repo.get_by(schema, email: email) do
nil -> {:error, nil}
resource -> {:ok, resource}
end
end
end
Use them like MyApp.Repo.insert(MyApp.User.changeset(%{ ... }))
and MyApp.Session.authenticate(User, %{email: ..., password: ...})
.
This is actually all it takes. Now you can both create new users using the same techiques as devise and authenticate everybody, both new and old.