AdminLTEで管理画面の実装

AdminLTEを使用し、以下のような管理画面トップページと管理画面へのログイン機能を実装する。 f:id:meo2:20210703181246p:plain

AdminLTEとは

Bootstrapをベースにした、管理画面等のCSSフレームワーク
今回はバージョン3を使用

AdminLTEのインストール

 $ yarn add admin-lte@^3.0

node_modulesディレクトリが作成され、ここにテンプレートが入っている。

マニフェストファイルの設定

マニフェストファイルとは、ロードするcssやjsファイルを記述しておく場所。
マニフェストファイルはapp/assets/javascriptsapp/assets/stylesheetsにある。
adminページ用のマニフェストファイルを作成

 //= require jquery3
//= require jquery_ujs
//= require admin-lte/plugins/bootstrap/js/bootstrap.bundle.min
//= require admin-lte/dist/js/adminlte.min
@import "font-awesome-sprockets";
@import "font-awesome";
@import 'admin-lte/plugins/fontawesome-free/css/all.min.css';
@import 'admin-lte/dist/css/adminlte.min.css';

application.jsの設定

今回はadmin用マニフェストファイルを同じディレクトリに作成するので、任意のファイルだけ読み込む必要がある。
require = tree.があるとapplication.js配下のファイルをすべて読み込んでしまうので、削除して個別にファイルを指定する。

//= require jquery3
//= require popper
//= require bootstrap-sprockets
//= require rails-ujs
//= require activestorage
//= require cable.js

アセットのプリコンパイル設定

アセットとはスタイルシートJavaScriptなどのリソース
application.js以外のファイルを読み込む場合はプリコンパイルの設定をする必要がある。
以下のファイルのコメントアウトを外す。

Rails.application.config.assets.precompile += %w( admin.js admin.css )

これで、admin.jsとadmin.cssが認識されるようになる。(プリコンパイル)

usersテーブルに権限を判定するカラムを追加

ユーザーとadminを判別するためにroleカラムを追加する。
データ型はinteger(整数)とし、ユーザー権限を0、admin権限を1とする。

$ rails g migration add_role_to_users 
class AddRoleToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :role, :integer, default: 0, null: false
  #デフォルトを0とし一般ユーザとする
  end
end

マイグレートする。

== 20210702210922 AddRoleToUsers: migrating ===================================
-- add_column(:users, :role, :integer, {:default=>0, :null=>false})
   -> 0.0013s
== 20210702210922 AddRoleToUsers: migrated (0.0016s) ==========================

enumの追加

enumは、先程定義したモデルのintegerカラムに対して文字列の名前をつけることができる。

  • general・・・一般
  • admin・・・管理者 と定義する。
   enum role: { general: 0, admin: 1 }

current_user.admin?などが使えるようになる。

ルーティング

ルーティングの指定にはnamespaceを使う。
/adminから始まるURLにすることができる。

  namespace :admin do
    root to: 'dashboards#index'
    get 'login', to: 'user_sessions#new'
    post 'login', to: 'user_sessions#create'
    delete 'logout', to: 'user_sessions#destroy'
  end

f:id:meo2:20210703181340p:plain

controller

管理画面用のコントローラの基盤を作成。
application_controllerを継承するadmin/base_controllerを作成する。
他の全ての管理画面用コントローラーはこのbase_controllerを継承する。

% rails g controller admin::base

::ディレクトリの階層を指定できる。

 class Admin::BaseController < ApplicationController
  before_action :check_admin
  layout 'admin/layouts/application'
  
  private
  
  def not_authenticated
    flash[:warning] = 'ログインしてください'
    redirect_to admin_login_path
  end
  
  def check_admin
    redirect_to root_path, warning: '権限がありません' unless current_user.admin?
  end
end

check_adminメソッドで管理者以外はトップページへ遷移させる。 さらに、管理画面用のトップページに遷移するコントローラーadmin/dashboards_controller.rb と管理画面ログイン用のadmin/user_sessions_controllerを作成する。
これらは全てAdmin::BaseControllerを継承する。

$ rails g controller admin::user_session
$ rails g controller admin::dashboards
class Admin::UserSessionController < Admin::BaseController
  skip_before_action :check_admin, only: %i[new create]
  skip_before_action :require_login, only: %i[new create]
  layout 'admin/layouts/admin_login'
  
  def new; end
  
  def create
    @user = login(params[:email], params[:password])
    if @user
      redirect_to admin_root_path, success: 'ログインしました'
    else
      flash.now[:danger] = 'ログインに失敗しました'
      render :new
    end
  end
  
  def destroy
    logout
    redirect_to admin_login_path, success: 'ログアウトしました'
  end
end

ログイン画面はskip_before_action :require_loginでログインしなくてもアクセスできるようにする。
ログイン画面で読み込むレイアウトファイルはadmin_login.html.erbを指定。

class Admin::DashboardsController < Admin::BaseController
  def index; end
end

Admin::BaseControllerを継承しているのでレイアウトは定義しなくていい。

ログイン画面のview

adminログイン画面の作成
/admin/loginへ以下のようなログイン画面を作成。
node_modules/admin-lte/pages/example/login.htmlのテンプレートを参考に作成する。
f:id:meo2:20210703181414p:plain:w300:h300

  <!DOCTYPE html>
