Recently, I was involved in a new project that uses Rails 3. That was my first Rails 3 project, I’ve always used Rails 2 before. Therefore, I tried to get accustomed to how Rails 3 works and how it differs from Rails 2. One thing that I really had to change back then was what authentication and authorization plugin to use. In my previous projects, I always used Authlogic and Lockdown. When I started this particular project, it seemed that both Authlogic and Lockdown was not Rails 3 ready (but I heard now both are Rails 3 ready). So I decided to give a try to different plugins. That’s how I found Devise and CanCan.
Most part of this writing is heavily duplicated from tutorials from Devise’s official github page, Asciicast’s episode on CanCan by Ryan Bates, and Tony Amoyal’s tutorial. I took most of the codes from these resources and then slightly modified it to meet my own needs. In this post, I try to share my experience working with both Devise and CanCan. I’ll explain it in a step-by-step style, maybe some steps are just too obvious for expert readers, please bear with it.
The Project
I wanted to have a user management feature. In the project, we had several roles with predefined sets of features that each role can access. The system has an administrator roles. Users can’t freely register to the system, the admins add them to the system and then assign a role to them. In the actual system I worked with, users passwords are generated by the application and then sent to the respective user’s mail. To keep this post simple, I’ll make administrator set the password directly.
1. Adding needed gems
Add these lines to your Gemfile:
1 2 | gem 'devise' gem 'cancan' |
then install the gems with this command from your command line:
1 | bundle install |
2. Adding Devise to your project
Run these commands from your command line:
1 2 | rails generate devise:install rails generate devise User |
3. Modifying user model and migration file
As said before, I needed the user management feature to be able to add, delete, and modify users through a CRUD mechanism. Therefore, there are several things we need to change in our user model and migration file. First, we delete the “registerable” attribute. Second, we add a “username” attribute to the model and to the migration file. Here is how your user model should look like:
1 2 3 4 5 6 7 8 9 | class User < ActiveRecord::Base # Include default devise modules. Others available are: # :token_authenticatable, :confirmable, :lockable and :timeoutable devise :database_authenticatable, :recoverable, :rememberable, :trackable, :validatable # Setup accessible (or protected) attributes for your model attr_accessible :email, :username, :password, :password_confirmation, :remember_me end |
and your migration file for user should look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | class DeviseCreateUsers < ActiveRecord::Migration def self.up create_table(:users) do |t| t.database_authenticatable :null => false t.recoverable t.rememberable t.trackable t.string :username # t.confirmable # t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :both # t.token_authenticatable t.timestamps end add_index :users, :email, :unique => true add_index :users, :reset_password_token, :unique => true # add_index :users, :confirmation_token, :unique => true # add_index :users, :unlock_token, :unique => true end def self.down drop_table :users end end |
4. Modifying the routes
When we installed Devise, it added it’s own routes to our routes.rb file. Since we want to have CRUD ability, we have to add another routes for our user resources. So this is what we do:
1 2 3 4 5 6 | DeviseTest::Application.routes.draw do devise_for :users resources :users # add another lines as you need... end |
5. Creating the controller and views
In this step, we can just use a generic controller and views template that usually created when you use rails generator to do a scaffolding. Anyway, this is how my users_controller.rb looks:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | class UsersController < ApplicationController # GET /users # GET /users.xml def index @users = User.all respond_to do |format| format.html # index.html.erb format.xml { render :xml => @users } end end # GET /users/1 # GET /users/1.xml def show @user = User.find(params[:id]) respond_to do |format| format.html # show.html.erb format.xml { render :xml => @user } end end # GET /users/new # GET /users/new.xml def new @user = User.new @current_method = "new" respond_to do |format| format.html # new.html.erb format.xml { render :xml => @user } end end # GET /users/1/edit def edit @user = User.find(params[:id]) end # POST /users # POST /users.xml def create @user = User.new(params[:user]) respond_to do |format| if @user.save format.html { redirect_to(@user, :notice => 'User was successfully created.') } format.xml { render :xml => @user, :status => :created, :location => @user } else format.html { render :action => "new" } format.xml { render :xml => @user.errors, :status => :unprocessable_entity } end end end # PUT /users/1 # PUT /users/1.xml def update @user = User.find(params[:id]) respond_to do |format| if @user.update_attributes(params[:user]) format.html { redirect_to(@user, :notice => 'User was successfully updated.') } format.xml { head :ok } else format.html { render :action => "edit" } format.xml { render :xml => @user.errors, :status => :unprocessable_entity } end end end # DELETE /users/1 # DELETE /users/1.xml def destroy @user = User.find(params[:id]) @user.destroy respond_to do |format| format.html { redirect_to(users_url) } format.xml { head :ok } end end end |
Again, although this is a generated view, I’ll include it here anyway. Here’s our _form.html.erb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | <%= form_for(@user) do |f| %> <% if @user.errors.any? %> <div id="error_explanation"> <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2> <ul> <% @user.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= f.label :email %><br /> <%= f.text_field :email %> </div> <div class="field"> <%= f.label :username %><br /> <%= f.text_field :username %> </div> <% if @current_method == "new" %> <div class="field"> <%= f.label :password %><br /> <%= f.password_field :password %> </div> <div class="field"> <%= f.label :password_confirmation %><br /> <%= f.password_field :password_confirmation %> </div> <% end %> <div class="actions"> <%= f.submit %> </div> <% end %> |
Our edit.html.erb:
1 2 3 4 5 6 | <h1>Editing user</h1> <%= render 'form' %> <%= link_to 'Show', @user %> | <%= link_to 'Back', users_path %> |
Our index.html.erb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <h1>Listing users</h1> <table> <tr> <th>Email</th> <th>Username</th> </tr> <% @users.each do |user| %> <tr> <td><%= user.email %></td> <td><%= user.username %></td> <td><%= link_to 'Show', user %></td> <td><%= link_to 'Edit', edit_user_path(user) %></td> <td><%= link_to 'Destroy', user, :confirm => 'Are you sure?', :method => :delete %></td> </tr> <% end %> </table> <br /> <%= link_to 'New user', new_user_path %> |
Our new.html.erb:
1 2 3 4 5 | <h1>New user</h1> <%= render 'form' %> <%= link_to 'Back', users_path %> |
And the last… our show.html.erb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <p id="notice"><%= notice %></p> <p> <b>Email:</b> <%= @user.email %> </p> <p> <b>Username:</b> <%= @user.username %> </p> <%= link_to 'Edit', edit_user_path(@user) %> | <%= link_to 'Back', users_path %> |
6. Using the Devise
As noted by Peter in the comment section, I forget to put this step in my original post. In order Devise to really work, we have to put this line in every controller that can only be accessed by authenticated users.
1 | before_filter :authenticate_user! |
Next Step
Well, it’s a long post already… . I guess it’s better for us to take a break. So, this is the first part. We have not used CanCan yet, but now we have a working user management feature as we wanted it in the beginning. Hope this post helps you with your own project using Devise. I’ll write the second part where we will use CanCan as soon as possible.
What about adding…
before_filter :authenticate_user!
…to the top of your User controller to make sure user is logged in before accessing this controller. I think it is a required Devise customization.
@ Peter:
You’re right! I totally forget about it in this blog post up until now. Thanks for your correction, I’ll add it in this post soon.
[...] Devise and CanCan with Rails 3. You can find the first part of this tutorial at the original blog here or at this site [...]
[...] This article is also published in Iqbal’s blog here. [...]
Great post. It was very helpful!
If I wanted to turn the check box into a single hidden field to give it a default role for users. Do you have any suggestions how I could do that. Thanks
Hi , and thanks for the tutorial, i ‘m novice in rails and i have one question, if you delete the registrations from the migrations how can sign up a new user (the first times) because we must be sign in to do this !!!!
and thx
Hi and thanx for the tutorial , i have one questions
If you delete the registrations from the migration how can’i sign in for the fisrt time via the browser because when a try to sign up a new user ( the message is => You need to sign in or sign up before continuing.)
and thanks
Hey ach4rails, as I said in “The Project” part, this was built following my project’s need back then. In my project, users are added by administrator. Therefore there was user CRUD page. If you wish to allow new users to register, simply put back :registerable option in your User model.
Thanks for one’s marvelous posting! I seriously enjoyed reading it, you might be a great author.I will ensure that I bookmark your blog and will come back later in life. I want to encourage one to continue your great job, have a nice evening!