gem 'devise'
Let's add the devise gem to our travel app.
And then bundle install in our command line:
$ bundle install
First we add this line to our gemfile:
$ rails g devise:install
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
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
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 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>
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 %>
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.
Let's test this and see if everything is working as intended. Click around and see if the flow makes sense.
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 %>
We link to the edit_user_registration_path
$ rails g 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>
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>
Now let's add some bootstrap form classes, replace labels with placeholders on the input fields and a button class.
views/devise/sessions/new.html.erb
<span class="input-group-addon glyphicon glyphicon-envelope"></span>
<span class="input-group-addon glyphicon glyphicon-lock" ></span>
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>
Final login page code:
$ rails g migration AddNameToUsers first_name:string last_name:string
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
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
Add a welcome message on the homepage for the current user.
<h1>Welcome to the Travel Site, <%= current_user.first_name %></h1>
Can you mimic this styling on the sign up page?
views/devise/registrations/new.html.erb
def show_users
@users = User.all
end
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'
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
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 %>
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
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
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
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 %>
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."
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