🦓

[Rails][AdminLTE]管理画面18/20

2023/07/09に公開

はじめに

管理画面へのログイン機能を実装していきます。

要件
  • 管理画面へのログイン画面のURLは/admin/loginであること
  • 管理画面へのログインが成功した際のURLは/adminであること
  • Usersテーブルに role カラムが追加されていること(general: 一般, admin: 管理者)
  • 管理画面へのログイン画面のページタイトルは ログイン | Article APP(管理画面) であること
  • 管理画面のトップページは ダッシュボード | Article APP APP(管理画面) であること
  • 管理画面へのログインに失敗した場合はadmin_login_pathにリダイレクトされること
    管理者権限を持たないユーザーでログインした場合はroot_pathにリダイレクトされること
  • ログアウト後は管理画面のログインページにリダイレクトされること
  • フラッシュメッセージが表示されること
    • ログアウト時のメッセージは『ログアウトしました』と表示されること
    • 成功時・・・『ログインしました』
    • 失敗時・・・『ログインに失敗しました』

環境

Rails 6.1.7.3
ruby 3.0.0

tl;dr

  1. AdminLTEをインストールする
  2. CSSとJSを読み込む
  3. プリコンパイルの設定
  4. usersテーブルにroleカラムを追加
  5. enumを設定する
  6. Adminコントローラーを作成する
  7. routes.rbを編集する
  8. 管理画面のビューを作成する
  9. 管理画面をローカライズする

管理画面

管理画面は、ウェブアプリケーションやシステムの管理者が、システムの設定や管理、データの操作などを行うための画面です。一般的に、管理者や特権ユーザーがアプリケーションのバックエンドにアクセスし、データベースや設定を変更したり、他のユーザーのアカウントを管理したりするためのインターフェースです。

管理画面は、一般のユーザーには公開されないことが多く、セキュリティ上の理由からログインが必要な場合があります。管理者は、特別な権限を持つユーザーアカウントでログインし、管理画面にアクセスすることで、システム全体の設定や操作を行うことができます。

例えば、以下のような機能が含まれることがあります:

  • ユーザーアカウントの管理:ユーザーの作成、編集、削除などの操作を行います。
  • データの管理:データの閲覧、検索、編集、削除などを行います。
  • システム設定の管理:アプリケーションの設定やパラメータの変更を行います。
  • レポートの生成:集計データや統計情報の表示やダウンロードを行います。
  • セキュリティ管理:アクセス権限の管理やログの監視、セキュリティ設定の管理を行います。

管理画面は、システム管理者や特定のユーザーにとって重要な役割を果たすものであり、システムの安全性や運用性を確保するために必要な機能を提供します。

今回はAdminLTEを使っていきます。
https://github.com/ColorlibHQ/AdminLTE
https://adminlte.io/docs/3.2/

AdminLTEをインストールする

yarn add admin-lte@^3.2

CSSとJSを読み込む

node_modulesにあるadminlte.scssadminLTE.jsを読み込みます。

app/javascript/packs/application.js
import 'admin-lte/build/js/AdminLTE'
app/javascript/stylesheets/application.scss
@import 'admin-lte/build/scss/adminlte'
rails webpacker:compile

プリコンパイルの設定

アセットのプリコンパイルは、アプリケーションのデプロイ時に行われ、パフォーマンスを向上させるために最適化されたアセットファイルが生成されます。
アプリケーションに独自のJavaScriptやCSSを追加し、それをアセットパイプラインで結合・圧縮する必要がある場合に使用します。
一般的には、カスタムのJavaScriptやCSSファイル、サードパーティのライブラリやテンプレートのアセットファイルをプリコンパイルする必要がある場合に、この設定を使用します。

config/initializers/assets.rb
Rails.application.config.assets.precompile += %w( adminLTE.js adminlte.scss )

usersテーブルにroleカラムを追加

enumを使ってロールを管理するのでデータタイプをintegerにします。

bin/rails generate migration AddRoleToUsers
      invoke  active_record
      create    db/migrate/20230708084922_add_role_to_users.rb
db/migrate/xxx_add_role_to_users.rb
class AddRoleToUsers < ActiveRecord::Migration[6.1]
  def change
    add_column :users, :role, :integer, default: 0, null: false
  end
end
bin/rails db:migrate
Running via Spring preloader in process 27263
== 20230708084922 AddRoleToUsers: migrating ===================================
-- add_column(:users, :role, :interger, {:default=>0, :null=>false})
   -> 0.0046s
== 20230708084922 AddRoleToUsers: migrated (0.0047s) ==========================

enumを設定する

app/models/user.rb
class User < ApplicationRecord
   enum role: { general: 0, admin: 1 }
end

Adminコントローラーを作成する

bin/rails g controller Admin::Base
      create  app/controllers/admin/base_controller.rb
      invoke  erb
      create    app/views/admin/base

このコントローラーファイルは、Admin ネームスペース内の他のコントローラーが継承するベースとなるコントローラーです。Admin ネームスペースのコントローラーで共通のロジックやフィルターを追加する場合に使用することができます。

Admin ネームスペース

