Devise & CanCan

 gem 'devise'

Devise gem

Let's add the devise gem to our travel app. 

https://github.com/plataformatec/devise

And then bundle install in our command line:

$ bundle install

First we add this line to our gemfile:

$ rails g devise:install

run the Generator

 The generator will install an initializer which describes ALL Devise's configuration options. 

Notice the 2 files that were created:

config/initializers/devise.rb create config/locales/devise.en.yml

 

$ rails g devise User

Devise model

Now it's model time. It is customary to model a "User" in devise.

This will create a model file and a migration file:

 

$ rake db:migrate
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

  before_action :authenticate_user!

end

Controller filter

Let's require users to log in before entering any page of our app. We'll add the following filter to our application controller:

Where would we move this filter if we wanted the home page to be available to all users, but the destinations page behind the login?

Rake

$ rake routes

Run this command to see all of your routes. There are about 15 new ones specific to devise.

  
  <div class="navbar-right">
    <% if user_signed_in? %>
      You are logged in as <%= current_user.email %> | 
      <%= link_to "Log out", destroy_user_session_path, method: :delete %>
    <% else %>
      You are not logged in.
      <%= link_to "Log in", user_session_path %> or
      <%= link_to "Sign Up", new_user_registration_path %>
    <% end %>
  </div>

Add to nav

Let's add some links with a bit of logic to our navbar so we can see devise in action. Both "user_signed_in?" and "current_user" are methods made available by devise. 

<% if notice != nil && user_signed_in? %>
  <p id="notice" class="alert alert-success" role="alert">
    <%= notice %>
  </p>
<% end %>

Notice

Let's add some code that lets the user know their login was successful. This will go in application.html.erb:

But some pages already have notices enabled like the destinations index and show pages. Let's remove these to prevent duplication when creating a new destination.

 

Test it!

Let's test this and see if everything is working as intended.  Click around and see if the flow makes sense.

 

 

Link Challenge

How could you link the user's email address in the authenticated nav to the edit user registration page?

 

Hint: rake your routes again...

 

 

You are logged in as 
<%= link_to "#{current_user.email}", edit_user_registration_path %>

We link to the edit_user_registration_path

You are logged in as 
<%= link_to "#{current_user.email}", edit_user_registration_path %>

Link solution

We link to the edit_user_registration_path

$ rails g devise:views

Devise views

This is how we generate editable devise views in our code. We will be editing these shortly:

Notice a new batch of files that were created in our views folder:


<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <h2>Log in</h2>

    <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
    <div><%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true %></div>

    <div><%= f.label :password %><br />
    <%= f.password_field :password, autocomplete: "off" %></div>

    <% if devise_mapping.rememberable? -%>
      <div><%= f.check_box :remember_me %> <%= f.label :remember_me %></div>
    <% end -%>
 
    <div><%= f.submit "Log in" %></div>
    <% end %>

    <%= render "devise/shared/links" %>
  </div>
</div>

Login Page

Let's snazz up our sad looking login page. First let's add a bootstrap grid with a 6 column width, offset by 3 so it's centered on the page.

views/devise/sessions/new.html.erb

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <h2>Log in</h2>
      <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
        <div class="input-group input-group-lg">
          <%= f.email_field :email, autofocus: true, placeholder: "Email", class: "form-control" %>
	</div>
	<br/>
	<div class="input-group input-group-lg">
	   <%= f.password_field :password, autocomplete: "off", placeholder: "Password", class: "form-control" %>
	</div>
	<br/>
        <div><%= f.submit "Log in", class: "btn btn-primary btn-lg"%></div>
      <% end %>
     <%= render "devise/shared/links" %>
  </div>
</div>

Login Page

Now let's add some bootstrap form classes, replace labels with placeholders on the input fields and  a button class. 

http://getbootstrap.com/components/#input-groups

views/devise/sessions/new.html.erb

 <span class="input-group-addon glyphicon glyphicon-envelope"></span>
     
 <span class="input-group-addon glyphicon glyphicon-lock" ></span>
   

login page

Finally let's add some glyphicons to give it some flair!

Let's remove the partial link reference. How could we add back just the "forgot password?" link under the password field?


<div class="row">
  <div class="col-md-6 col-md-offset-3">
     <h2>Log in</h2>
       <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
            		  
       <div class="input-group input-group-lg">
         <span class="input-group-addon glyphicon glyphicon-envelope"></span>
         <%= f.email_field :email, autofocus: true, placeholder: "Email", class: "form-control" %>
       </div>
       <br/>
       
       <div class="input-group input-group-lg">
         <span class="input-group-addon glyphicon glyphicon-lock" ></span>
         <%= f.password_field :password, autocomplete: "off", placeholder: "Password", class: "form-control" %>
       </div>
       <p class="pull-right">
         <%= link_to "Forgot Password?", new_user_password_path %>
       </p>
       <br/>
       <div class="input-group input-group-lg"> 
         <%= f.submit "Log in", class: "btn btn-primary btn-lg"%>
       </div>
            
       <% end %>
  </div>
