Criando Uma App Com Devise CanCan e Bootstrap

Recentemente surgiu essa dúvida no GuruPI. Para tentar ajudar aqueles que estão começando, bem como meu colega que estava com a dúvida, resolvi criar esse post e colocar a app criada no Github.

Sobre as gems usadas no post

Devise

Uma gem bastante fexível para autenticação em aplicações web, bem como genrenciamento dos dados do usuário.

CanCan

Gem que irá nos permitir fazer o gerenciamento de permissões para cada usuário do sistema.

Twitter Bootstrap Rails

Esta gem nos ajudará a reconfigurar toda a estrutura de layout padrão do Rails, nos dando uma visualização mais agradável da aplicação atraves do bootstrap.

Seguindo com o nosso post, essa será mais ou menos a estrutura de nossa aplicação:

1
2
3
4
5
6
7
8
9
User
  belongs_to :role

Role
  has_many :users
  has_and_belongs_to_many :permissions

Permission
  has_and_belongs_to_many :roles

Vamos criar nossa aplicação com o seguinte comando:

rails new exemplo_cancan_e_devise

Depois de criar vamos deletar o seguinte arquivo public/index.html

rm public/index.html

Agora colocaremos as gems que iremos usar no arquivo Gemfile:

1
2
3
4
gem 'devise'
gem 'cancan'
gem 'commonjs', '= 0.2.0' # essa foi pq deu um probleminha quando coloquei a gem do bootstrap
gem 'twitter-bootstrap-rails'

Com isso nossa app sabe que queremos usar essas gems, então vamos executar o seguinte comando, para podermos usar os geradores de cada gem:

bundle install

Eu iriei fazer um breve comentário sobre cada comando usado de cada gem aqui, para se aprofundar mais você deve ir ate a url colocada no inicio do post de cada gem.

Esse comando irar criar um arquivo com o nome devise.rb, dentro de config/initializers com as configurações padrões do devise e colocará devise_for :users, dentro do arquivo config/routes, assim teremos todas as rotas do devise.    
rails generate devise:install

Esse irá criar um model com as definições do devise, bem como uma migration com os campos padrões do devise
rails generate devise user

Comando que executa todas as migrations(nada mais é que uma forma organizada de criar tabelas, deletar, alterar ou criar novos campos no banco de dados).
rake db:migrate

Irá gerar a views usadas pelo devise dentro de app/views/devise
rails generate devise:views

Como esse comando vamos criar um controller dentro da pasta app/controllers, assim poderemos acessar alguns dados do usuário nas views que o devise nao cria.
rails g controller users

Aqui o cancan cria uma classe dentro da pasta models com o nome ability, essa classe irá manter, gerenciar as permissões em cada usuário
rails g cancan:ability

Com o comando scaffold eu posso criar um CRUD rapidamente, passando o nome e logo em seguida os campos com seus tipos.
Aqui ele cria um controller um model uma migration com o campo name do tipo string
rails g scaffold roles name:string

O paramentro role:references irar criar um campo role_id do tipo inteiro dentro da tabela e adiciona belongs_to role, dentro do model Permission
rails g scaffold permissions action:string subject_class:string role:references

rake db:migrate

Com isso vamos colocar os arquivos js, bem como suas chamadas dentro do arquivo application.js encontrado em app/assets/javascritps e de css em app/assets/stylesheets do boostrap.
rails g bootstrap:install

Aqui estou criando um arquivo com nome application com o css do boostrap dentro de views/layouts
rails g bootstrap:layout application fixed

Esses comando da gem bootstrap ira varer as tabelas e inserir dentro das views de cada pasta passada, dentro de app/views, substituindo as tags em html e css, colocando as do boostrap em cada campo da tabela.
rails g bootstrap:themed Users
rails g bootstrap:themed Roles
rails g bootstrap:themed Permissions

Agora vamos criar 2 migrations uma para adicionar role_id dentro da tabela users e a outra para fazer o gerenciamento de muitos para muitos entre Permission e Role.

rails g migration add_role_id_to_users
rails g migration create_table_permissions_roles

Abra os arquivos que se encontram em db/migrate e modifique-os:

1
2
3
def change
  add_column :users, :role_id, :integer #adicionando coluna role_id do tipo inteiro na tabela users