Admin ネームスペースは、Railsアプリケーション内で管理者向けの機能や管理画面に関連するコードをグループ化するための仕組みです。

通常、Railsアプリケーションはユーザー向けのフロントエンドや一般的な機能を提供するために使用されますが、管理者が利用する機能は一般ユーザーとは異なるケースがあります。管理者向けの機能は、通常は認証や権限管理、データの管理や編集などを含みます。

Admin ネームスペースを使用することで、管理者向けのコードを独自の名前空間でグループ化し、管理画面に関連するコントローラーやビュー、ヘルパー、モデルなどを整理することができます。これにより、コードの可読性や保守性を向上させることができます。

例えば、Admin::UsersControllerAdmin::ProductsController のように、管理者がユーザーや製品を管理するためのコントローラーを Admin ネームスペース内に作成することができます。

また、ルーティングにも namespace メソッドを使用して Admin ネームスペースを適用することができます。これにより、管理画面用のルーティングをグループ化し、URLのパスやヘルパーメソッドにも admin の接頭辞が付与されます。

namespace :admin do
  resources :users
  resources :products
end

以上のように、Admin ネームスペースを使用することで、管理者向けの機能を組織化し、アプリケーション内の他の部分と分離して扱うことができます。

Admin用のトップページへ遷移するコントローラーを作成する

node_modules/admin-lte/starter.htmlを参考して作成していきます。

bin/rails g controller Admin::Dashboards index
      create  app/controllers/admin/dashboards_controller.rb
       route  namespace :admin do
                get 'dashboards/index'
              end
      invoke  erb
      create    app/views/admin/dashboards
      create    app/views/admin/dashboards/index.html.erb

Admin用のログインコントローラーを作成する

node_modules/admin-lte/pages/example/login.htmlを参考して作成していきます。

bin/rails g controller Admin::Sessions new
      create  app/controllers/admin/sessions_controller.rb
       route  namespace :admin do
                get 'sessions/new'
              end
      invoke  erb
      create    app/views/admin/sessions
      create    app/views/admin/sessions/new.html.erb

routes.rbを編集する

admin認証用ルートを追加します。

config/routes.rb
namespace :admin do
  root to: 'dashboards#index'
  get 'login', to: 'sessions#new'
  post 'login', to: 'sessions#create'
  delete 'logout', to: 'sessions#destroy'
end

Admin用のレイアウトファイルを作成する

views/admin/layouts/views/admin/shared/を作成します。
node_modules/admin-lte/starter.htmlを参考します。

views/admin/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta lang='ja'>
    <meta name="robots" content="noindex, nofollow">
    <title><%= page_title(yield(:title), admin: true) %></title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback">
  </head>
  <body class="hold-transition sidebar-mini">
    <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>
  </body>
</html>
views/admin/shared/_header.html.erb
<!-- Navbar -->
<nav class="main-header navbar navbar-expand navbar-white navbar-light">
  <!-- Left navbar links -->
  <ul class="navbar-nav">
    <li class="nav-item">
      <a class="nav-link" data-widget="pushmenu" href="#"><i class="fas fa-bars"></i></a>
    </li>
  </ul>
  <!-- Right navbar links -->
  <ul class="navbar-nav ml-auto">
    <!-- Navbar Search -->
    <li class="nav-item">
      <%= link_to t('defaults.logout'), admin_logout_path, method: :delete, class: 'nav-link' %>
    </li>
  </ul>
</nav>
<!-- /.navbar -->
views/admin/shared/_sidebar.html.erb
<!-- Main Sidebar Container -->
<aside class="main-sidebar sidebar-dark-primary elevation-4">
  <!-- Brand Logo -->
  <a href="index3.html" class="brand-link">
    <%= image_tag 'AdminLTELogo.png', class: 'brand-image img-circle elevation-3' %>
    <span class="brand-text font-weight-light">AdminLTE 3</span>
  </a>

  <!-- Sidebar -->
  <div class="sidebar">
    <!-- Sidebar user panel (optional) -->
    <div class="user-panel mt-3 pb-3 mb-3 d-flex">
      <div class="image">
        <%= image_tag Current.user.profile.thumb.url, class: 'img-circle elevation-2' %>
      </div>
      <div class="info">
        <a href="#" class="d-block"><%= Current.user.user_name %></a>
      </div>
    </div>

    <!-- Sidebar Menu -->
    <nav class="mt-2">
      <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
        <li class="nav-item">
          <%= link_to '#', class: "nav-link" do %>
            <i class="nav-icon far fa-file"></i>
            <p>
              投稿一覧
            </p>
          <% end %>

        <li class="nav-item">
          <%= link_to '#', class: "nav-link" do %>
            <i class="nav-icon far fa-user"></i>
            <p>
              ユーザ一一覧
            </p>
          <% end %>
        </li>
      </ul>
    </nav>
    <!-- /.sidebar-menu -->
  </div>
  <!-- /.sidebar -->
