Nested attributes updating without password on devise registrable custom views - ruby-on-rails

I have a two model setup with devise, users and profiles, with devise 4.2. I have implemented nested attributes with custom views as shown here: Rails 4.0 with Devise. Nested attributes Unpermited parameters
On my registrations/edit.html.haml view, when a user submits the form they must provide their current password by default. However, if the nested attributes are edited, and the password is not provided, the form will still update the nested attributes. How do I prevent this from happening?
registrations_controller.rb:
class RegistrationsController < Devise::RegistrationsController
before_filter :configure_permitted_parameters
def new
build_resource({})
self.resource.profile = Profile.new
respond_with self.resource
end
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:account_update) do |u|
u.permit(<user fields>, profile_attributes: [<:profile_fields>])
end
devise_parameter_sanitizer.permit(:sign_up) do |u|
u.permit(<user_fields>, profile_attributes: [<profile_fields>])
end
end
end
registrations/edit.html.haml:
.authform
%h3
Edit #{resource_name.to_s.humanize}
= form_for(resource, :as => resource_name, :url => user_registration_path, :html => { :method => :patch, :role => 'form'}) do |f|
= devise_error_messages!
.form-group
= f.label :email
= f.email_field :email, class: 'form-control'
- if devise_mapping.confirmable? && resource.pending_reconfirmation?
%div
Currently waiting confirmation for: #{resource.unconfirmed_email}
%fieldset
%p Leave these fields blank if you don't want to change your password.
.form-group
= f.label :password
= f.password_field :password, :autocomplete => 'off', class: 'form-control'
.form-group
= f.label :password_confirmation
= f.password_field :password_confirmation, class: 'form-control'
%fieldset
= f.fields_for :profile do |profile_fields|
.form-group
= profile_fields.label :a_field_1
= profile_fields.number_field :a_field_1, min: 0, max: 8
.form-group
= profile_fields.label :a_field_2
= profile_fields.number_field :a_field_2, min: 0, max: 8
.form-group
= profile_fields.label :a_field_1
= profile_fields.text_field :a_field_2, class: 'form-control'
%fieldset
%p You must enter your current password to make changes.
.form-group
= f.label :current_password
= f.password_field :current_password, class: 'form-control'
= f.submit 'Update', :class => 'button right'

It turned out that the root cause was an internal call to rails assign_attributes method which assigns nested attributes even if the password is invalid. The fix was to over-ride the update_resource method in the registrations controller with the following code
def update_resource(resource, params)incorrect
unless resource.valid_password?(params[:current_password])
resource.errors.add(:current_password, params[:current_password] ? :blank : :invalid)
return resource
end
resource.update_with_password(params)
end
Which checks if the password is valid and stops the assignment process if it is not.

Related

Submit one form to 2 tables in database - ruby on rails

