I set these methods to automatically encrypt values.
class User < ApplicationRecord
def name=(val)
super val.encrypt
end
def name
(super() || '').decrypt
end
When I try to submit the form and there is an error (missing phone), then the name
attribute shows up garbled.
<input class="form-control" type="text" value="Mg8IS1LB2A1efAeZJxIDJMSroKcq6WueyY4ZiUX+hfI=" name="user[name]" id="user_name">
It works when the validations succeeds. It also works in the console when I go line-by-line through my controller #update
.
irb(main):015:0> u = User.find 1
irb(main):016:0> u.name
=> "Sue D. Nym"
irb(main):017:0> u.phone
=> "212-555-1234"
irb(main):018:0> u.update name: 'Sue D. Nym', phone: ''
(10.0ms) BEGIN
(1.0ms) ROLLBACK
=> false
irb(main):020:0> u.save
=> false
irb(main):029:0> u.errors.full_messages.join ','
=> "Phone can't be blank"
irb(main):031:0> u.build_image unless u.image
=> nil
irb(main):033:0> u.name
=> "Sue D. Nym"
users_controller.rb
def update
@user = User.find current_user.id
@user.update user_params
if @user.save
flash.notice = "Profile Saved"
redirect_to :dashboard
else
flash.now.alert = @user.errors.full_messages.join ', '
@user.build_image unless @user.image
render :edit
end
end
The view is somehow getting the encrypted value without going through #name
, and only after a validation failure.
I reduced the controller to the absolute minimum and it fails immediately after #update
. However, it's working on the console!
def update
@user = User.find current_user.id
@user.update user_params
render :edit
return
I reduced my view to the absolute minimum and it shows the name, but only outside of form_for
. I don't know why yet.
edit.haml
[email protected]
=form_for @user, html: { multipart: true } do |f|
=f.text_field :name
HTML source
<span>Sue D. Nym</span>
<form class="edit_user" id="edit_user_1" enctype="multipart/form-data" action="/users/1" accept-charset="UTF-8" method="post">
<input name="utf8" type="hidden" value="✓"><input type="hidden" name="_method" value="patch"><input type="hidden" name="authenticity_token" value="C/ScTxfENNxCKgzG0qAlPElOKI7nOYxZimQ7BsB64wIWQ9El4+vOAfxX3qHL08rbr0sxRiJnzQti13e4DAgkfQ==">
<input type="text" value="sER9cjwa6Ov5weXjEQN2KJYoTOXtVBytpX/cI/aPrFs=" name="user[name]" id="user_name">
</form>
I noticed attributes
still returned encrypted values so I tried adding this but form_for
still manages to obtain the encrypted value and put it in the form!
def attributes
attr_hash = super()
attr_hash["name"] = name
attr_hash
end
Rails 5.0.2
While you can work around this by overloading name_before_type_case
, I think this is actually the wrong place to be doing this kind of transformation.
Based on your example, the requirements here appear to be:
So if we move the encrytion/decryption transformation to the Ruby-DB boundary, this logic becomes much cleaner & reusable.
Rails 5 introduced a helpful Attributes API for dealing with this exact scenario. Since you have provided no details about how your encryption routine is implemented, I'm going to use Base64
in my example code to demonstrate a text transformation.
app/types/encrypted_type.rb
class EncryptedType < ActiveRecord::Type::Text
# this is called when saving to the DB
def serialize(value)
Base64.encode64(value) unless value.nil?
end
# called when loading from DB
def deserialize(value)
Base64.decode64(value) unless value.nil?
end
# add this if the field is not idempotent
def changed_in_place?(raw_old_value, new_value)
deserialize(raw_old_value) != new_value
end
end
config/initalizers/types.rb
ActiveRecord::Type.register(:encrypted, EncryptedType)
Now, you can specify this attribute as encrypted in the model:
class User < ApplicationRecord
attribute :name, :encrypted
# If you have a lot of fields, you can use metaprogramming:
%i[name phone address1 address2 ssn].each do |field_name|
attribute field_name, :encrypted
end
end
The name
attribute will be transparently encrypted & decrypted during roundtrips to the DB. This also means that you can apply the same transform to as many attributes as you like without rewriting the same code.
Why are you exposing it as name at all ?
class User < ApplicationRecord
def decrypted_name=(val)
name = val.encrypt
end
def decrypted_name
name.decrypt
end
end
Then you use @model.decrypted_name
instead of @model.name
as name is encrypted, and such saved in DB.
edit.haml
[email protected]_name
=form_for @user, html: { multipart: true } do |f|
=f.text_field :decrypted_name
And name
if it is encrypted should not be handled directly but with this decrypted_name
accessor.
I found this similar question: How do input field methods (text_area, text_field, etc.) get attribute values from a record within a form_for block?
I added
def name_before_type_cast
(super() || '').decrypt
end
And now it works!
Here is the full solution:
@@encrypted_fields = [:name, :phone, :address1, :address2, :ssn, ...]
@@encrypted_fields.each do |m|
setter = (m.to_s+'=').to_sym
getter = m
getter_btc = (m.to_s+'_before_type_cast').to_sym
define_method(setter) do |v|
super v.encrypt
end
define_method(getter) do
(super() || '').decrypt
end
define_method(getter_btc) do
(super() || '').decrypt
end
end
Some docs: http://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html
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