end
1
2
3
4
5
create_table :permissions_roles, :id => false do |t|
  t.integer :permission_id
  t.integer :role_id
end
#criando a tabela permissions_roles, o id => false, estou dizendo para o rails nao criar a tabela com o campo id, e lembrando que nossa tabela muitos para muitos o nome deve ser em ordem alfabetica dos models

Execute novamente:

rake db:migrate

Depois de tudo isso feito nossa aplicação esta quase pronta, faltando apenas algumas validações e modificações no layout e a configuração do arquivo ability.

Começaremos pelas validações e relacionamentos, como existia a necessidade de termos a permissão para cada view no banco, apareceu o relacionamento muitos para muitos entre Permission e Role, logo vamos deixa-los assim:

1
2
3
4
5
6
class Permission < ActiveRecord::Base
  has_and_belongs_to_many :roles

  validates_uniqueness_of :action, :scope => :subject_class
  validates_presence_of :action, :subject_class
end
1
2
3
4
5
6
7
class Role < ActiveRecord::Base
  has_many :users, :dependent => :restrict
  has_and_belongs_to_many :permissions

  validates_presence_of :name, :permissions
  validates_uniqueness_of :name
end

Aproveitando vamos colocar também no model User.

1
2
3
4
5
6
7
8
9
10
11
class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  # Setup accessible (or protected) attributes for your model
  attr_accessible :email, :password, :password_confirmation, :remember_me, :role_id
  validates_presence_of :role_id
  belongs_to :role
end

Explicando:

Com o has_and_belongs_to_many, estou dizendo que existe um relacionameto muitos para muitos.

has_many :users, :dependent => :restrict, aqui estou dizendo que Role pode ter muitos usuários, mas que se existir algum usuário com um role, esse role nao pode ser deletado(:dependent => :restrict).

belongs_to aqui entende-se que dentro dessa tabela que o usa nos temos um campo id de outra tabela.

validates_uniqueness_of :action, :scope => :subject_class, aqui eu estou dizendo que o campo action deve ser unico e passando :scope eu faço uma verificação de exclusividade com o campo subject_class, assim eu nao vou poder ter 2x action index para subject_class User. Isso simplesmente irá evitar de termos 2x a mesma permissão dentro de Role.

validates_presence_of aqui ele verifica se o campo foi preenchido.

Assim como o cancan, o :dependent => :restrict gera uma exception, fazendo com que a aplicação não tenha um comportamento adequado, para ajustarmos isso, adicionamos os seguintes códigos dentro de app/controllers/application_controller.rb

1
2
3
4
5
6
7
  rescue_from CanCan::AccessDenied do |exception|
    redirect_to root_url, :alert => exception.message
  end

  rescue_from ActiveRecord::DeleteRestrictionError do |exception|
      redirect_to :back, :alert => exception.message
  end

Agora vamos até o arquivo app/models/ability.rb e modifica-lo:

1
2
3
4
5
6
7
8
9
10
11
12
class Ability
  include CanCan::Ability

  def initialize(user)
  can :manage, :all if user.role.name == 'admin'
    user.role.permissions.each do |permission|
      can permission.action.to_sym, permission.subject_class.constantize do
          user.id.nil? || user.role_id?
      end
    end
  end
end

O que basicamente ele faz é verifica se o role do usuario logado é admin, se for o usuário tera todas as permissões na aplicação, agora senão for admin, ele irá verificar as permissões do usuário para cada action em cada model.

Agora vamos fazer as modificações na view devise/registration/edit.html.erb e new.html.erb, para podermos modificar e adicionar o role do usuário, adicione o seguinte trecho:

1
2
3
4
5
6
<div class="control-group">
  <%= f.label :role_id, :class => 'control-label' %>
  <div class="controls">
    <%= f.collection_select :role_id, roles, :id, :name, :include_blank => "--- Select ---" %>
  </div>
</div>

Hum têm alguns helpers que devem ser criados, vamos ao arquivo app/helpers/application_helper.rb e coloque isso:

1
2
3
4
5
6
7
  def get_model_names_sub
      Dir['app/models/*.rb'].map {|f| File.basename(f, '.*').camelize.constantize.name }.reject!{|m| m.constantize.superclass != ActiveRecord::Base }
  end

  def roles
  Role.all
  end

