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]
