简体   繁体   中英

Rails controller params conditionals - how to do this cleanly

I'm need to check a bunch of conditions in a controller method. 1) it's a mess and 2) it's not even hitting the right redirects.

def password_set_submit
  password_check = /^(?=.*[a-z]{1,})(?=.*[A-Z]{1,})(?=.*\d{1,}){8,}.+$/

  @user = User.find(session[:id])
    if params[:password] && params[:password_confirmation] && params[:username] && params[:old_password]
        if params[:password] == params[:password_confirmation] && params[:password] =~ password_check

          # do some api stuff here

        if @user.save
          flash[:success] = 'Password updated.'
          redirect_to login_path and return
        end
      end
      if params[:password] != params[:password_confirmation]
        flash[:error] = 'Passwords did not match.'
        redirect_to password_set_path and return
      end
      if params[:password] == params[:password_confirmation] && params[:password] !~ password_check
        flash[:error] = 'Passwords did not match password criteria.'
        redirect_to password_set_path and return
      end
    end
  else
    flash[:error] = 'Please fill all inputs.'
    redirect_to password_set_path and return
  end
end

This needs to do the following:

1) If less than four params submitted, redirect and display 'Fill all inputs'

2) If password and password confirmation don't match each other, redirect and display 'Password did not match'

3) If password and password confirmation match each other but do not match criteria, redirect and display 'Passwords did not match criteria'

4) If password and password confirmation match each other and match criteria, make api call and redirect to login

I'm out of if/else ideas and I hope cleaning this up will help me nail the redirects correctly.

The Rails way to this is by using model validations.

class User < ActiveRecord::Base
  validates :password, confirmation: true, presence: true# password must match password_confirmation
  validates :password_confirmation, presence: true # a password confirmation must be set
end

If we try to create or update a user without a matching pw / pw confirmation the operation will fail.

irb(main):006:0> @user = User.create(password: 'foo')
   (1.5ms)  begin transaction
   (0.2ms)  rollback transaction
=> #<User id: nil, password: "foo", password_confirmation: nil, created_at: nil, updated_at: nil>
irb(main):007:0> @user.errors.full_messages
=> ["Password confirmation can't be blank"]
irb(main):008:0> 

However

When dealing with user passwords you should NEVER NEVER NEVER store them in the database in plain text!

Since most users reuse a common password you might also be compromising their email, bank account etc. You could potentially be held financially and legally responsible and it can destroy your career.

The answer is to use an encrypted password. Since this is incredibly easy to get wrong Rails has something called has_secure_password which encrypts and validates passwords.

The first thing you want to do is to remove the password and password_confirmation columns from your users database.

Add a password_digest column. And then add has_secure_password to your model.

class User < ActiveRecord::Base
  PASSWORD_CHECK = /^(?=.*[a-z]{1,})(?=.*[A-Z]{1,})(?=.*\d{1,}){8,}.+$/
  has_secure_password
  validates :password, format: PASSWORD_CHECK
end

This will automatically add validations for the password, confirmation and getters and setters for password and password_confirmation .

To check if the old password is correct we would do:

@user = User.find(session[:id]).authenticate(params[:old_password])
# user or nil

This is an example of the Rails way of doing it:

class UsersController

  # We setup a callback that redirects to the login if the user is not logged in
  before_action :authenticate_user!, only: [ :password_set_submit ]

  def password_set_submit
    # We don't want assign the the old_password to user.
    unless @user.authenticate(params[:old_password])
      # And we don't want to validate on the model level here
      # so we add an error manually:
      @user.errors.add(:old_password, 'The current password is not correct.')
    end
    if @user.update(update_password_params)
      redirect_to login_path, notice: 'Password updated.'
    else
      # The user failed to update, so we want to render the form again.
      render :password_set, alert: 'Password could not be updated.'
    end
  end

  private 

  # Normally you would put this in your ApplicationController 
  def authenticate_user!
    @user = User.find(session[:id])
    unless @user
      flash.alert('You must be signed in to perform this action.')
      redirect_to login_path
    end
  end

  # http://guides.rubyonrails.org/action_controller_overview.html#strong-parameters
  def update_password_params
    params.require(:user).permit(:password, :password_confirmation)
  end
end

Notice how the logic in our action is much much simpler? Either the user is updated and we redirect or it is invalid and we re-render the form.

Instead of creating one flash message per error we display the errors on the form:

<%= form_for(@user, url: { action: :password_set_submit}, method: :patch) do |f| %>

  <% if @user.errors.any? %>
    <div id="error_explanation">
      <h2>Your password could not be updated:</h2>
      <ul>
      <% @user.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>
  <div class="row">
    <%= f.label :password, 'New password' %>
    <%= f.password_field_tag :password %>
  </div>
  <div class="row">
    <%= f.label :password_confirmation %>
    <%= f.password_field_tag :password_confirmation %>
  </div>
  <div class="row">
    <p>Please provide your current password for confirmation</p>
    <%= f.label :old_password, 'Current password' %>
    <%= f.password_field_tag :old_password %>
  </div>
  <%= f.submit 'Update password' %>
<% end %>

I would remove all code related to this password reset from the controller and put into its own model User::PasswordReset :

# in app/models/user/password_reset.rb
class User::PasswordReset
  attr_reader :user, :error

  PASSWORD_REGEXP = /^(?=.*[a-z]{1,})(?=.*[A-Z]{1,})(?=.*\d{1,}){8,}.+$/

  def initialize(user_id)
    @user  = User.find(user_id)
  end

  def update(parameters)
    if parameters_valid?(parameters)
      # do some api stuff here with `user` and `parameters[:password]`
    else
      false
    end
  end

private

  def valid?
    error.blank?
  end

  def parameters_valid?(parameters)
    parameter_list_valid(parameters.keys) &&
      password_valid(params[:password], params[:password_confirmation])
  end

  def parameter_list_valid(keys)
    mandatory_keys = [:password, :password_confirmation, :username, :old_password]

    unless mandatory_keys.all? { |key| keys.include?(key) }
      @error = 'Please fill all inputs.'
    end

    valid?
  end

  def password_valid(password, confirmation)
    if password != confirmation
      @error = 'Passwords did not match.'
    elsif password !~ PASSWORD_REGEXP
      @error = 'Passwords did not match password criteria.'
    end

    valid?
  end

end

That would allow to change the controller's method to something simpler like this:

def password_set_submit
  password_reset = User::PasswordReset.new(session[:id])

  if password_reset.update(params)
    flash[:success] = 'Password updated.'
    redirect_to(login_path)
  else
    flash[:error] = password_reset.error
    redirect_to(password_set_path)
  end
end

Once you did this refactoring it should be much easier to find problems in your conditions and to extend your code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM