I have a User model:
class User < ActiveRecord::Base
has_secure_password
# validation lets users update accounts without entering password
validates :password, presence: { on: :create }, allow_blank: { on: :update }
validates :password_confirmation, presence: { if: :password_digest_changed? }
end
I also have a password_reset_controller:
def update
# this is emailed to the user by the create action - not shown
#user=User.find_by_password_reset_token!(params[:id])
if #user.update_attributes(params[:user])
# user is signed in if password and confirmation pass validations
sign_in #user
redirect_to root_url, :notice => "Password has been reset."
else
flash.now[:error] = "Something went wrong, please try again."
render :edit
end
end
Can you see the problem here? A user can submit a blank a password/confirmation and rails will sign them in, because the User model allows blank on update.
It's not a security concern, since an attacker would still need access to a user's email account before they could get anywhere near this action, but my problem is that a user submitting 6 blank chars would be signed in, and their password would not be changed for them, which could lead to confusion later on.
So, I've come up with the following solution, and I'd like to check if there's a better way of doing it, before I push to production:
def update
#user=User.find_by_password_reset_token!(params[:id])
# if user submits blank password, add an error, and render edit action
if params[:user][:password].blank?
#user.errors.add(:password_digest, "can't be blank.")
render :edit
elsif #user.update_attributes(params[:user])
sign_in #user
redirect_to root_url, :notice => "Password has been reset."
else
flash.now[:error] = "Something went wrong, please try again."
render :edit
end
end
Should I be checking for nil as well as blank? Are there any rails patterns or idiomatic ruby techniques for solving this?
[Fwiw, I've got required: true on the html inputs, but want this handled server side too.]
Please try this:
we can use - present?
Ex:
if !params[:user][:password].present?
Related
This is my code for changing a password:
class RegistrationsController < Devise::RegistrationsController
protect_from_forgery
def update_password
if current_user.update_with_password(devise_parameter_sanitizer.sanitize(:account_update))
sign_in(current_user, bypass: true)
redirect_to settings_path, notice: "updated"
else
redirect_to settings_path, alert: current_user.errors.full_messages
end
end
protected
def update_resource(resource, params)
resource.update_without_password(params)
end
def after_sign_up_path_for(_resource)
end
def after_update_path_for(_resource)
settings_path
end
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:account_update, keys: [:first_name, :last_name])
end
private
def user_params
# NOTE: Using `strong_parameters` gem
params.require(:user).permit(:current_password, :password, :password_confirmation)
end
end
The code works if I enter my current password and set a new one, but it doesn't when I enter a correct current password and as a new password + confirmation I leave empty fields (empty string).
The password will not get changed as "no password", but I get a flash message "updated". How do prevent that? I can think of this:
if current_user.update_with_password(devise_parameter_sanitizer.sanitize(:account_update))
if params[:user][:password].blank?
redirect_to settings_path, alert: "blank pswd"
return
end
sign_in(current_user, bypass: true)
redirect_to settings_path, notice: "Your password has been updated!"
else
redirect_to settings_path, alert: current_user.errors.full_messages
end
However, this solution is a bit... ugly. Is there a more elegant way to handle this situation?
Thank you in advance
Reading your question I was a bit surprised about this behavior. But looking at the source code I could confirm that this behavior is expected and describe in a comment in the code:
# This method also rejects the password field if it is blank (allowing
# users to change relevant information like the e-mail without changing
# their password). In case the password field is rejected, the confirmation
# is also rejected as long as it is also blank.
I have mixed feelings about how to handle it. I would probably not show an error message because when the user didn't enter a new password they probably would not expect the password to change.
But another way to handle the error message could be to not handle that case in the method at all but to use a before_action:
before_action :ensure_new_password_provided, only: :update_password
private
def ensure_new_password_provided
return if params[:user][:password].present?
redirect_to settings_path, alert: "blank pswd"
end
Hello I am trying to fix up my user sign up so if a user is already registered with the site with the same email, they cannot sign up. So far this is the code in my controller I am trying to implement.
User controller
class UsersController < ApplicationController
def create
unless User.exists?(:email => params[:email])
#user = User.new(user_params)
if #user.save
#user.cart = Cart.create
#user.save
session[:user_id] = #user.id
redirect_to #user
else
render 'new'
end
end
end
I figured that in the users controller I would have an unless conditional so if a user already exists it would prevent that user from signing up and just render the new page again. However the user is still able to sign up. Any ideas on how to do this properly would really help out.
Instead of validating this in your controller, move the validation to the User model, and add this line:
validates :email, uniqueness: true
I have an auth system from scratch, and when a user clicks on 'edit profile' it has to input the current password no matter the field he wants to edit.
def update
if params[:user][:password].present?
authenticated = #user.authenticate(params[:user][:current_password])
if authenticated && #user.update(user_params)
redirect_to root_url
flash[:notice] = "Your profile was successfully updated!"
else
#user.errors.add(:current_password, 'is invalid') unless authenticated
render :edit
end
elsif #user.update(user_params)
redirect_to root_url
flash[:notice] = "Your profile was successfully updated!"
else
render :edit
end
end
How can I call authenticate or use some context model validation only for the scenario when the user wants to change his password?
I wouldn't recommend mixing this logic into the model because you end up with complexity that is hard to follow as your application grows over time.
Try taking a look into form objects:
Form-backing objects for fun and profit
Railscast #416 Form Objects [paid subscription required]
I'd implement something like this:
class UserUpdateForm
include ActiveModel::Model
# Attributes
attr_accessor :user, :new_password, :new_password_confirmation
# Validations
validates :current_password, if: :new_password
validate :authenticate, if: :current_password
validates :new_password, confirmation: true, allow_blank: true
def initialize(user)
self.user = user
end
def submit(params)
self.new_password = params[:new_password]
self.new_password_confirmation = params[:new_password_confirmation]
if self.valid?
# Set other attributes as needed, then set new password below.
self.user.password = self.new_password if self.new_password.present?
self.user.save
else
false
end
end
private
def authenticate
unless self.authenticate(self.current_password)
self.errors.add(:current_password, 'is invalid')
end
end
end
Then you can call it from your controller like so:
def update
#user_update_form = UserUpdateForm.new(#user)
if #user_update_form.submit(params)
flash[:notice] = "Your profile was successfully updated!"
redirect_to root_url
else
render :edit
end
end
See the links above for how to handle the view and such. This is just to get you started.
You may create a nested if-else in this action statement that will check for existence of new_password and new_password_confirmation (or whatever the new password and confirmation fields are called) in the params[:user] object. If they are present - you may redirect to some king of page with request to enter existent password.
Another way is to use ajax to show asynchronously the dialog box with the same request (like respond_with self-invoking javascript function that handles that). Then handle submit button in of the dialog in the other action of the controller.
Update (considering use of validators):
Considering validation you may write your own validator (for password) and condition to check when the new password field come with some data from the client.
I think it could look like this:
validate :password_update?
def password_update?
if new_password.present?
if current_password !== self.password
errors.add(:current_password, "must be supplied!")
else
# update data and password
end
else
# do your regular update
end
end
In my update user profile form, the first field asks the user to enter her current password. When she submits the form, I verify the password before accepting the changes in other fields. Here's how I'm currently doing this in the users controller:
def update
#user = User.find(params[:id])
if #user.has_password?(params[:user][:password])
if #user.update_attributes(params[:user])
flash[:success] = "Profile updated."
redirect_to #user
else
render 'edit'
end
else
flash[:failure] = "Password does not match!"
render 'edit'
end
end
I feel like there's a better way to do this. For instance I could make password matching a validation in the user model. Then formtastic would automatically handle the error message for me (as opposed to my ugly flash approach above). I tried doing this with
validate :password_match?, :on => :update
And
def password_match?
has_password(params[:user][:password])
end
But as suspected params is not accessible from the model.
I searched SO for 20 minutes for a way to do this, couldn't find anything that did not involve Devise or Authlogic. I'm doing authentication from scratch (everything works fine: signin, sessions, etc.).
Please, show me the light on the better way!
You don't need devise, just use a before filter on your controller on update
On your profile controller.
before_filter password_match, :only => :update
then on the bottom as private.
private
def password_match
#user = User.find(params[:id])
#user.has_password?(params[:user][:password])
guys. I have a problem when doing the authlogic login validation. I have the magical "active" field in the user model, say if active is false, when login with correct password, it will pop up the message like "You are suspended, please contact your administration" (I defined this message in I18n file); when login with wrong password, it will pop up the the not_active message plus password_invalid message like "password invalid". I think that is because authlogic did the validation both for "active" field and password and it seems password validation comes first.
My question is, how can bypass the password validation if 'active' is false. Or, can I only show not_active message? my code like:
if #user_session.save
redirect_to home_path
else
render :json => {:success => false, :error => #user_session.errors.full_messages.join("\n")}
end
OK, so I don't like this as a user-experience, but if you really want to, do something like:
before_filter :restrict_inactive_users, :on=>:create
def restrict_inactive_users
#user = User.find_by_login(params[:user_session][:login]) rescue nil
return unless #user
unless #user.active?
flash[:error] = "You are suspended, please contact your administration"
render :action=>:new
return false
end
end
def create
#user_session = UserSession.new(params[:user_session])
if #user_session.save
redirect_to home_path
else
render :json => {:success => false, :error => #user_session.errors.full_messages.join("\n")}
end
end
Today I thought out a solution which doesn't bypass the password validation but just delete the password error message from user_session. Code like:
if #user_session.save
redirect_to home_path
else
#user_session.errors.delete(:password) unless #user_session.attempted_record.active
render :json => {:success => false, :error => #user_session.errors.full_messages.join("\n")}
end
Start with fetching the user by the identifier of your choice, like the email or user name.
If the user is not active you can remove the other errors before redirecting back to the login page.
#user_session.errors.clear
Then the errors will not show when the page is rerendered. But you must provide a custom error message, for example via the flash.now[:error] or your json response.