O primeiro me retorna todos os Models da aplicação e o segundo todos os roles.

Seguindo a leitura da dúvida do meu colega, tem-se uma imagem, como eu não queria criar uma tabela para fazer os gerenciamentos dos models da app, pensei em usar somente um metodo para fazer isso é aqui que entra o helper get_model_names_sub lol.

Abrimos o arquivo _form.html.erb da pasta views/roles e modificamos:

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
<%= form_for @role, :html => { :class => 'form-horizontal' } do |f| %>
  <fieldset>
    <legend><%= controller.action_name.capitalize %> /Role</legend>

    <div class="control-group">
      <%= f.label :name, :class => 'control-label' %>
      <div class="controls">
        <%= f.text_field :name, :class => 'text_field' %>
      </div>
    </div>

  <% get_model_names_sub.each do |models_names| %>    
    <div class="control-group">
      <%= field_set_tag models_names do %>
        <% Permission.where(:subject_class => models_names).each do |permission| %>
          <label class="checkbox">
            <%= check_box_tag 'role[permission_ids][]', permission.id, @role.permissions.include?(permission) %>
            <%= permission.action %>      
          </label>
        <%end%>
      <%end%>
    </div>
  <%end%>

    <div class="form-actions">
      <%= f.submit nil, :class => 'btn btn-primary' %>
      <%= link_to 'Cancel', roles_path, :class => 'btn' %>
    </div>
  </fieldset>
<% end %>

Ficou um pouco feio, mas o post ja estava muito grande, então vamos seguindo.

Eu criei um arquivo seed, então é so dar uma olhada aqui, para os que não sabem esse arquivo serve para popular o banco de dados e fica dentro da pasta db. O comando é o seguinte:

rake db:seed

Ah falta modificar o menu, abra o arquivo app/layouts/application.html.erb:

1
2
3
4
5
6
7
8
9
10
11
        <% if user_signed_in? %>
          <ul class="nav">
            <li><%= link_to "Users", users_path if can? :read, 'User' %></li>
            <li><%= link_to "Roles", roles_path if can? :read, 'Role' %></li>
            <li><%= link_to "Permissions", permissions_path if can? :read, 'Permission' %></li>
          </ul>
          <ul class="nav pull-right">
            <li><%= link_to current_user.email, edit_user_registration_path %></li>
            <li><%= link_to "Logout", destroy_user_session_path, :method => :delete %></li>
          </ul>
        <%end%>

Assim o menu só será exibido se o usuário tiver permissão, também ficou faltando adicionar load_and_authorize_resource dentro dos controllers, sem ele o cancan não vai poder fazer as verificações de permissão.

E finalizando o nosso controller de users:

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
class UsersController < ApplicationController
  
 before_filter :authenticate_user!
 load_and_authorize_resource

 def index
    @users = User.all

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @users }
    end
  end

  # GET /users/1
  # GET /users/1.json
  def show
    @user = User.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.json { render json: @user }
    end
  end

end

Teve tambem essa pequena adição no arquivo app/views/layouts/application.html.erb:

1
2
3
<% flash.each do |name, msg| %>
    <%= content_tag :div, msg, :class => "alert alert-#{name}" %>
<% end %>

Essa dentro do arquivo app/assets/javascript/boostrap_and_overrides.css.less:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.alert-notice {
    background-color: #DFF0D8;
    border-color: #D6E9C6;
    color: #468847;
}

.form-horizontal .control-group:before, .form-horizontal .control-group:after {
    content: "";
    display: inline !important;
}

.radio, .checkbox {
  display: inline-block;
}

E essa dentro do config/application.rb:

1
2
3
4
5
6
7
8
9
ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
    if html_tag =~ /<input/ || html_tag =~/<select/
        %|<div class="control-group error">#{html_tag} <span class="help-inline">#{[instance.error_message].join(', ')}</span></div>|.html_safe
    elsif html_tag =~ /<label/
        %|<div class="control-group error">#{html_tag}|.html_safe
    else
        html_tag
    end
end

Essa é parra mudar as mensagens de validação, as outras são mudanças no comportamento do css.

Simples e fácil lol, o código esta aqui, espero não ter esquecido de nada.

Comments