I have 2 tables, landslides and sources (maybe doesn't relate to each other). I want a form which lets user to fill in information and then submit to both tables. Here's my current form without sources fields:
= form_for :landslide, :url => {:controller => 'landslides', :action => 'create'} do |f|
.form-inputs
%form#landslideForm
.form-group.row
%label.col-sm-2.col-form-label{for: "textinput"}Date
.col-sm-10
= f.date_select :start_date, :class => "form-control"
#Some fields
.form-actions
= f.button :submit, class: "btn btn-lg btn-primary col-sm-offset-5", id: "submitButton"
And parameters:
def landslide_params
params.require(:landslide).permit(:start_date, :continent, :country, :location, :landslide_type, :lat, :lng, :mapped, :trigger, :spatial_area, :fatalities, :injuries, :notes)
end
def source_params
params.require(:source).permit(:url, :text, :landslide_id)
end
Also there's a column in sources calls landslide_id which take the landslide ID from table landslides. So when a user submits a new landslide, how can I take the upcoming landslide ID (which is auto increment, user doesn't need to fill in)?
Thanks!
HTML does not allow nested <form> elements and you can't pass the id of record that has not been persisted yet through a form (because it does not have an id).
To create a nested resource in the same request you use accepts_nested_attributes_for:
class Landslide
# or has_many
has_one :source
accepts_nested_attributes_for :source
end
class Source
belongs_to :landslide
end
This means that you can do Landslide.create(source_attributes: { foo: 'bar' }) and it will create both a Landslide and a Source record and will automatically link them through sources.landslide_id.
To create the form inputs use fields_for:
# use convention over configuration
= form_for #landslide do |f|
.form-inputs
.form-group.row
# use the form builder to create labels instead
= f.label :start_date, class: 'col-sm-2 col-form-label'
.col-sm-10
= f.date_select :start_date, class: "form-control"
%fieldset
%legend Source
= f.fields_for :sources do |s|
.form-group.row
= s.label :url, class: 'col-sm-2 col-form-label'
.col-sm-10
= s.text_field :url, class: "form-control"
# ...
class LandslidesController
# ...
def new
#landslide = Landslide.new
# this is needed to seed the form with inputs for source
#landslide.source.new
end
def create
#landslide = Landslide.new(landslide_params)
if #landslide.save
redirect_to #landslide
else
#landslide.source.new unless #landslide.source.any?
render :new
end
end
private
def landslide_params
params.require(:landslide).permit(
:start_date, :continent, :country,
:location, :landslide_type,
:lat, :lng, :mapped, :trigger, :spatial_area,
:fatalities, :injuries, :notes,
source_attributes: [ :url, :text ]
)
end
end
You need to use accept_nested_attributes_for and nest your form accordingly:
(With reservation in regards to what form should be nested in which, I use the example of Sources submitted via landslide-form.)
in landslide.rb
accept_nested_attributes_for :sources
In your view (I don't know haml but anyways)
<%= form_for :landslide do |f|%>
<%= f.select :start_date %>
<%= fields_for :sources do |s| %>
<%= s.input :your_column %>
<% end %>
<%= f.button :submit %>
<% end %>
Btw, there are a lot of questions on this already, it's called 'Nested Forms'
Nested forms in rails - accessing attribute in has_many relation
Rails -- fields_for not working?
fields_for in rails view

Multiple require & permit strong parameters rails 4

In the below case, i am trying to use strong parameters. I want to require email_address, password and permit remember_me fields.
But using like below it only allows the LAST line in the method Ex:- In below case it only take params.permit(:remember_me)
private
def registration_params
params.require(:email_address)
params.require(:password)
params.permit(:remember_me)
end
Another Ex:- In this below case, if i rearrange it like below it will take only params.require(:email_address) where am i going wrong ?
def registration_params
params.require(:password)
params.permit(:remember_me)
params.require(:email_address)
end
UPDATE
Params hash be like
{
"utf8" => "✓",
"email_address" => "test1#gmail.com",
"password" => "password123",
"remember_me" => "true",
"commit" => "Log in",
"controller" => "registration",
"action" => "sign_in"
}
Ok found the answer through a friend ...one way to do this is
params.require(:email_address)
params.require(:password)
params.permit(
:email_address,
:password,
:remember_me
)
Works good.
Stong parameters are to prevent mass-assignment to Active Record models. Your parameters should be set up in a model backed form. Example from the Michael Hartl Tutorial:
REGISTRATION FORM
<%= form_for(#user) do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :email %>
<%= f.email_field :email %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation %>
<%= f.submit "Create my account", class: "btn btn-primary" %>
<% end %>
This will create a parameter that looks like:
PARAMS
{
"utf8" => "✓",
"user" => { email: "test1#gmail.com", name:"Test Name", password: "password", password_confirmation: "password" },
"remember_me" => "true",
"commit" => "Log in",
"controller" => "registration",
"action" => "sign_in"
}
Then in your registration controller you can use strong parameters like:
STRONG PARAMETERS
params.require(:user).permit(:name, :email, :password, :password_confirmation)
It looks like in your case, you are handling a log in, in which case, you only need to use regular parameters to capture the login information.
SESSION CREATION
def sign_in
email = params[:email]
password = params[:password]
if User.authenticate!(email, password)
# do something
else
# do something different
end
end
Edit:
Here is the Rails way for you to handle logins and, I believe, cases where you need to 'require' multiple parameters and provide errors back to the user.
Unlike using strong params, this approach provides feedback to the user (using validation errors) when parameters are missing or blank. This is more user-friendly than throwing an exception.
Create an ActiveModel (not ActiveRecord) form backing object. This form backing object is where you specify which fields are required and when a call to valid? is performed, these fields will be validated.
With this, you will get nice user-friendly errors if:
email is missing
password is missing
email and password do not match
models/session.rb
class Session
include ActiveModel::Model
attr_accessor :password, :email, :remember_me
validates_presence_of :password, :email # these fields are required!
def authenticate
return false unless valid? # this checks that required params
# are present and adds errors to the
# errors object if not
if User.authenticate(:password, :email) # validate credentials
true
else
errors.add(:email, "and password didn't match") # wrong credentials. add error!
false
end
end
end
Create the controller. Here is what your controller would look like for logging in a user:
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
# GET /login
def new
#session = Session.new
end
# POST /login
def create
#session = Session.new(login_params)
if #session.authenticate
# do whatever you need to do to log the user in
# set remember_me cookie, etc.
redirect_to '/success', notice: 'You are logged in'
else
render :new # shows the form again, filled-in and with errors
end
end
private
def login_params
params.require(:session).permit(:email, :password, :remember_me)
end
end
Set up the view
app/views/sessions/new.html.erb
<% if #session.errors.any? %>
<ul>
<% #session.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
<% end %>
<%= form_for #session, :url => login_path do |f| %>
<div>
<%= f.label :email, 'Email:' %>
</div>
<div>
<%= f.text_field :email %>
</div>
<div>
<%= f.label :password, 'Password:' %>
</div>
<div>
<%= f.password_field :password %>
</div>
<div>
<%= f.label :remember_me, 'Remember Me?' %>
<%= f.check_box :remember_me %>
</div>
<div>
<%= f.submit %>
</div>
<% end %>
Lastly, make sure the routes are configured
config/routes.rb
get 'login' => 'sessions#new'
post 'login' => 'sessions#create'
2020 solution:
def registration_params
params.require([:email_address, :password]) #require all of these
params.permit(:email_address, :password, :remember_me) #return hash
end

update_without_password causes devise to lose password validation

Assume my current passwort is 1234 and it has to be at least 4 characters long.
I have 3 input fields in a form:
new password
new password confirmation
current password
when I use update_with_password and the input
new password: 5678
new password confirmation: 5678
current paswword: 1234
It updates successfully
If for instance I use
new password: 12
new password confirmation: 23
current password: 1234
I get multiple devise errors: password too short, passwords don't match etc.
when I use update_without_password I expect I only need to remove the current password field and everything would stay the same.
Instead if I do that and give the input:
new password: 12
new password confirmation: 34
I get the message account updated successfully and the user record is not updated
this is my controller:
class Users::RegistrationsController < Devise::RegistrationsController
def edit
#images = Dir.glob("public/assets/images/users/#{current_user.id}/med/*")
end
def update
if params[:image_file_path]
ff = File.open("public/"+params[:image_file_path])
resource.image = ff
resource.save!
end
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 = resource.update_without_password(account_update_params)
yield resource if block_given?
if resource_updated
if is_flashing_format?
flash_key = update_needs_confirmation?(resource, prev_unconfirmed_email) ?
:update_needs_confirmation : :updated
set_flash_message :notice, flash_key
end
sign_in resource_name, resource, bypass: true
respond_with resource, location: after_update_path_for(resource)
else
#images = Dir.glob("public/assets/images/users/#{current_user.id}/med/*")
clean_up_passwords resource
respond_with resource
end
end
private
def sign_up_params
params.require(:user).permit(:first_name, :last_name, :email, :password, :password_confirmation, :telephone, :image, :address, :birthday)
end
def account_update_params
params.require(:user).permit(:first_name, :last_name, :email, :password, :password_confirmation, :telephone, :image, :image_file_path, :address, :birthday)
end
protected
def update_resource(resource, params)
resource.update_without_password(params)
end
def after_update_path_for(resource)
edit_user_registration_path
end
end
this is my view:
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: {method: :put }) do |f| %>
<div class="form-group">
<%= f.label :password, "Change Password", class: 'control-label' %>
<i>(leave blank if you don't want to change it)</i><br />
<%= f.password_field :password, autocomplete: "off", class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :password_confirmation, "New Password Confirmation", class: 'control-label' %><br />
<%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control' %>
</div>
<%= f.submit "Save", class: 'btn btn-success' %>
<%end%>
From http://www.rubydoc.info/github/plataformatec/devise/Devise/Models/DatabaseAuthenticatable:update_without_password:
Updates record attributes without asking for the current password. Never allows a change to the current password. If you are using this method, you should probably override this method to protect other attributes you would not like to be updated without a password.
It likely silently ignores the password and password_confirmation parameters in your request.

Rails 4: Password Validation Error: "{:password=>["can't be blank"]}" even though form is filled out

I'm relatively new to Rails (using Rails 4), and am having a problem with validation for my user model. Even when the form is fully filled in with both the passwords, when I submit the code two errors print out:
{:password=>["can't be blank"], :password_confirmation=>["doesn't match Password"]}
I would like the user to be saved into the database, but these validation errors are preventing that from happening. What I would like to know is what I need to change in order to get rid of these errors.
I am printing out the params object and it looks like this (the authenticity token is omitted here):
params: {"utf8"=>"✓","authenticity_token"=>"[omitted]",
"user"=>{"username"=>"testuser1", "password"=>"test",
"password_confirmation"=>"test", "email_attributes"=>{"email"=>"d#d.com"},
"first_name"=>"test", "last_name"=>"user", "gender"=>"male", "city"=>"la",
"state"=>"ca", "country"=>"usa", "dob"=>"1980-11-20"},
"commit"=>"Create Account", "action"=>"create", "controller"=>"users"}
So it appears that the password and password_confirmation attributes are getting passed correctly. I am wondering if this may have to do with the virtual attribute password I have defined in the user model, but if that is the case I am still not quite sure how to solve this problem. Any help would be greatly appreciated. Let me know if I need to elaborate further.
Here is relevant code for reference:
Controller:
class UsersController < ApplicationController
def new
#user = User.new
#user.build_email
end
def create
if #user = User.create(user_params)
logger.debug "#{#user.errors.messages}"
logger.debug "params: #{params}"
redirect_to :action => "new"
else
logger.debug "#{#user.errors.messages}"
logger.flush
redirect_to :action => "new"
end
end
private
def user_params
params.require(:user).permit(:username, :password, :password_confirmation, :first_name, :last_name, :gender, :dob, :city, :state, :country, :admin_level, email_attributes: [:email])
end
end
Model:
class User < ActiveRecord::Base
has_one :email
validates_presence_of :username, :email, :password
validates_confirmation_of :password, :on => :create
accepts_nested_attributes_for :email
def password_valid?(candidatePass)
candidatePassAndSalt = "#{candidatePass}#{self.salt}"
candidatePasswordDigest = Digest::SHA1.hexdigest(candidatePassAndSalt)
if (candidatePasswordDigest == self.password_digest)
return true
else
return false
end
end
def password
end
def password=(text)
self.salt = Random.new.rand
passAndSalt = "#{text}#{self.salt}"
self.password_digest = Digest::SHA1.hexdigest(passAndSalt)
end
end
View:
<%= form_for #user, url: {action: "create"}, html: {class: "user-creation-form"} do |f| %>
<%= f.text_field :username %>username<br/>
<%= f.password_field :password %>pw<br/>
<%= f.password_field :password_confirmation %>pwcopy<br/>
<%= f.fields_for :email do |email_form| %>
<%= email_form.text_field :email %>email<br />
<% end %>
<%= f.text_field :first_name %>first<br/>
<%= f.text_field :last_name %>last<br/>
<%= f.radio_button :gender, "male" %>
<%= f.label :gender_male, "M" %>
<%= f.radio_button :gender, "female" %>
<%= f.label :gender_female, "F" %><br />
<%= f.text_field :city %>city<br/>
<%= f.text_field :state %>state<br/>
<%= f.text_field :country %>country<br/>
<%= f.date_field :dob %>dob<br/>
<%= f.submit "Create Account" %><br/>
<% end %>
The issue is your empty getter:
def password
end
It always return nil.
2 small additions to the previous answer, which should resolve your issue by the way.
1) If you're using Rails >3 (I assume you are by looking at your user_params method in the controller) you don't have to specify all those password fields and validations.
ActiveRecord automatically includes this ActiveModel method :
has_secure_password
More details at : http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html#method-i-has_secure_password
2) If the uncrypted password/password_confirmation are shown in your log files your app is insecure. Add this to your config/application.rb :
config.filter_parameters = [:password, :password_confirmation]
This should not be needed if you are using has_secure_password in your User model.

