Rails Authentication and 1:M Associations

Objectives

  • Create users and store their passwords securely
  • Enable the ability to authenticate users and store sessions once logged in
  • Utilize filters and validations in Rails
  • Establish 1:M relationships

Remember all that hassle of setting up authentication in Node? Rails makes it easy.

Create a new project

You should know how to do this now. If not, see notes from Intro to Rails.

Create a user model

We need to first start creating a user model that has a username/email field and a password_digest. Note that you have to name the field this.

rails g model user email password_digest
rails db:migrate

Add some validations

http://guides.rubyonrails.org/active_record_validations.html

app/models/user.rb

validates :email,
presence: true,
uniqueness: {case_sensitive: false}

Note that we're only checking for presence and uniqueness of the email. If we want to validate the format of the email, we can use an Regex like so

validates :email,
presence: true,
uniqueness: {case_sensitive: false},
format: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i

or we can use this gem for a more comprehensive comparrison.

Add password hashing

  • Add has_secure_password to the user model
  • uncomment gem 'bcrypt' on your Gemfile and run the bundler

Now that we have has_secure_password, Rails gives out a password setter that we can use in validations even though we don't have a password field in our database.

validates :password, length: { in: 8..72 }, on: :create

Register

We now have everything we need to create a user, and we can do so using Rails Console.

User.create!(email: '[email protected]', password: '12345678')

But that's no use to our users, so let's add a registration page.

Add the register page

Let's create a users controller to handle registering.

rails g controller users new

Manually add a controller action to create - as it won't have a view.

Add the routes

get "register" => "users#new"
post "register" => "users#create"

Create a Registration Form

<h1>Register</h1>

<%= form_for @user, url: register_path do |f| %>
  <%= f.email_field :email, placeholder: "Enter your email" %>
  <%= f.password_field :password, placeholder: "Enter your password" %>
  <%= f.password_field :password_confirmation, placeholder: "Please confirm it" %>
  <%= f.submit "Register" %>
<% end %>

Controller Actions

We need to fill up our controller to serve these requests.

def new
  @user = User.new
end

def create
  @user = User.new(user_params)
  if @user.save
    flash[:success] = "Account Created. Please Login"
    redirect_to root_path
  else
    render :new
  end
end

private

def user_params
  params.require(:user).permit(:email, :password, :password_confirmation)
end

We should now be able to register a new user, next we want to be able to log them in.

Logging In & Out (Sessions)

Using had_secure_password, we gain an authenticate method that we can call on any instance of a user and compare a password. It will return the user if the password is valid else false, for example...

User.find_by_email('[email protected]').try(:authenticate, '123')

We can add a class method to our User model to wrap this up and find and return a user only if it exists and the password is valid - based on the passed in params from the controller.

Add a helper method to the class

app/models/user.rb

def self.find_and_authenticate_user(params)
  User.find_by_email(params[:email]).try(:authenticate, params[:password])
end

The finished User model

class User < ActiveRecord::Base
  validates :email,
  presence: true,
  uniqueness: {case_sensitive: false}

  validates :password,
  length: { in: 8..72 },
  on: :create

  has_secure_password

  def self.find_and_authenticate_user(params)
    User.find_by_email(params[:email]).try(:authenticate, params[:password])
  end
end

Add the login pages

Let's create a session controller to handle logging in/out. We'll organize this by calling the controller sessions, because in reality, we're creating and destroying sessions on login and logout.

rails g controller sessions new

manually add actions create and destroy - they won't have views.

Lets create some routes

get "login" => "sessions#new"
post "login" => "sessions#create"
delete "logout" => "sessions#destroy"

Lets generate a form

<h1>Login</h1>

<%= form_for :user do |f| %>
  <%= f.email_field :email, placeholder: "Enter your email" %>
  <%= f.password_field :password, placeholder: "Enter your password" %>
  <%= f.submit "Login" %>
<% end %>

Wait, why are we using the symbol? See this StackOverflow answer

Authenticate

Authenticate the user on sessions#create

