In my registrations edit form, I have the basic password change logic implemented. When I wanted to style the error messages, I tested the functionality for the first time and I ran into two major issues:
This only happens on the second try! First form submit shows the correct error messages, if I send the data (click the submit button) a second time: tadaaa - logged out and password changed. I don't even know what else to add, it is one of the most basic concepts of authentication.
What am I missing? Anyone has experience with this issue?
config/initializers/devise.rb
# ==> Configuration for :validatable
# Range for password length.
config.password_length = 6..128
user.rb
devise :invitable, :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, :confirmable
registrations/edit.html.erb
<%= f.label :change_password,
class: 'user__password-change' %>
<%= f.input :password,
hint: "leave it blank if you don't want to change it",
required: false,
error: false %>
<%= f.error :password, class: 'form-error' %>
<%= f.input :password_confirmation,
required: false,
error: false %>
<%= f.error :password_confirmation, class: 'form-error' %>
I know that I have a required: false
on both, but this basically just takes the asterisk away.
registrations_controller.rb
def update
super
@user.update(country: set_country, city: set_city)
@couch = @user.couch
@couchfacilities = @couch.couch_facilities
update_couch
update_couch_facilities
if offers_couch == "0"
set_couch_inactive
elsif offers_couch == "1"
set_couch_active
end
@usercharacteristics = @user.user_characteristics
update_user_characteristics
end
The core issue here is that you have not approached customizing the Devise controller in a good way at all.
Lets start by looking at how the super method is implemented:
class Devise::RegistrationsController < DeviseController
# PUT /resource
# We need to use a copy of the resource because we don't want to change
# the current user in place.
def update
self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key)
prev_unconfirmed_email = resource.unconfirmed_email if resource.respond_to?(:unconfirmed_email)
resource_updated = update_resource(resource, account_update_params)
yield resource if block_given?
if resource_updated
set_flash_message_for_update(resource, prev_unconfirmed_email)
bypass_sign_in resource, scope: resource_name if sign_in_after_change_password?
respond_with resource, location: after_update_path_for(resource)
else
clean_up_passwords resource
set_minimum_password_length
respond_with resource
end
end
...
end
As you have implemented it now your code is being executed way after the response has already been decided. You added a ton of additional code paths as well as each additional update may fail which are not handled at all.
If you want to tap into the flow and do something to the resource after it has been saved pass but before the response is rendered pass a block to super:
def update
super do |resource|
resource.update(country: set_country, city: set_city)
end
end
If you want to instead do something before its saved override the update_resource
method:
def update_resource(resource, params)
resource.assign_attributes(
country: set_country, city: set_city
)
super(resource, params)
end
You should also consider if you really need to jam all this functionality into a single endpoint.