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>
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.