How could I make this code better?

How could I clean this action up?

def create
    user = User.find_by_email(params[:email])
    if user && user.authenticate(params[:password]) && user.verified
      session[:user_id] = user.id
      flash.notice = "Successfully Signed In!"
      redirect_to dashboard_path
    elsif user && !user.verified
      flash.now.alert = "Please Verify your account before logging in"
      render "new"
    else
      flash.now.alert =  "Invalid Username or Password!"
      render "new"
    end
  end

I think this looks a little messy

Thanks

1 Like

@scott, what is this a create action for? A session?

I think your code is more complicated than it needs to be because authenticate is implemented as an instance method on User. Itā€™s important that User have credentials, but I donā€™t think that it needs to know how to perform that authentication. What if authentication were the responsibility of a controller method?

def authenticate
  User.find_by_email_and_password(params[:email], params[:password])
end

now your create looks like this:

def create
  if user
    if user.verified
      # sign them in
    else
      # tell them to verify
    end
  else
    # tell them they are invalid
  end
end

def user
  @user || authenticate
end

Thatā€™s still not great, but weā€™re getting somewhere. You could extract the lines that set flash.now and render 'new' into a method that takes the error message. That would eliminate some duplication. As you start cleaning it up, you might see more possible refactorings - maybe extracting a before filter to handle some of the error cases?

The key is to take small steps, run your tests to verify, and see how things feel to you.

As an aside, it appears your authentication scheme here is relying on plaintext passwords being stored in the database. If this code is anything more than an exercise I would probably move to something like Clearance for authentication as it worries about such things so you donā€™t have to :wink:

@scott, like we discussed in the Learn Campfire room, hereā€™s my thoughts on this:

If this will be in production, I would suggest you use a gem like Clearance or Devise and not trying to implement an authentication system yourself.

Second, if you just want to learn about authentication systems, I would recommend reading the source code for Clearance or Devise: fork the repos, run the tests, poke around in the code. If you take this option, Iā€™d recommend studying Clearance first. Devise is pretty extensive and may bring more confusion than enlightenment initially.

Lastly, if this is really just for practice, I would probably try to move stuff over to a class that handles this.
Here is an example from the Code Climate Blog:
7 Patterns to Refactor Fat ActiveRecord Models | Code Climate (see point NĀŗ 2 - Extracting Service Objects).

I donā€™t want to pile on, but I canā€™t stress enough that you should not be writing an authentication system from scratch for use in a production system.

@derekprior It is a create for a session yes sorry. I am using bcrypt so the passwords are being hashed and salted into the db and not stored in plain text.
The authenticate method is also part of bcrypt, this is the code if you want to have a better look https://github.com/scott2619/aas/tree/products_model.

@pedromoreira Thanks I will have a look.

@cpytel I am doing this as an exercise so I can see how things are working.

@scott Great, Iā€™m glad to hear its not for production.

@cpytel How would you make the create method above better?

Building on what @derekprior wrote, I think you could also flatten out the nested conditionals by changing the order of the clauses, which would make things a bit more readable:

def create
  if user.nil?
    # tell them they are invalid
  elsif user.verified
    # sign them in
  else
    # tell them to verify
  end
end

A few other things you might want to consider:

  • Ending methods that return a boolean with a ?, for example user.verified? instead of user.verified (if verified is a database field on an ActiveRecord model then youā€™ll already have a verified? method).
  • Extracting each branch of the conditional as a private method, so I donā€™t have to read and understand all of the details of each branch to understand the create method.
1 Like

@georgebrock Thanks in @derekpriorā€™s user method he does

however authenticate is a method from BCrypt so could I do this? or is there a better way?

def user(user)
  @user ||= user.authenticate(params[:password]) if user
end

Thanks

I have now come up with this, have I made it better or worse?

If worse how could I improve?

class SessionsController < ApplicationController
  def new
    redirect_to companies_path if current_user
  end

  def create
    if user.nil?
      failed_login("Invalid email address or password!")
    elsif user.verified?
      sign_in(user)
    else
      failed_login("Please verify your account before logging in")
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_url, notice: "Logged out!"
  end

  private

  def user
    @user || authenticate
  end

  def authenticate
    user = User.find_by_email(params[:email])
    user.authenticate(params[:password]) if user
  end

  def sign_in(user)
    session[:user_id] = user.id
    flash.notice = "Successfully signed in!"
    redirect_to companies_path
  end

  def failed_login(message)
    flash.now.alert = message
    render "new"
  end

end

1 Like

Can you please edit your comment to provide the whole controller, or at least any before filters?

I have edited the post :slight_smile:

I donā€™t understand how @user would be set in def user, is that correct?

I donā€™t fully understand the user method myself, It was what @derekprior suggested.

I thought it was to catch the nil case of user.nil ?

Should it be @user ||= authenticate ?

1 Like

Yes, ||= is what I meant. It is acting as an accessor for the user variable but also performing the authentication the first time its called. Itā€™s a little awkward because itā€™s quite possible authenticate will return nil, in which case calling user again will cause authenticate to run again, etc, etc. In this use case it works, though, because the very first thing you do with user is handle the case where its nil.

Thank you for clearing that up for me @derekprior