Devise custom sign up parameters not permitted - ruby-on-rails

Preamble
As a little bit of a setup to the problem, we have an authenticatable user model that is separate from the user model that contains non-authentication related data.
Problem
As the title suggests, adding custom fields to the built in configure_sign_up_params method does not actually permit the fields in the create action. I've generated the relevant controller and views with rails generate devise:controllers user and rails generate devise:views user, and made modifications to both. In the view:
<h2>Custom User Sign up</h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= devise_error_messages! %>
<%= fields_for(:extra_user_attributes) do |pa| %>
<div class="field">
<%= pa.label :first_name %><br />
<%= pa.text_field :first_name %>
</div>
<div class="field">
<%= pa.label :last_name %><br />
<%= pa.text_field :last_name %>
</div>
<% end %>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email %>
</div>
<div class="field">
<%= f.label :password %>
<% if #minimum_password_length %>
<em>(<%= #minimum_password_length %> characters minimum)</em>
<% end %><br />
<%= f.password_field :password, autocomplete: "off" %>
</div>
<div class="field">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "off" %>
</div>
<div class="actions">
<%= f.submit "Sign up" %>
</div>
<% end %>
<%= render "user/shared/links" %>
We have a nested :extra_user_attributes field that we need permitted, so in the devise generated controller, I uncommented a couple of lines and added the relevant logic:
class CustomUser::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
# POST /resource
def create
ap sign_up_params
end
protected
# If you have extra params to permit, append them to the sanitizer.
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [:extra_user_attributes => [:first_name, :last_name]])
end
end
This is where I got stuck. I've tried several permutations in the keys array, but everything I've tried thus far yields the same result. For my own sanity, I verified the before_action actually does trigger.
As for the endpoint, I'm simply printing the output to the console just to see what the permitted signup parameters are.
Parameters: {"utf8"=>"✓", "authenticity_token"=>"5MqzDUsbrdMdE0Z7/Vw70zJddaKR+0eLYdnum3wEyTShNjB9o3Nb4ZsnE0dZEPlgs4SheZtQad0VpuIGtCeSmw==", "extra_user_attributes"=>{"first_name"=>"", "last_name"=>""}, "user"=>{"email"=>"", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Sign up"}
{
"email" => "",
"password" => "",
"password_confirmation" => ""
}
Am I missing something here?
Workaround
A workaround would be to:
def other_signup_params
params.require(:extra_user_attributes).permit(:first_name, :last_name)
end
Which works just fine, but it just doesn't feel right. I feel like I should be able to use the built in configure_sign_up_parameters method that Devise provides but I'm somehow doing something wrong. Appreciate the help in advance.

The problem was not with devise, but a problem with the nested parameters I set up.
<%= fields_for(:extra_user_attributes) do |pa| %>
<div class="field">
<%= pa.label :first_name %><br />
<%= pa.text_field :first_name %>
</div>
<div class="field">
<%= pa.label :last_name %><br />
<%= pa.text_field :last_name %>
</div>
<% end %>
The fields_for did not get associated with the parent form, and got orphaned. It should have read:
<%= f.fields_for(:extra_user_attributes) do |pa| %>
Then, the sanitizer properly permits the fields:
{
"email" => "foo#example.com",
"password" => "hello!",
"password_confirmation" => "world!",
"extra_user_attributes" => {
"first_name" => "Foo",
"last_name" => "Bar"
}
}

Related

How to show validation errors for a login form when fields are empty on submit

I'm using the Devise gem for authentication and for the login form when I click on "Log in" with the fields blank I get the error "Invalid Email or password". I think it would be nice just like the registration form to have errors like "Name can't be blank" or "Email can't be blank" depending on the field that is blank. How can one achieve that?
Log in view
<h2>Log in</h2>
<%= form_for(resource, as: resource_name, url: session_path(resource_name), data: { turbo: false }) do |f| %>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
</div>
<div class="field">
<%= f.label :password %><br />
<%= f.password_field :password, autocomplete: "current-password" %>
</div>
<% if devise_mapping.rememberable? %>
<div class="field remember">
<%= f.check_box :remember_me %>
<%= f.label :remember_me %>
</div>
<% end %>
<div class="actions">
<%= f.submit "Log in" %>
</div>
<% end %>
<%= render "devise/shared/links" %>
application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
before_action :authenticate_user!
before_action :update_allowed_parameters, if: :devise_controller?
def after_sign_up_path_for(_resource)
groups_path
end
def after_sign_in_path_for(_resource)
groups_path
end
protected
def update_allowed_parameters
devise_parameter_sanitizer.permit(:sign_up) do |u|
u.permit(:name, :email, :password, :password_confirmation)
end
devise_parameter_sanitizer.permit(:account_update) do |u|
u.permit(:name, :email, :password, :password_confirmation, :current_password)
end
end
end
Its pretty trivial to just add a client side validation which will provide immediate feedback if you want to:
# app/views/devise/sessions/new.html.erb
<h2>Log in</h2>
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, autocomplete: "email", required: true %>
</div>
<div class="field">
<%= f.label :password %><br />
<%= f.password_field :password, autocomplete: "current-password", required: true %>
</div>
<% if devise_mapping.rememberable? %>
<div class="field">
<%= f.check_box :remember_me %>
<%= f.label :remember_me %>
</div>
<% end %>
<div class="actions">
<%= f.submit "Log in" %>
</div>
<% end %>
<%= render "devise/shared/links" %>`
The views in your application will take priority over those defined by the engine.
The reason why Devise doesn't actually use model validation at all for sessions is that models are not context aware and adding this feature would add a lot of complexity for very little gain.
Your model doesn't have a concept of "signing in" so if you called resource.valid? it will fire all the validations for creating a record like for example uniqueness of the email.
While you could do this by creating a form object or by overriding the controller method and adding the errors "inline" it provides very little additional value in safeguarding your application against bad user input which is what server side validations are primarily intendended to do (user feedback is the secondary purpose). YAGNI.

How i can do this when user click on buyer role it will go to homepage and when user select seller role it will go to Dashboard

This is application Controller for devise
class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
private
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:role])
end
end
signup form from devise and adding role
<h2>Sign up</h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
</div>
<div class="field">
<%= f.label :password %>
<% if #minimum_password_length %>
<em>(<%= #minimum_password_length %> characters minimum)</em>
<% end %><br />
<%= f.password_field :password, autocomplete: "new-password" %>
</div>
<div class="field">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
</div>
This is a role code
There is a problem when user select buyer on signup page it should go the homepage , when user select seller it should go to the selled dashboard
<%= f.label :role %>
<%= f.select :role, User.roles.keys %>
Here is the submit button
<div class="actions">
<%= f.submit "Sign up", data: {turbo: false} %>
</div>
<% end %>
<%= render "devise/shared/links" %>
You didn't provide much context about what you do in your controller when a user is signed up and how you build the form. Therefore I will just assume that it is a very basic form and all attributes are nested properly in the request and that the create action follows a simple scaffolding structure.
Then you basically only need to change where to redirect the user to depending on its role after the record was created.
# in controllers/users.rb
def create
user = User.new(user_params)
if user.save
case user.role
when 'user'
redirect_to root_path
when 'seller'
redirect_to dashboard_path
end
else
render :edit
end
end

Adding more fields to devise's new registration view causing ActiveRecord::StatementInvalid error

I am very naive in using devise with rails. I just created default views for employees signup, and my table contains default devise user fields(such as password, email etc) along with my custom fields such as first_name and last_name. In the new.html.erb file, which was like this initially,
<h2>Sign up</h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= devise_error_messages! %>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email%>
</div>
<div class="field">
<%= f.label :password %>
<% if #minimum_password_length %>
<em>(<%= #minimum_password_length %> characters minimum)</em>
<% end %><br />
<%= f.password_field :password, autocomplete: "off" %>
</div>
<div class="field">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "off" %>
</div>
<div class="actions">
<%= f.submit "Sign up" %>
</div>
<% end %>
<%= render "employees/shared/links" %>
I added two more text fields as:
<h2>Sign up</h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= devise_error_messages! %>
<div class="field">
<%= f.label :first_name %><br />
<%= f.text_field :first_name, autofocus: true %>
</div>
<div class="field">
<%= f.label :last_name %><br />
<%= f.text_field :last_name%>
</div>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email%>
</div>
<div class="field">
<%= f.label :password %>
<% if #minimum_password_length %>
<em>(<%= #minimum_password_length %> characters minimum)</em>
<% end %><br />
<%= f.password_field :password, autocomplete: "off" %>
</div>
<div class="field">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "off" %>
</div>
<div class="actions">
<%= f.submit "Sign up" %>
</div>
<% end %>
<%= render "employees/shared/links" %>
but these two parameters, first_name and last_name are not being passed in the post call, since the error I get says:
PG::NotNullViolation: ERROR: null value in column "first_name" violates not-null constraint DETAIL: Failing row contains (6, null, null, null, prabhjotsinghrai1#gmail.com, $2a$11$KpJ4wnRfgeJ.N7hqnamiyOMpABXtPw0VppB1KBV6sL4fpJs3pTLCS, null, null, null, 0, null, null, null, null, 2017-02-05 14:00:44.450243, 2017-02-05 14:00:44.450243). : INSERT INTO "employees" ("email", "encrypted_password", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"
I see that the first_name and last_name are not being added to the INSERT query. Can you help me out?
You probably faced the problem when the new attributes you've added to your model aren't permitted for mass assignment. If so you need to set up strong parameters for Devise with your first_name and last_name attributes like:
class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name])
end
end
See Devise Strong Parameters for more info.
P.S. You may want to add validations for the presence of first_name and last_name for you could handle that errors better (instead of getting errors from Postgres)

Create Devise User From A Different Form and Controller - Ruby on Rails

We have two models, a 'Devise User' and an 'Influencer'. An Influencer is a User, as such it must have a User (from the db standpoint). A User can be multiple other things. Thus, we want to have the ability to sign up a User without being an Influencer and we want to sign up a User when they want to sign up as an Influencer.
I have a form like so:
influencers/new.html.erb
<%= form_for #influencer do |i| %>
<%= i.fields_for(resource, as: resource_name, url: registration_path(resource_name)) do |u| %>
<div id="registration_fields">
<%= render 'devise/registrations/registration_fields', f: u %>
</div>
<% end %>
<div class='field'>
<%= i.label :twitter_handle %><br/>
<%= i.text_field :twitter_handle %>
</div>
<div class='field'>
<%= i.label :short_bio %><br/>
<%= i.text_area :short_bio %>
</div>
/views/devise/registrations/_registration_fields
<%= devise_error_messages! %>
<div class="field">
<%= f.label :first_name %> <br />
<%= f.text_field :first_name %>
</div>
<div class="field">
<%= f.label :last_name %> <br />
<%= f.text_field :last_name %>
</div>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true %>
</div>
<div class="field">
<%= f.label :password %>
<% if #minimum_password_length %>
<em>(<%= #minimum_password_length %> characters minimum)</em>
<% end %><br />
<%= f.password_field :password, autocomplete: "off" %>
</div>
<div class="field">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "off" %>
</div>
We've modified the ApplicationHelper and 'new' method so that it could render this Devise form without problems. Unfortunately, we are stuck as to how to properly make the 'create' method for our InfluencersController.
This is the hash we receive:
Parameters: {..., "influencer"=>{"user"=>{"first_name"=>"buddy", "last_name"=>"king", "email"=>"bdking#gmail.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "twitter_handle"=>"#bdking", "short_bio"=>"None"}, "commit"=>"Join as influencer"
Essentially we want to Devise to handle the user information while we handle the influencer information. We have tried calling the Devise::RegistrationsController.new.create method from within InfluencersController#create. However, this poses its own difficulties (even with multiple hacks we reach different problems such as, missing '#response' or missing 'response.env' or missing 'devise.mappings').
With that said, We believe that inheriting will allow us to call 'super' in the create function. However, we do not want to have InfluencersController inherit from Devise::RegistrationsController since this controller is not by any means a true Devise controller.
Is there any way we could get around this?
I would use the tried and true pattern of users and roles.
Basically your User class is in charge of authentication (identity) and the user has many Roles which can be used for authorization (permissions).
class User < ActiveRecord::Base
has_many :roles
def has_role?(role)
roles.where(name: role)
end
end
class Role < ActiveRecord::Base
belongs_to :user
validates_uniqueness_of :name, scope: :user_id
end
So an "influencer" is really just a user with a Role(name: "influencer") attached to it. The real power and flexibility is that it makes it trivial to implement granting/revoking roles from a web GUI.
And you don't have to mess around with how Devise/Warden handle authentication to support multiple classes which can get really messy.
Best part is that the Rolify gem makes it really trivial to set up.
If you need to setup a specific endpoint (influences/registrations) to register "influencers" you can simply override the build_resource method in the Devise controller:
class InfluencerRegistrationsController < Devise::RegistrationsController
def resource_class
User
end
def build_resource
super
self.resource.roles.new(name: :influencer)
end
end

Rails - nested model inside devise model

I have two models which is Members and Company...
Members is the devise model... When I sign up I want to include the following fields in the signup form.
Name
Email address
Password
Company name (from Company model)
company type (from Company model)
Member has one company
I am trying to create the signup form via nested form.. But I am not sure to build the form for the Company to receive input from the user...
Here is my controller
class Brands::Members::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
# before_action :configure_account_update_params, only: [:update]
# GET /resource/sign_up
def new
#company = Company.new
super
end
# POST /resource
def create
#company = Company.new(configure_sign_up_params)
#company.valid?
super
end
end
Here is my View
<h2>Sign up</h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= devise_error_messages! %>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name, autofocus: true %>
</div>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email %>
</div>
<div class="field">
<%= f.label :password %>
<% if #minimum_password_length %>
<em>(<%= #minimum_password_length %> characters minimum)</em>
<% end %><br />
<%= f.password_field :password, autocomplete: "off" %>
</div>
<!--
<div class="field">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "off" %>
</div>
-->
<%= #company.errors %>
<%= fields_for #company do |fc| %>
<div class="field">
<%= fc.label :name %><br />
<%= fc.text_field :name %>
</div>
<% end %>
<div class="actions">
<%= f.submit "Sign up" %>
</div>
<% end %>
<%= render "brands/members/shared/links" %>
Hope this will help,
Change your new action to this
#member = Member.new
#member.build_company

Resources