<html>
 <head>
   <meta charset="utf-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="robots" content="noindex, nofollow">
   <title>AdminLTE 3 | Log in</title>
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <%= csrf_meta_tags %>
   <%= stylesheet_link_tag 'admin', media: 'all' %>
 </head>
 <body class="hold-transition login-page">
   <div>
     <%= render 'shared/flash_message' %>
     <%= yield %>
   </div>
 </body>
</html>
  <% content_for(:title, 'ログイン') %>
 <div class="login-box">
   <div class="login-logo">
     <h1>ログイン</h1>
   </div>
   <!-- /.login-logo -->
   <div class="card">
     <div class="card-body login-card-body">
 
       <%= form_with url: admin_login_path, local: true do |f| %>
         <%= f.label :email, 'メールアドレス' %>
         <div class="input-group mb-3">
           <%= f.text_field :email, class: 'form-control', placeholder: 'メールアドレス' %>
           <div class="input-group-append">
             <div class="input-group-text">
               <span class="fas fa-envelope"></span>
             </div>
           </div>
         </div>
 
         <%= f.label :password, User.human_attribute_name(:password) %>
         <div class="input-group mb-3">
           <%= f.password_field :password, class: 'form-control', placeholder: :password %>
           <div class="input-group-append">
             <div class="input-group-text">
               <span class="fas fa-lock"></span>
             </div>
           </div>
         </div>
 
         <div class="row">
           <div class="col-12">
             <%= f.submit (t 'defaults.login'), class: 'btn btn-block btn-primary' %>
           </div>
         </div>
       <% end %>
     </div>
   </div>
 </div>

ログインフォームはモデルと紐付かないので、form_withのオプションはurl:を使用
フォーム送信先/admin/loginなのでadmin_login_pathとする。
何もないとajax通信になるのでlocal: trueをつける。

ユーザに管理者権限を付与する。

実際にログインしたりするには管理者権限がついたユーザを作る必要がある。
今回はID=1のユーザに管理者権限を設定する。

 pry(main)> user = User.find(1)
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<User:0x00007f905aa97c60
 id: 1,
 email: "meo@gmail.com",
 crypted_password: "$2a$10$sdmyh3nhvkXVVIWTA5IVA.b4Pc6Syqz5/UCuBFCOMUg/fPVSMAxAa",
 salt: "56AQgaWqsDrNWaDTq7Kw",
 created_at: Sun, 30 May 2021 16:45:18 JST +09:00,
 updated_at: Wed, 23 Jun 2021 16:22:28 JST +09:00,
 last_name: "meoq",
 first_name: "meoq",
 avatar: "883957.png",
 reset_password_token: "Fzo_q2jdTB3qbxQqsxJm",
 reset_password_token_expires_at: nil,
 reset_password_email_sent_at: Sun, 27 Jun 2021 19:59:50 JST +09:00,
 access_count_to_reset_password_page: 0,
 role: "general">

 pry(main)> user.admin!
   (0.1ms)  begin transaction
  User Exists (0.3ms)  SELECT  1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ?  [["email", "meo@gmail.com"], ["id", 1], ["LIMIT", 1]]
  User Exists (1.3ms)  SELECT  1 AS one FROM "users" WHERE "users"."reset_password_token" = ? AND "users"."id" != ? LIMIT ?  [["reset_password_token", "Fzo_q2jdTB3qbxQqsxJm"], ["id", 1], ["LIMIT", 1]]
  User Update (0.5ms)  UPDATE "users" SET "updated_at" = ?, "role" = ? WHERE "users"."id" = ?  [["updated_at", "2021-07-03 17:48:59.494861"], ["role", 1], ["id", 1]]
   (1.3ms)  commit transaction
=> true

 pry(main)> user.save
   (1.7ms)  begin transaction
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ?  [["email", "meo@gmail.com"], ["id", 1], ["LIMIT", 1]]
  User Exists (0.1ms)  SELECT  1 AS one FROM "users" WHERE "users"."reset_password_token" = ? AND "users"."id" != ? LIMIT ?  [["reset_password_token", "Fzo_q2jdTB3qbxQqsxJm"], ["id", 1], ["LIMIT", 1]]
   (0.1ms)  commit transaction
=> true

管理者画面のview

以下のような管理者画面共通のviewの作成。 f:id:meo2:20210703181246p:plain
管理者用テンプレートの作成

 <!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta lang='ja'>
    <meta name="robots" content="noindex, nofollow">
    <title>ダッシュボード | (管理画面)</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag    'admin', media: 'all' %>
    <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700" rel="stylesheet">
  </head>

  <body class="hold-transition sidebar-mini layout-fixed">
  <div class="wrapper">
    <%= render 'admin/shared/header' %>
    <%= render 'admin/shared/sidebar' %>

    <!-- Content Wrapper. Contains page content -->
    <div class="content-wrapper">
     <%= render 'shared/flash_message' %>
     <%= yield %>
    </div>
    <!-- /.content-wrapper -->
  
    <%= render 'admin/shared/footer' %>
  </div>
  <%= javascript_include_tag 'admin' %>
  </body>
</html> 

views/admin/sharedのheader/sidebar/footerのパーシャルを読み込む。
<%= stylesheet_link_tag 'admin', media: 'all' %>
<%= javascript_include_tag 'admin' %>この記述でブラウザにアセットを読み込んでいる。

ダッシュボードのview

<div class="content-wrapper">
  <div class="row">
   <p>ダッシュボードです</p>
  </div>
</div>