</div>

login page

Final login page code:

$ rails g migration AddNameToUsers first_name:string last_name:string

Add Name Fields

How would we add "first_name" and "last_name" to our sign up page?

Then add the fields to the sign_up page:

app/views/devise/registrations/new.html.erb
$ rake db:migrate
<div><%= f.label :first_name %><br/>
  <%= f.text_field :first_name, autofocus:true %> 
</div>

<div><%= f.label :last_name %><br/>
  <%= f.text_field :last_name %> 
</div>
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.for(:sign_up) { |u| u.permit({ roles: [] }, :email, :password, 
    :password_confirmation, :first_name, :last_name) }

   devise_parameter_sanitizer.for(:account_update) { |u| u.permit({ roles: [] }, :email, :password, 
   :password_confirmation, :current_password, :first_name, :last_name) }
  end

Permitted parameters

Now we have to add first_name and last_name to our permitted parameters action so Rails will allow us to save them to the database.

 

app/controllers/application_controller.rb

Custom message

Add a welcome message on the homepage for the current user.

<h1>Welcome to the Travel Site, <%= current_user.first_name %></h1>

Sign up Activity

Can you mimic this styling on the sign up page?

views/devise/registrations/new.html.erb

 def show_users
   @users = User.all
 end

show users page

Let's add a view so we're able to browse our users. We'll add a new action to our welcome controller and a new page in the welcome folder called show_users.html.erb

<h1>Show Users</h1>
  <table>
    <tr>
      <td>User Id</td>
      <td>First Name</td>
      <td>Last Name</td>
      <td>Email Address</td>
    </tr>	
    <% @users.each do |user| %>
      <tr>
        <td><%= user.id %></td>
	<td><%= user.first_name %></td>
	<td><%= user.last_name %></td>
	<td><%= user.email %></td>
      </tr>
  <% end %>	
</table>
gem 'cancan'

do the CanCan

Let's add a new gem called 'cancan.' This will allow us to control user roles so that only admins can edit content.  https://github.com/ryanb/cancan

$ bundle install

Add the gem to your gemfile and bundle!

$ rails g cancan:ability

You CanCan

We now create a model specific to cancan. 

Now we set roles. Admins can manage all content.

app/models/ability.rb

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new # guest user (not logged in)
    if user.admin?
      can :manage, :all
    else
      can :read, :all
    end
  end
end
 <% if can? :update, destination %>
   <div class="btn-group">
     <%= link_to 'Show', destination, class: "btn btn-default" %>
     <%= link_to 'Edit', edit_destination_path(destination), class: "btn btn-default" %>
     <%= link_to 'Destroy', destination, method: :delete, data: { confirm: 'Are you sure?' }, 
     class: "btn btn-default" %>
   </div>
 <% end %>

Remove Crud buttons

for non admins

Admins should be the only ones allowed to create, update, and delete (CUD?) on our destinations page. So let's add some logic around our button links in the destination index page:

<% if can? :update, @destination %>
  <%= link_to 'New Destination', new_destination_path, class: "btn btn-primary" %>
<% end %>

And don't forget the new destination button!

$ rails g migration AddAdminToUsers admin:boolean

Add Admin

This all fine and good, but we don't actually have an admin column defined in our database. So let's add one.

Now let's set admin to be true for the user of our choice using rails console. User.all will display all users in the db.

$ rails c
> User.all
$ rake db:migrate
> admin_guy = User.find_by_id(1)
> admin_guy.admin = "true"
> admin_guy.save

Add Admin

Make sure you have at least 2 users in your database for testing purposes. We'll set one of them to have "true" in their admin field. Find the id of the user you'd like to promote to top dog!

Now try logging in with your admin user and navigating to the destinations page. Notice your edit buttons are in place. Log in with a regular non-admin user and notice the buttons are missing.

load_and_authorize_resource

Authorization

In order to automatically authorize all actions on the destinations page we must add this line to the top of the destinations controller:

We'll also need this method at the bottom of the application controller to redirect in case of an exception:

 rescue_from CanCan::AccessDenied do |exception|
    redirect_to destinations_path, :alert => exception.message
  end

And output the exception message on the view

<% if alert %>
  <div id="notice" class="alert alert-danger" role="alert"><%= alert %></div>
<% end %>

Authorization

Finally, we'll output the exception message on the destination index view

<% if alert %>
  <div id="notice" class="alert alert-danger" role="alert"><%= alert %></div>
<% end %>

Go ahead and test this out. Try to browse to "destinations/new" with a non-admin user logged in. The user should be re-routed to the destination index page with error message that says:

 

"You are not authorized to access this page."

Homework

Clone my "to do list app" on github. Add devise and cancan and push it to your own github page.

 

https://github.com/jpanaia/todolist

 

 

Devise & Cancan

By tts-jaime

Devise & Cancan

Devise and Cancan gems for FT

  • 1,150