Devise how to add a addtional field to the create User form?

I am trying to add a username to my User on create.
In devise/registrations/new I have:
<h2>Sign up</h2>
<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %>
<%= devise_error_messages! %>
<p><%= f.label :username %><br />
<%= f.text_field :username %></p>
<p><%= f.label :email %><br />
<%= f.email_field :email %></p>
<p><%= f.label :password %><br />
<%= f.password_field :password %></p>
<p><%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation %></p>
<p><%= f.submit "Sign up" %></p>
<% end %>
<%= render :partial => "devise/shared/links" %>
The problem is there is no params[:username] sent to the controller and I get the following error in the view:
ActiveRecord::StatementInvalid in Devise::RegistrationsController#create
Mysql::Error: Column 'username' cannot be null:
INSERT INTO `users` (`email`, `encrypted_password`, `reset_password_token`,
`reset_password_sent_at`, `remember_created_at`, `sign_in_count`,
`current_sign_in_at`, `last_sign_in_at`, `current_sign_in_ip`, `last_sign_in_ip`,
`created_at`, `updated_at`, `username`) VALUES ('mail#test.dk',
'$2a$10$bWjAXLY8QGXrXeVrGciv2O6mjRF940lajBEsUOPPtPDhKyj0A/gia', NULL, NULL,
NULL, 0, NULL, NULL, NULL, NULL, '2011-05-15 16:16:36', '2011-05-15 16:16:36',
NULL)
Rails.root: C:/Rails/densjove
Application Trace | Framework Trace | Full Trace
Request
Parameters:
{"utf8"=>"✓",
"authenticity_token"=>"qkQ8L0ZonXYxWQ2f4cfdREZ222oa2zGUb/qll3TRxjQ=",
"user"=>{"username"=>"hansen",
"email"=>"mail#test.dk",
"password"=>"[FILTERED]",
"password_confirmation"=>"[FILTERED]"},
"commit"=>"Sign up"}
I have added the username coloumn to my model, but how can I access the params[:username] in my controller?
Rails 4 moved the param sanitizing to the controller.
One way to add custom fields for devise is to add a before filter in the Application Controller calling a method to define which are your permitted parameters.
In Code From https://github.com/plataformatec/devise#strong-parameters
class ApplicationController < ActionController::Base
before_filter :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) << :username
end
end
The above code is if you are adding a field named username. If you were adding first_name it would be:
devise_parameter_sanitizer.for(:sign_up) << :first_name
This is one way and I strongly consider reading over the docs at the link above in order to learn more about customizing devise to permit certain fields.
Add the username field to attr_accessible in app/model/user.rb
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me, :username
Taken from comment #1 above so others can easily see the solution
Rails 4 Strong Params way to add to the controller
https://github.com/plataformatec/devise#strong-parameters
If you are wishing to add FirstName, LastName or any column while Registration of User, You have to configure those column with devise permitted parameters.
class ApplicationController < ActionController::Base
before_action :configure_new_column_to_devise_permitted_parameters, if: :devise_controller?
protected
def configure_new_column_to_devise_permitted_parameters
registration_params = [:first_name, :last_name, :email, :password, :password_confirmation]
if params[:action] == 'create'
devise_parameter_sanitizer.for(:sign_up) {
|u| u.permit(registration_params)
}
elsif params[:action] == 'update'
devise_parameter_sanitizer.for(:account_update) {
|u| u.permit(registration_params << :current_password)
}
end
end
end
For example : To add one more column like alternate_email at User Registration, Just add alternate_email column to registration_params
registration_params = [:first_name, :last_name, :email, :password,
:password_confirmation, :alternate_email ]
email]

Resources