Rails Admin Feature Implementation
Admin
Admin is administrator. I want to
In this term, admin role requires following
- Admin has name(email) and password
- Admin can manage almost all items in this application
- Data management editor has access control for security
- Everyone can access top(admin top)
Topics are following
namespace and top
I want to access following address for test http://localhost:3000/admin
Use namespace
config/routes.rb
namespace :admin do root 'top#index' # here is additional controller under admin end
Create top controller
Admin::Controller class
rails g controller admin/top
where is view?
app/views/admin/top
Action?
app/controllers/admin/top_controller.rb
class Admin::TopController < ApplicationController def index render action: 'index' # use index template end end [/ruby] <h4>Layout</h4> app/controllers/application_controller.rb [ruby] layout :set_layout private def set_layout if params[:controller].match(%r{\A(others|admin)/}) Regexp.last_match[1] else 'general' end end
Decide layout basically, use layout whose name is same
admin/top -> admin
Administrator Model
Create Administrator model
rails g model Administrator
Edit migration file
class Administrator < ActiveRecord::Migration def change create_table :administrators do |t| t.string :email, null:false # mail address t.string :email_for_index, null: false # index mail address t.string :hashed_password t.boolean :suspended, null: false, default: false # suspend flag t.timestamps end add_index :administrators, :email_for_index, unique: true end end [/ruby] <h4>Delete Model</h4> [bash] rails destroy model xxxx [/bash] Delete model file and migration file <h4>Migration</h4> [bash] rake db:migrate [/bash] If you want to redo from the beginning [bash] rake db:migrate:reset [/bash] <h4>Hashed Password</h4> In Model app/models/administrator.rb [ruby] class Administrator < ActiveRecord::Base before_validation do self.email_for_index = email.downcase if email end def password=(raw_password) if raw_password.kind_of?(String) self.hashed_password = BCrypt::Password.create(raw_password) elsif raw_password.nil? self.hashed_password = nil end end end [/ruby] <h4>Spec Test</h4> <h4>Seed</h4> Details are <a href="http://atmarkplant.com/seedsrb/" title="Populating the database with seeds.rb">Populating the database with seeds.rb</a> Add table name and prepare data under db/seeds/development Seed file is administrators.rb [ruby] Administrator.create( email: 'yoona@snsd.com', password: 'happy-yoona' )
Run seed
rake db:seed
Reset
rake db:reset
Check seed
rails r 'puts Administrator.count'
Session and sign in
- Status the admin sign-in and sign-out
- Status the admin access and close
- Rails should keep admin data while the admin accesses
Create Base class under admin namespace
Add current_administrator method to get current admin from session
app/controllers/admin/base.rb
class Admin::Base < ApplicationController private def current_administrator if session[:administrator_id] @current_administrator ||= Administrator.find_by(id: sesion[:administrator_id]) end end helper_method :current_administrator end [/ruby] helper_method in controller is available in your view. Change controller base class to Admin::Base app/controllers/admin/top_controller.rb [ruby] class Admin::TopController < Admin::Base def index end end [/ruby] <h4>Routing</h4> config/routes.rb [ruby] Rails.application.routes.draw do namespace :admin do get 'login' => 'sessions#new', as: :login post 'session' => 'sessions#create', as: :session delete 'session' => 'sessions#destroy' end end
After creating routes, we can set link for template
app/views/
<header> <%= if current_administrator link_to 'Sign out', :admin_session, method: :delete else link_to 'Sign in', :admin_login end %> </header>
Form
Form has 2 types
- from_tag : FORM
- form_for : Argument form objects(model, form)
Form Example
<% form_for @object, url: :registration do |f| %> <%= f.label :name, 'Name' %> <%= f.text_field :email %> <%= f.submit 'Send' %> <% end %>
f : form builder
url : URL symbol, routing name(config/routes.rb)
Details are Form
Create Form Object
form object : Non Active Record Model instance
We can create form which is not connected database model
Create forms directory
mkdir -p app/forms/admin
Create Form Object
app/forms/admin/login_form.rb
class Admin::LoginForm include ActiveModel::Model attr_accessor :email, :password end
Next, create controller(sessions controller)
rails g controller admin/sessions
app/controllers/admin/sessoins_controller.rb
class Admin::SessionsController < Admin::Base def new if current_administrator redirect_to :admin_root # back to root else @form = Admin::LoginForm.new # Create Form object and set @form render action: 'new' end end end [/ruby] <h4>Sing-in Form and handle form</h4> app/views/admin/sessions/new.html.erb [ruby] <% @title = 'Sign in' %> <div id="login-form"> <h1><%= @title %></h1> <%= form_for @form, url: :admin_session do |f| %> <div> <%= f.label :email, 'Mail adress' %> <%= f.text_field :email %> </div> <div> <%= f.label :password, 'Password' %> <%= f.password_field :password %> </div> <div> <%= f.submit 'Sign in' %> </div> <% end %> </div>
Form processing in session controller
app/controllers/admin/sessions_controller.rb
def create @form = Admin::LoginForm.new(params[:admin_login_form]) if @form.email.present? admin = Administrator.find_by(email_for_index: @form.email.downcase) end if admin session[:administrator_id] = admin.id redirect_to :admin_root else render action: 'new' end end
parameter key is generated from Admin::LoginForm -> admin_login_form
Sign out
Add destroy in controller
app/controllers/admin/sessions_controller.rb
def destroy session.delete(:administrator_id) redirect_to :admin_root end
Service Object
Do something like Action and independent class
Create directory for it
mkdir -p app/services/admin
app/services/admin/authenticator.rb
Implement authentication feature in Service.
class Admin::Authenticator def initialize(administrator) @administrator = administrator end def authenticate(raw_password) @administrator && @administrator.hashed_password && BCrypt::Password.new(@administrator.hashed_password) == raw_password end end
This is Password authentication
Integrate Authentication Service into Session Controller
app/controllers/admin/sessions_controller.rb
def create @form = Admin::LoginForm.new(params[:admin_login_form]) if @form.email.present? admin = Administrator.find_by(email_for_index: @form.email.downcase) end if Admin::Authenticator.new(admin).authenticate(@form.password) session[:administrator_id] = admin.id flash.notice = 'Success sign in' redirect_to :admin_root else flash.now.alert = 'Mail address or password are wrong' render action: 'new' end end
Add Message
Use flash
flash.notice = 'Success sign in' flash.now.alert = 'Mail address or password are wrong'
For example, add these messages to shared header
app/views/admin/shared/_header.html.erb
<%= content_tag(:span, flash.notice, class: 'notice') if flash.notice %> <%= content_tag(:span, flash.alert, class: 'alert') if flash.alert %>
Strong Parameters
Mass assignment vulnerability
This is controller side validation.
config/appliction.rb
comment out
#config.action_controller.permit_all_parameters = true
Receive only permitted parameters.
Example
class Admin::MembersConstoller < Admin::Base def create @member = Member.new(member_params) if @member.save redirect_to :admin_members else render action: 'new' end end def member_params params.require(:member).permit( :email, :password, :family_name, first_name, :start_date, :end_date, :suspended end end [/ruby] Member has email, password, family_name, first_name, start_date, end_date, suspended, hashed_password, but we don't want to hashed_password directly.
Top controller
app/controller/admin/top_controller.rb
class Admin::TopController < Admin::Base def index if current_administrator render action: 'index' else redirect_to :admin_login end end end [/ruby]