Rails controller params conditionals - how to do this cleanly - ruby-on-rails

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.

Related

Expected at least 1 element matching "div#error_explanation", found 0.. Expected 0 to be >= 1

I'm a rails noob & can't make the connection of this error and the code while following Hartl's Rails tutorial.
Similar questions here # stackoverflow don't solve the problem of my particular test failure error message as my partial code is same as Hartl's code on github and all other aspects of my integration testing pass green.
sample_app_3rd_edition/app/views/shared/_error_messages.html.erb
Here is my code from the integration test
/test/integration/password_resets_test.rb
#Invalid password & confirmation
patch password_reset_path(user.reset_token),
email: user.email,
user: { password: "foobazz",
password_confirmation: "barquux" }
assert_select 'div#error_explanation'
which generates an error message referring to line 6:
test_password_resets#PasswordResetsTest (1442030711.19s)
Expected at least 1 element matching "div#error_explanation", found 0..
Expected 0 to be >= 1.
Next is the partial
/app/views/shared/_error_messages.html.erb
from which assert_select 'div#error_explanation' is supposed to render the error message needed for the invalid password and confirmation.
/app/views/shared/_error_messages.html.erb
<% if #user.errors.any? %>
<div id="error_explanation">
<div class="alert alert-danger">
The form contains <%= pluralize(#user.errors.count,"error") %>
</div>
<ul>
<% #user.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
I simply don't understand why assert_select is not working here since the _error_message partial works for all other tests referencing the same partial. I think I understand that the integration test failure is telling me that "there is no message (element) where there should be one."
Help clearing up my confusion is greatly appreciated! I'll be happy to post UserController/ PasswordController or whatever #user variable code is needed.
updated 10/10/15 here is user.rb
class User < ActiveRecord::Base
attr_accessor :remember_token, :activation_token, :reset_token
before_save :downcase_email
before_create :create_activation_digest
validates :name, presence: true, length: {maximum:50}
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validates :email,presence: true, length: {maximum:255},
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: { minimum: 6 }, allow_blank: true
Here is the PasswordResetsController
class PasswordResetsController < ApplicationController
before_action :get_user, only:[:edit, :update]
before_action :valid_user, only:[:edit, :update]
before_action :check_expiration, only:[:edit, :update]
def new
end
def create
#user = User.find_by(email: params[:password_reset] [:email].downcase)
if #user
#user.create_reset_digest
#user.send_password_reset_email
flash[:info] = "Email sent with password reset instructions"
redirect_to root_url
else
flash.now[:danger] = "Email address not found"
render 'new'
end
end
def edit
end
def update
if password_blank?
flash.now[:danger] = "Password can't be blank"
render 'edit'
elsif #user.update_attributes(user_params)
log_in #user
flash[:success] = "Password has been reset"
redirect_to #user
else
render 'edit'
end
end
private
def user_params
params.require(:user).permit(:password,
:password_confirmation)
end
#Returns true if password is blank.
def password_blank?
params[:user][:password].blank?
end
#Before filters
def get_user
#user = User.find_by(email: params[:email])
end
#Confirms a valid user.
def valid_user
unless (#user && #user.activated? &&
#user.authenticated?(:reset, params[:id]))
redirect_to root_url
end
end
#Checks expiration of reset token
def check_expiration
if #user.password_reset_expired?
flash[:danger] = "Password reset has expired."
redirect_to new_password_reset_url
end
end
end
I guess that you are not rendering the shared/_error_messages.html.erb in your app/views/password_resets/edit.html.erb form.
<% provide(:title, 'Reset password') %>
<h1>Reset password</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(#user, url: password_reset_path(params[:id])) do |f| %>
# Render the error messages!
<%= render 'shared/error_messages' %>
<%= hidden_field_tag :email, #user.email %>
Make sure that this is correctly inserted and the test should be successful!
Take a look at your User model (user.rb). Somewhere, there should be a line has_secure_password. This is a Rails method that adds the password and password_confirmation virtual attributes, along with their validations.
This is explained in section 6.3.1 of the Hartl Tutorial.
In the test, a user object is created with the password and password_confirmation attributes. Posting to the password_reset_path attempts to save this user object. Every time a model object tries to save, all model validations are run. So without this has_secure_password validation, the user object was allowed to save, and thus you were seeing no error.
1) Failure:
HomeControllerTest#test_should_get_home [/home/ubuntu/workspace/test/controllers/home_controller_test.rb:11]:
Expected at least 1 element matching "title", found 0..
Expected 0 to be >= 1.

How to allow a user to login with user_name or email using Rails

I have been trying to give users the ability to sign-in using either their unique user_name or the email address they input while registering.
Session_Controller.rb:
class SessionsController < ApplicationController
def create
#user = User.where(email: params[:email]).first
#user = User.where(username: params[:username]).first if #user.nil?
if #user && #user.password == params[:password]
session[:user_id] = #user.id
flash[:notice] = "Successfully Logged In #{ #user.fname }"
else
flash[:alert] = "Your credentials do not match the database"
end
redirect_to "/users/#{ #user.id }"
end
User_controller.rb:
class UsersController < ApplicationController
def index
#users = User.all
current_user
if #current_user
#leaders = #current_user.leaders
end
end
def create
#user = User.new(user_params)
if #user.save
flash[:notice] = "Signup Complete"
else
flash[:alert] = "Unsuccessful Signup"
end
redirect_to "/users"
end
user.rb:
class User < ActiveRecord::Base
validates :email, :password, presence: true # validates_presence_of :email
validates :password, length: {in:6..10}
Login page:
<h2>Login</h2>
<%= form_tag('/sessions', method: "POST") do %>
<%= email_field_tag(:email, "", placeholder: "email") %>
<%= password_field_tag(:password, "", placeholder: "password" ) %>
<%= submit_tag("Login") %>
<% end %>
Instead of using an email_field_tag, use a text_field tag. That way, the user won't have to deal with client-side email validation when logging in via username. Then change your parameter name accordingly (e.g. username_or_password, instead of email).
In your controller, you can use this parameter for both queries:
#user = User.where(email: params[:username_or_password]).first
#user = User.where(username: params[:username_or_password]).first if #user.nil?
Alternatively, you could attempt to determine whether or not the parameter is an email address by parsing the string - that way, you would not be executing two separate queries whenever someone tried to log in with a username.

Unable to update password with Bcrypt

I've tried this many ways but it seems BCrypt is encrypting a users submitted password twice.
When a user signs up- Bcrypt works great, and I am able to sign in. But when I try and update their password in my password_resets_controller, I'm no longer able to log in. My database shows that the password is being updated and hashed, but I can't sign in.
I even removed the line #customer.save, yet my database is still showing that the password is being updated !
Is something being updated under the hood I'm not aware of? See relatd SO thread:
Updating password with BCrypt
In my Customer.rb
require 'bcrypt'
class Customer < ActiveRecord::Base
include BCrypt
def password
#password ||= Password.new(password_hash)
end
def password=(new_password)
#password = Password.create(new_password)
self.password_hash = #password
end
def self.authenticate(email, password)
#customer = Customer.find_by_email(email)
if #customer && #customer.password == password
return #customer
else
return nil
end
end
end
In my customer_controller, the create code that actually works
require 'bcrypt'
class CustomersController < ApplicationController
def create_customer_account_iphone
#customer_count = Customer.where(email: params[:email]).size rescue nil
if(#customer_count == 0 || #customer_count == nil ||)
#customer = Customer.new(first_name: params[:first_name], email: params[:email])
#customer.password = params[:password] //this calls my model methods
#customer.save //here I am saving
unless (!#customer.save)
respond_to do |format|
msg = {:status => "SUCCESS", :messages => "Customer created", :data => #customer.as_json}
format.json { render :json => msg } # don't do msg.to_json
end
else
respond_to do |format|
msg = {:status => "FAILED", :messages => "Customer Not Saved"}
format.json { render :json => msg } # don't do msg.to_json
end
end
def sign_in_iphone
#customer = Customer.authenticate(params[:email], params[:password])
unless (#customer == 0 || #customer == nil)
respond_to do |format|
msg = {:status => "SUCCESS", :message => "CUSTOMER", :data => #customer.as_json}
format.json { render :json => msg } # don't do msg.to_json
end
else
respond_to do |format|
msg = {:status => "FAILED"}
format.json { render :json => msg } # don't do msg.to_json
end
end
end
In my password_reset_controller
class CustomerPasswordResetsController < ApplicationController
def edit
#customer = Customer.find_by_password_reset_token!(params[:id])
end
def update
#customer = Customer.find_by_password_reset_token!(params[:id])
if #customer.password_reset_sent_at < 2.hours.ago
redirect_to new_customer_password_reset_path, :alert => "Password reset has expired."
else
#customer.password_hash = BCrypt::Password.create(params[:password])
# #customer.save
unless !#customer.save
redirect_to new_customer_password_reset_path, :alert => "Password has been reset!"
else
render :edit
end
end
end
In my password_reset.html.erb
<%= form_for #customer, :url => customer_password_reset_path(params[:id]), :method => :patch do |f| %>
<% if #customer.errors.any? %>
<div class="error_messages">
<h2>Form is invalid</h2>
<ul>
<% for message in #customer.errors.full_messages %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :password %>
<%= f.password_field :password %>
</div>
<div class="field">
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation %>
</div>
<div class="actions"><%= f.submit "Update Password" %></div>
Given your form the new password will be in params[:customer][:password] not params[:password] - your existing code always sets the password to nil.
Changing the password resets controller update action to instead do
#customer.password = params[:customer][:password]
should do the trick. As a side note the commented out customer.save doesn't matter because you save again on the next line.
Next time something like this happens consider using the debugger to examine what is happening in your action - it would be easy enough to spot that the password was being set to nil. The debugging guide has lots more tip on this.
It's possible that you're assigning password as well as doing some post-processing with password_hash. The way this is intended to be used is via password alone if you have the model code with that password= method. This means you won't need to do any additional work beyond simply assigning it.
What you want in your password_reset method is:
#customer.password = params[:password]
#customer.save!
That should take care of it by running it through the appropriate model code.

Password validation triggers when it should not be triggering?

Working on a password reset mechanism for users. The password length validation is triggering and I'm trying to understand why.
user.rb
class User < ActiveRecord::Base
has_secure_password
validates :password, length: { minimum: 6 }
...
def create_password_reset_token
self.update_attributes!(password_reset_token: SecureRandom.urlsafe_base64, password_reset_sent_at: Time.zone.now)
end
def reset_password(params)
self.update_attributes!(params)
self.update_attributes!(password_reset_token: nil, password_reset_sent_at: nil)
end
end
password_resets_controller.rb
def create
user = User.find_by_email(params[:email])
if user
user.create_password_reset_token
UserMailer.password_reset_email(user).deliver
redirect_to root_url, :notice => "Email sent with password reset instructions!"
else
flash[:error] = "A user with that email address could not be found."
render 'new'
end
end
def edit
#user = User.find_by_password_reset_token(params[:id])
if #user
render 'edit'
else
flash[:error] = "Invalid password reset code."
redirect_to root_url
end
end
def update
#user = User.find_by_password_reset_token(params[:id])
if #user.password_reset_sent_at < 2.hours.ago
flash[:error] = "Password reset has expired."
redirect_to new_password_reset_path
elsif #user.reset_password(user_params)
flash[:success] = "Password has been reset."
redirect_to root_url
else
render 'edit'
end
end
password_resets/new.html.erb:
<%= form_tag password_resets_path, :method => :post do %>
<%= label_tag :email %>
<%= text_field_tag :email, params[:email] %>
<%= submit_tag "Reset Password" %>
<% end %>
password_resets/edit.html.erb:
<%= form_for #user, :url => password_reset_path(params[:id]) do |f| %>
<h1 class="centertext">Reset Password</h1>
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.label :password_confirmation, "Confirm password" %>
<%= f.password_field :password_confirmation %>
<%= f.submit "Update password" %>
<% end %>
The error is:
Validation failed: Password is too short (minimum is 6 characters)
The line that throws it is inside the create_password_reset_token method:
self.update_attributes!(password_reset_token: SecureRandom.urlsafe_base64, password_reset_sent_at: Time.zone.now)
Why does the validation trigger here? I'm not doing anything with the password itself. I'm simply creating a token and a time inside the user record.
Changing the validation to say on: :create makes it not trigger. The problem is that then users are able to reset their password to something fewer than six characters.
CLARIFICATION
To be clear, the order of operations is:
User clicks a link saying "I forgot my password."
They are taken to password_reset_controller/new.html.erb. This form has one field: email address. They enter their email and submit it.
Controller checks to see if that user exists. If it does, it tells the model to generate a password_reset_token.
Controller then orders an email to be sent to the user with a URL that contains the token.
The user clicks the URL. If the token is valid, they are taken to edit.html.erb and they enter their new email and its confirmation.
The controller calls the reset_password method, which actually resets the user's password.
Currently, the validation triggers on step 2, after they enter their email and click submit.
your create_password_reset_token is calling update_attributes which will trigger validations on every field in your User model and hence trigger the password validation as it doesn't have a current one set
you would need to either
1) Use update_attribute for those specific fields and that wouldn't trigger the validation
2) Add some password_reset field or enum to your model and set that to true when the password reset button is clicked and then do something like this in your user model
has_secure_password :validations => false
validates :password, length: {minimum: 6}, unless: -> { user_password_reset? }
3) Use the devise gem to take care of this for you
Update:
Try this
def create_password_reset_token
self.update_attribute(:password_reset_token, SecureRandom.urlsafe_base64)
self.update_attribute(:password_reset_sent_at, Time.zone.now)
end
I resolved this by adding a Proc statement to the password validation, like so:
validates :password, length: { minimum: 6 }, unless: Proc.new { |a| !a.password_reset_token.nil? }
Now the validation runs both during user creation and password reset, but not during the interval when there is a password reset token set. All tests are passing.

validate presence of not working in form_for tag

This is my first time doing validation on a rails application. I saw many tutorials which made it seem easy. I don't know why I cant get it to work.
Below is my setup.
Controller Admin (action = login)
def login
session[:user_id] = nil
if request.post?
#user = User.authenticate(params[:userId], params[:password])
if true
session[:user_id] = #user.user_id
flash.now[:notice] = "Login Successful"
redirect_to(:controller => "pages", :action => "mainpage")
else
flash.now[:notice] = "Invalid user/password combination"
end
end
end
So first time user comes to admin/login they are just presented with a form below
login.erb.html
<% form_for :user do |f| %>
<p><label for="name">User ID:</label>
<%= f.text_field :userid %>
</p>
<p><label for="password">Password:</label>
<%= f.password_field :password%>
</p>
<p style="padding-left:100px">
<%= submit_tag 'Login' %>
</p>
<% end %>
My User model:
class User < ActiveRecord::Base
validates_presence_of :userid, :password
def self.authenticate(userid, password)
user = self.find_by_userid_and_password(userid, password)
user
end
end
Actual field names for userId and password in my DB: userid password
I am expecting behavior that when user does not enter anything in the fields and just clicks submit. it will tell them that userid and password are required fields. However, this is not happening
From the console I can see the messages:
>> #user = User.new(:userid => "", :password => "dsf")
=> #<User id: nil, userid: "", password: "dsf", created_at: nil, updated_at: nil>
>> #user.save
=> false
>> #user.errors.full_messages
=> ["Userid can't be blank"]
So error is somewhere in my form submit...
UPDATE: validations only happen when u SAVE the object....here I am not saving anything. So in this case I have to do javascript validations?
It's the if true line. Change it to
if #user = User.authenticate(params[:userId], params[:password])
or
#user = User.authenticate(params[:userId], params[:password])
if #user
...
end
I'd also add redirect_to login_path to the failure case.
You can also slim down your auth method:
def self.authenticate(userid, password)
find_by_userid_and_password(userid, password)
end
It turns out, there are several issues here, and I'll try to cover them all. Let's start with your model:
class User < ActiveRecord::Base
validates_presence_of :userid, :password
def self.authenticate(userid, password)
self.find_by_userid_and_password(userid, password)
end
end
The validation doesn't come into play for logging in, only for creating and updating user records. The authentication has been trimmed, because ruby automatically returns the last calculated value in a method.
Next, the login action of your controller:
def login
session[:user_id] = nil
if request.post?
if #user = User.authenticate(params[:userId], params[:password])
session[:user_id] = #user.user_id
flash[:notice] = "Login Successful"
redirect_to(:controller => "pages", :action => "mainpage")
else
flash.now[:error] = "Invalid user/password combination"
end
end
end
Notice we don't use flash.now on a redirect - flash.now is only if you're NOT redirecting, to keep rails from showing the message twice.
Finally, you shouldn't be using form_for, because this is not a restful resource form. You're not creating or editing a user, so use form_tag instead:
<% form_tag url_for(:controller => :users, :action => :login), :method => :post do %>
<%= content_tag(:p, flash[:error]) if flash[:error] %>
<p><label for="name">User ID:</label>
<%= text_field_tag :userid %>
</p>
<p><label for="password">Password:</label>
<%= password_field_tag :password%>
</p>
<p style="padding-left:100px">
<%= submit_tag 'Login' %>
</p>
<% end %>
This will do what you want. This is a great learning exercise, but if you're serious about user authentication in a production application, checkout rails plugins like restful_authentication or clearance that do this for you in a much more sophisticated (and RESTful) way.

Resources