def create
  user = User.find_and_authenticate_user(user_params)

  if user
    session[:user_id] = user.id
    flash[:success] = "User logged in!!"
    redirect_to root_path
  else
    flash[:danger] = "Credentials Invalid!!"
    redirect_to login_path
  end
end

def destroy
  session[:user_id] = nil
  flash[:success] = "User logged out!!"
  redirect_to root_path
end

private

def user_params
  params.require(:user).permit(:email, :password)
end

Add current User capabilities

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  def is_authenticated
    unless current_user
      flash[:danger] = "Credentials Invalid!!"
      redirect_to login_path
    end
  end

  def current_user
    @current_user ||= User.find_by_id(session[:user_id])
  end

  helper_method :current_user
end

The final line tells rails that we want the current_user method to be available inside of our views not just our controllers.

Adding Flash Messages

The flash hash is accessible in every Rails controller and view. To access it, we'll need a way to iterate through the hash and print out the keys and values. The best way is to create a partial and include it on the layout (so it'll be on every page).

Partials have to start with an underscore in Rails. We can render the partial by using the render helper.

With a partial at app/views/partials/_flash.html.erb

<%= render "partials/flash" %>

_flash.html.erb

<% flash.each do |key, value| %>
  <div class="alert alert-<%= key %>">
    <%= value %>
  </div>
<% end %>

Protect a controller or action

To protect a controller or action we can simply add a before_action :is_authenticated at the top. For example, let's add a profile page that you can only see when logged in.

class UsersController < ApplicationController
  before_action :is_authenticated, only: [:profile]

  ...

  def profile
  end

Let's add a view.

users/profile.html.erb

<h1>Hello <%= current_user.email %></h1>

finally add a route to get there. We can also make it the default (root) route of our site.

  root 'users#show'
  get "profile" => "users#show"

Adding 1:M relationships with another model

Let's first add another model to relate to the user. In order for the user to have many pets, we can create the model by including the model name and references as the type.

rails g model pet name user:belongs_to

This will make the following migration, which will include a userId in the pet model.

class CreatePets < ActiveRecord::Migration
  def change
    create_table :pets do |t|
      t.string :name
      t.references :user, index: true, foreign_key: true

      t.timestamps null: false
    end
  end
end

Then, make sure to migrate and include the associations in each model.

rails db:migrate

models/user.rb

class User < ActiveRecord::Base
  has_many :pets

  # ...
end

models/pet.rb

class Pet < ActiveRecord::Base
  belongs_to :user
end

Now try testing in the Rails console

User.first.pets

User.first.pets.create(name: 'Fido')

Pet.all

Pet.first

Pet.first.user

Once we create the CRUD controller for our pets, we can easily associate models with the logged in user, through the current_user method.

current_user.pets

current_user.pets.create(name: 'Fido')

current_user.pets.find_by(id: params[:id])

The last example is useful for stopping a user accessing a pet they didn't create. The query is effectively looking for a Pet with a particular id and also a particular user_id.

Relationship Constraints

Rails enables us to add additional properties to our associations. For instance we can tell rails to automatically delete all pets, if we delete their owner.

models/user.rb

class User < ActiveRecord::Base
  has_many :pets, dependent: :destroy

  # ...
end

If you don't want to delete the associated method but you want it to always have an owner then you can use a before_destroy action to reassign it, for example...

models/user.rb

  has_many :pets
  before_destroy :re_home

  private

  def re_home
    # you can be more specific but here we just randomly pick another user, whose is not this one. If no user is found only then do we destroy the pets
    new_owner = User.where.not(id: id).sample(1).first
    if new_owner
      pets.update_all(user_id: new_owner.id)
    else
      pets.destroy_all
    end
  end

Optional Ownership

Rails 5, will also stop us from creating a record that has a belongs_to association, unless that association exists. For example, we cannot create a Pet without an owner.

We can disable this behavior if we want using the optional flag.

models/pet.rb

class Pet < ActiveRecord::Base
  belongs_to :user, optional: true
end

results matching ""

    No results matching ""