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
&#91;/ruby&#93;

<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
&#91;/ruby&#93;

<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
&#91;/ruby&#93;

<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

  1. Status the admin sign-in and sign-out
  2. Status the admin access and close
  3. 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&#91;:administrator_id&#93;
	     @current_administrator ||= Administrator.find_by(id: sesion&#91;:administrator_id&#93;)
	  end
   end
   
   helper_method :current_administrator   
end
&#91;/ruby&#93;
helper_method in controller is available in your view.

Change controller base class to Admin::Base
app/controllers/admin/top_controller.rb
&#91;ruby&#93;
class Admin::TopController < Admin::Base
  def index
  end
end
&#91;/ruby&#93;

<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
&#91;/ruby&#93;
<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]