</aside>
views/admin/shared/_footer.html.erb
<!-- Main Footer -->
<footer class="main-footer">
  <!-- To the right -->
  <div class="float-right d-none d-sm-inline">
    Anything you want
  </div>
  <!-- Default to the left -->
  <strong>Copyright &copy; 2014-2021 <a href="https://adminlte.io">AdminLTE.io</a>.</strong> All rights reserved.
</footer>

レイアウト宣言

Railsには、個別のコントローラやアクションに割り当てる特定のレイアウトをより正確に指定するため宣言する必要があります。

app/controllers/admin/base_controller.rb
class Admin::BaseController < ApplicationController
  layout 'admin/layouts/application'
end

Admin::DashboardsコントローラーはAdmin::Baseコントローラーを継承する

継承すればレイアウトの宣言は不要です。

app/controllers/admin/dashboards_controller.rb
class Admin::DashboardsController < Admin::BaseController
  skip_before_action :check_admin
  before_action :login_required, unless: -> { Current.user }
  def index
  end
end

https://railsguides.jp/layouts_and_rendering.html#レイアウトの探索順序

管理画面用のタイトルを設定する

app/helper/application_helper.rb
module ApplicationHelper
    def page_title(title)
        base_title = 'Article APP' 

        title.empty? ? base_title : title + " | " +  base_title
    end
end

こちらのメソッドをadminを判断する用のif文を追加します。
一般ユーザー向けのファイルに影響が出ないようにデフォルトをfalseにします。

app/helper/application_helper.rb
module ApplicationHelper
    def page_title(title, admin = false)
        base_title = if admin
                        'Article APP(管理画面)'
                     else
                        'Article APP'
                     end

        title.empty? ? base_title : title + " | " +  base_title
    end
end

レイアウトファイルにtrueを指定しましたね。

app/views/admin/layouts/application.html.erb
<title><%= page_title(yield(:title), admin: true) %></title>

Admin::Baseコントローラーを設定する

管理画面全体に共通する機能を追加します。
Adminが未ログインの状態だとログインを促します。
ユーザーのロールがAdminではないとページをroot_pathへ遷移します。
admin?メソッドはenumを設定した際にroleカラムの1をadminとしたので使えるようになっているメソッドです。

app/controllers/admin/base_controller.rb
class Admin::BaseController < ApplicationController
    before_action :check_admin
    layout 'admin/layouts/application'

    private

    def login_required
        flash.now[:danger]=t('defaults.message.require_login')
        redirect_to admin_login_path unless Current.user
    end

    def check_admin
        flash.now[:danger]=t('defaults.message.not_authorized')
        redirect_to root_path unless Current.user&.admin?
    end
end

Admin::Sessionsコントローラー

ログインに関しての記述を追加していきます。
Admin::Baseと違うレイアウトを使用するので宣言する必要があります。
ログインした後は、admin_root_pathへリダイレクトされ、admin/dashboards#indexアクションが呼び出されます。

このとき、管理者ではない一般ユーザでログインした場合は、before_action :check_adminフィルタによって、一般ユーザ用のトップページへリダイレクトされます。

app/controllers/admin/sessions_controller.rb
class Admin::SessionsController < Admin::BaseController
  skip_before_action :check_admin, only: %i[new create]
  layout 'admin/layouts/admin_login'

  def new
  end

  def create
    @user = User.find_by(email: params[:email])
    if @user.present? && @user.authenticate(params[:password])
        session[:user_id] = @user.id
        flash[:success] = t('.success')
        redirect_to admin_root_path
    elsif @user.admin?
        flash[:danger] = t('.danger')
        render :new
    else
        flash[:danger] = t('defaults.message.not_authorized')
        redirect_to root_path
    end
  end

  def destroy
    session[:user_id] = nil
    flash[:success] = t('.success')
    redirect_to admin_login_path
  end
end

ログインのビューを作成する

adminログインフォームのテンプレートはnode_modules/admin-lte/pages/example/login.htmlを参考にします。

app/views/admin/layouts/admin_login.html.erb
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title><%= page_title(yield(:title), admin: true) %></title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>
  <body class="hold-transition login-page">
    <div>
      <%= render 'shared/flash_message' %>
      <%= yield %>
    </div>
  </body>
</html>

ログインフォームを作成する

app/views/admin/sessions/new.html.erb
<%= content_for(:title, t('.title')) %>
<div class="login-box">
  <div class="login-logo">
    <h1><%= t('.title') %></h1>
  </div>
  <!-- /.login-logo -->
  <div class="card">
    <div class="card-body login-card-body">

      <%= form_with url: admin_login_path, locale: true do |f| %>
        <%= f.label :email, User.human_attribute_name(:email) %>
        <div class="input-group mb-3">
          <%= f.email_field :email, class: 'form-control', placeholder: 'Email'%>
          <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>

管理画面をローカライズする

config/locales/views/ja.yml
ja:
  admin:
    sessions:
      new:
        title: 'ログイン'
      create:
        success: 'ログインしました。'
        danger: 'ログインに失敗しました。もう一度試してください。'
      destroy:
        success: 'ログアウトしました。'
    dashboards:
      index:
        title: 'ダッシュボード'

終わりに

管理画面の実装でした。
復習して理解できるようになりましょう。

Discussion