I have a before_save callback in my model which encrypts 2 fields before they're saved to the database.
class Account < ActiveRecord::Base
before_save :encrypt_credentials, if: "!username.blank? && !password.blank?"
def encrypt_credentials
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
self.username = crypt.encrypt_and_sign(username)
self.password = crypt.encrypt_and_sign(password)
end
def decrypted_username
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
crypt.decrypt_and_verify(username)
end
def decrypted_password
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
crypt.decrypt_and_verify(password)
end
end
The situation is very similar to Devise models run before_save multiple times?. When I call Model.create!(...) - which includes the 2 fields that need to be encrypted, the before_save gets called twice, ending up in the fields being encrypted twice.
Account.create!(
{
username: ENV['USERNAME'],
password: ENV['PASSWORD']
})
Why is before_save called multiple times? I don't like the solution of the post linked above and I don't want to do new/build followed by save.
It was user error :( After calling account = Account.create!, I had other code which called save! back on the model: account.foo = bar; account.save!. This obviously called befor_save again and re-encrypted my fields. I ended up with something like this:
class Account < ActiveRecord::Base
before_save :encrypt_username, if: :username_changed?
before_save :encrypt_password, if: :password_changed?
def encrypt_username
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
self.username = crypt.encrypt_and_sign(username)
end
def encrypt_password
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
self.password = crypt.encrypt_and_sign(password)
end
def decrypted_username
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
crypt.decrypt_and_verify(username)
end
def decrypted_password
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
crypt.decrypt_and_verify(password)
end
end
Option 1 (could be a mistake in usage of callbacks):
Short answer: use after_save instead of before_save
Long answer: How to organize complex callbacks in Rails?
When you use the:
account = Account.new
account.save
You are firing the before_save hook each time.
Option 2 (could be a bug):
Maybe you're actually touching the record several times.
For example in:
def create
@account = Customer.find(params[:customer_id]).accounts.create(account_params)
if @account.save
redirect_to customer_account_path(@account.customer.id, @account.id)
else
render :new
end
end
You are in fact touching it with create and save. In which case I suggest:
def create
@account = Customer.find(params[:customer_id]).accounts.build(account_params)
if @account.save
redirect_to customer_account_path(@account.customer.id, @account.id)
else
render :new
end
end
Build doesn't try to save the record so you shouldn't have any more problems. Hope this helps! Have a great day!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With