I have a rails app with a user model, which is able to have several roles. I implemented this using a bitmask like this:
class User < ActiveRecord::Base
DEFAULT_ROLES = %w[developer entrepreneur]
ROLES = ['admin', 'entrepreneur', 'developer']
def has_role?(role)
roles.include?(role.to_s)
end
def is?(role)
has_role?(role)
end
def roles=(roles)
self.roles_mask = (roles & ROLES).map { |r| 2**ROLES.index(r) }.inject(0, :+)
end
def roles
ROLES.reject do |r|
((roles_mask.to_i || 0) & 2**ROLES.index(r)).zero?
end
end
end
In the signup page for the app, I want users to choose if they are an 'entrepreneur' or a 'developer'. However, I want to ensure that they are not able assign themselves (or anyone else) any other role, unless they are already an admin.
My first thought was to do this in the roles= method by changin it to look like
def roles=(roles)
unless current_user.is?(:admin)
validates_inclusion_of roles, :in => DEFAULT_ROLES
end
self.roles_mask = (roles & ROLES).map { |r| 2**ROLES.index(r) }.inject(0, :+)
end
However, as I found out, you can't access current_user from inside a model (which I guess makes sense if you think about it...)
My next attempt was to see if I could do this using Strong Parameters.
I was expecting it would look something like this (I'm using devise, overriding the RegistrationsController)
class RegistrationsController < Devise::RegistrationsController
private
def sign_up_params
if (user_signed_in?) && (current_user.is?(:admin))
params.require(:user).permit(:name, :school, :email, :password, :password_confirmation, {roles: User::ROLES})
else
params.require(:user).permit(:name, :school, :email, :password, :password_confirmation, {roles: User::DEFAULT_ROLES})
end
end
def account_update_params
if (user_signed_in?) && (current_user.is?(:admin))
params.require(:user).permit(:name, :school, :email, :password, :password_confirmation, :current_password, :about_me, {roles: User::ROLES})
else
params.require(:user).permit(:name, :school, :email, :password, :password_confirmation, :current_password)
end
end
end
However, when I tried that, I got this:
which makes me think I'm misunderstanding how Strong Parameters really works.
Is it possible to restrict what values a user can enter for any given field based on that users's role with Strong Parameters? If not, is there a different way to accomplish this?
I figured it out, here's how I did it. (This is the method overrides Devise's RegistrationController, if you're not using devise, then simply replace whatever method controls what parameters are entered into a new user.)
class RegistrationsController < Devise::RegistrationsController
private
def sign_up_params
parameters = params.require(:user).permit(:name, :school, :email, :password, :password_confirmation, :about_me, roles: [])
unless (user_signed_in?) && (current_user.is?(:admin))
parameters[:roles] = parameters[:roles].reject { |h| !User::DEFAULT_ROLES.include? h }
parameters
end
end
def account_update_params
if can? :assign_roles, :all
params.require(:user).permit(:name, :school, :email, :password, :password_confirmation, :current_password, :about_me, roles: [])
else
params.require(:user).permit(:name, :school, :email, :password, :password_confirmation, :current_password)
end
end
end
I just filtered out the parameter in parameters[:roles] to only include values that were contained in User::DEFAULT_ROLES (shown above in the question), and then returned the parameters object.
Related
I have a multistep form, which I created with wizard. Basically the first tep of the form is user/sign_up - which in my understanding not a step yet. After hitting the sign-up button, user moves to the "real" first step, which is :address.
class UserStepsController < ApplicationController
include Wicked::Wizard
steps :address
def show
#user = current_user || User.from_omniauth(request.env["omniauth.auth"])
render_wizard
end
def update
#user = current_user || User.from_omniauth(request.env["omniauth.auth"])
#user.update!(user_params)
render_wizard #user
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation, :remember_me, :first_name, :last_name, :street, :house_number, :city, :zip_code)
end
def redirect_to_finish_wizard(options = nil, params = nil)
redirect_to new_user_profile_path(current_user)
end
end
This is basically the end of the form already. All gets saved to the user. Now I am stuck with validations.
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:omniauthable, omniauth_providers: %i[facebook]
has_one :profile, dependent: :destroy
after_create :create_profile
accepts_nested_attributes_for :profile
validates :first_name, presence: true
validates :last_name, presence: true
validates :street, presence: true
validates :house_number, presence: true
validates :city, presence: true
validates :zip_code, presence: true
def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
user.email = auth.info.email
user.password = Devise.friendly_token[0, 20]
name = auth.info.name
user.first_name = name.split(" ")[0]
user.last_name = name.split(" ")[1]
end
end
end
I would love to work with the the conditional validations in my model and only validate presence if on a certain step. This should be easy, as I theoretically only have one step, which is address. All I find on the internet, is way too complicated. Question is, do I have to somehow change user/sign_up to a first step in the form and address would be the second step? Or is it fine like this? And if so, can I just add the "if" statements to the address attributes in my validations, somehow defining what is the address step? Would it work like this?
def on_address_step?
wizard.steps = wizard.steps.first
end
Or how do I define it? The validations would look like this then:
validates :first_name, presence: true
validates :last_name, presence: true
validates :street, presence: true, if: :on_address_step?
validates :house_number, presence: true, if: :on_address_step?
validates :city, presence: true, if: :on_address_step?
validates :zip_code, presence: true, if: :on_address_step?
This is surely not that easy. For now this also doesn't work. How do I need to change it? Thanks.
P.S: here is also my Users Controller:
class UsersController < ApplicationController
def index
#users = User.all
end
def new
#user = User.new
end
def create
#user = User.new(params[:user])
if #user.save
session[:user_id] = #user.id
redirect_to user_steps_path
else
render :new
end
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation, :remember_me, :first_name, :last_name, :street, :house_number, :city, :zip_code)
end
end
If filling in the address is a completely separate process I would just branch the address out into its own model and controller.
class User < ApplicationRecord
# ...
has_one :address
end
class Address < ApplicationRecord
# ...
belongs_to :user
validates :first_name, :last_name, :street,
:house_number, :city, :zip_code, presence: true
end
This avoids turning your user model into even more of a god object and removes the need for the conditional validation that makes your model much more aware of the UX steps than it should be.
# routes.rb
resources :addresses, only: [:new, :create]
class UsersController < ApplicationController
# ...
def create
#user = User.new(params[:user])
if #user.save
session[:user_id] = #user.id
redirect_to new_address_path
else
render :new
end
end
end
class AddressesController < ApplicationController
# You should have some sort of method that checks if the user
# is signed in and redirect otherwise
before_action :authenticate_user!
# GET /addresses/new
def new
# I'm assuming you have some sort of method to fetch the signed in user
#address = current_user.build_address
end
# POST /addresses
def create
#address = current_user.build_address(address_params)
if #address.save
redirect_to '/somepath'
else
render :new
end
end
def address_params
params.require(:address).permit(
:first_name, :last_name, :street,
:house_number, :city, :zip_code
)
end
end
<%= form_with(model: #address) %>
# ... inputs
<% end %>
I doubt you really want the complexity involved with using Wicked which is ok if you really need a long multiple step form but in this case there is a far simpler and better design choice.
One thing that confuses me the most is when doing validation in one model with two controllers. I have a login system which register and logs users in. There both use the same model but both does not use the same amount of HTML widgets. One controller contains password, retype password, user name, first & second name and so on. The second controller uses only the user name and password fields. How would you do validation in the same model for this situation?
Thank you
here is the controller that register new users:
class UsersController < ApplicationController
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
session[:user_id] = #user.id
redirect_to '/cool'
else
#user = Newuser.new
#user.valid?
#user.errors.messages
render 'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password, :salt, :firstname, :secondname, :address, :postcode)
end
end
second controller:
class LoginsController < ApplicationController
before_filter :authorize
def index
#rentals = Rental.where(user_id: current_user.id).limit(5)
#buys = Buy.where(user_id: current_user.id).limit(5)
#users = User.where(id: current_user.id)
#buyGames = BuyGame.where(user_id: current_user.id).limit(5)
end
def destroy
#user = User.find(params[:id])
#user.destroy
redirect_to '/logout'
end
def edit
#user = User.find(params[:id])
end
def update
#user = User.find(params[:id])
if #user.update(account_params)
redirect_to '/cool'
else
render 'edit'
end
end
private
def account_params
params.require(:user).permit(:name, :email, :password, :salt, :firstname, :secondname, :address, :postcode)
end
end
Here is my model:
class User < ApplicationRecord
has_secure_password
end
One way to go is to remove validations from the model and put them in form objects. For this case, you'll have two form objects, each with its own set of validations. And you use the appropriate one in respective controllers. Something along these lines:
# logins_controller
def update
login_form = FormObjects::LoginForm.new(login_params)
if login_form.valid?
redirect_to '/cool'
else
render 'edit'
end
end
# users controller
def create
signup_form = FormObjects::SignupForm.new(user_params)
if signup_form.save
redirect_to '/cool'
else
render 'new'
end
end
# signup_form
module FormObjects
class SignupForm
include ::ActiveMode::Model
validate_presense_of :email, :password, :password_confirmation, :address, :whatever_else
def save
# create user here
end
end
end
# login_form
module FormObjects
class LoginForm
include ::ActiveMode::Model
validate_presense_of :email, :password
end
end
You can simply specify validations on actions, that is:
validates :first_name, presence: true, on: :create # which won't validate presence of first name on update or any other action
I believe the trick you are looking for is to define validation actions on create/update of the model. Something roughly along these lines:
class User < ActiveRecord::Base
# These are example validations only; replace with your actual rules.
validates :password, confirmation: true
validates_presence_of :username
validates :first_name, presence: true, format: {with: /.../}, on: create
validates :last_name, presence: true, format: {with: /.../}, on: create
end
...However, I am unclear why you would want to do this in your specific example. It would be advisable to always run all validation checks on fields like first_name, to help maintain data integrity.
Below is my model and controller from which i have filtered unneccessary lines.
class User < ActiveRecord::Base
validates :password, presence: true, on: :create
validates :password_confirmation, presence: true, if: "password.present?"
validates :password, confirmation: true,
length: { in: 6..20}, if: "password.present?"
end
and controller-
class UsersController < ApplicationController
def update
#user = User.find(params[:id])
if params[:password].present?
final_params = get_params
else
final_params = get_params.delete_if { |k,v|
k == "password" or k == "password_confirmation"
}
end
if #user.update(final_params)
redirect_to #user
else
render 'edit'
end
end
private
def get_params
params.require(:user).permit(:first_name, :last_name, :email, :date_of_birth,
:password,:password_confirmation, :mobile, :country, :state, :city, :pincode)
end
end
the problem is when updating a data, it shows a validation error i.e password confirmation can not be blank. even if I enter something to that field and submit. and to find error i tried replacing "password.present?" from password confirmation validation with "password.exists?" and it showed exception that exists is not a valid method for "123456 : string" . 123456 is the current password in DB. why is it checking password against db ? and please help me to solve this.
if params[:password].present?
final_params = get_params
else
final_params = get_params.delete_if { |k,v| k == "password" or k == "password_confirmation"}
end
Your problem is the first line here... your params are params[:user][:password] not params[:password] (you can see that in your get_params method)
So always your code is going to run the section that removes the password/confirmation
Also:
validates :password_confirmation, presence: true, if: "password.present?"
using a string of ruby in the validation is not generally considered good practice. How about adding a method like so:
validates :password_confirmation, presence: true, if: :confirmation_needed?
def confirmation_needed?
password.present?
end
Finally, you also need to not check the length of password_confirmation if it hasn't actually been entered:
validates :password, confirmation: true, length: { in: 6..20},
allow_blank: true, if: :confirmation_needed?
It's never a good idea to chunk down the incoming parameters in the controller.
Rather, putting proper validations in the model is a good idea !
hence cleaned up your controller.
Check below code:
class UsersController < ApplicationController
def update
#user = User.find(params[:id])
redirect_to #user and return if #user.update_attributes(get_params)
# render will not be executed if the user is redirected & returned
render :edit
end
private
def get_params
params.require(:user).permit(:first_name, :last_name, :email, :date_of_birth,
:password, :password_confirmation, :mobile, :country :state, :city, :pincode)
end
end
modified model:
class User < ActiveRecord::Base
validates :password, presence: true, on: :create
# above validation will be effective only for during new record creation.
# below 2 validations will be cheked only if password is present in the params list.
validates :password, confirmation: true,
length: { in: 6..20 }, if: validate_password?
validates :password_confirmation, presence: true, if: validate_password?
private
def validate_password?
password.present?
end
end
if still this does not help, then try to debug the self object in the validate_password? method. use raise self.inspect in the validation method to verify the incoming parameters.
That way you can track where you are going wrong.
I have been working on Devise and I tried to find a way to add more database attributes to the users table from devise. But it seems devise supports only email and password fields.
Is it possible to add more fields to the devise, so devise can validate to and save it to the users table on create, update calls.
yes you can add more fields by using migration.
I hope below link is help full to you.
http://www.jacopretorius.net/2014/03/adding-custom-fields-to-your-devise-user-model-in-rails-4.html
Yes. Checkout their documentation. https://github.com/plataformatec/devise
and search the page for "strong parameters" that should show you mostly what you're looking for.
an example:
class ApplicationController < ActionController::Base
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) { |u| u.permit({ roles: [] }, :email, :password, :password_confirmation, :first_name, :last_name, :gender, :avatar, :latitude, :longitude ) }
devise_parameter_sanitizer.for(:account_update) { |u| u.permit({ roles: [] }, :email, :password, :password_confirmation, :current_password, :first_name, :last_name, :age, :about_me, :gender, :avatar, :latitude, :longitude ) }
end
Yes you can like below I did for first_name, last_name
class RegistrationsController < Devise::RegistrationsController
private
def sign_up_params
params.require(:user).permit(:first_name, :last_name, :email, :password, :password_confirmation)
end
def account_update_params
params.require(:user).permit(:first_name, :last_name, :email, :password, :password_confirmation, :current_password)
end
end
def user_params
if current_user.admin?
params.require(:user).permit(:name, :email, :password, :student, :admin)
else
params.require(:user).permit(:name, :email, :password)
end
end
I would like admins to be able to update users' roles.
Users_params is a strong parameter to prevent hackers from changing users' roles.
The below code is insecure, correct? I see this code all over GitHub.
def user_params
params.require(:user).permit(:name, :email, :password, :student, :admin)
end
def user_params
if current_user.admin?
params.require(:user).permit!
else
params.require(:user).permit(:name, :email, :password)
end
end