管理画面のログイン- その1

 


課題の細分化


  • AdminLTE3をyarnでインストール
    • yarnを入れて生成されたnode_modules/admin-lte/pages/example/login.htmlを参考にadminログインフォームのテンプレートを作成
  • マニフェストファイルを作成し読み込む
  • usersテーブルにroleカラム、値で権限を判定、 enumを使用
  • URLは/admin
    • application_controllerを継承するadmin/base_controllerを作成
    • admin/user_sessions_controllerを作成
  • ログイン認証
  • 管理画面
    • 管理画面へのログインフォームを作成
    • ログイン後の画面はnode_modules/admin-lte/starter.htmlを参考
    • ログイン後、管理者権限がある場合はadmin/dashboards#indexへ
    • トップページに遷移するコントローラーとしてadmin/dashboards_controller.rb を作成
    • 管理者権限を持たないユーザーでログインした場合はroot_pathにリダイレクトされること
    • ログイン画面で読み込むレイアウトファイルはviews/admin/layouts/admin_login.html.erb
    • ログイン後の画面のレイアウトファイルはviews/admin/layouts/application.html.erb
    • 部分テンプレート = admin/shared 配下に作成したものを読み込む render(ヘッダー、メニュー、フッター)
    • 「AdminLTEのロゴ」と「ユーザーのアイコン」と「ヘッダーのハンバーガーメニュー」以外は削除
    • 管理画面用のページタイトルには語尾に(管理画面)の文字が表示される
    • ルーティングで指定したアクションを表示させるということは、そのコントローラーに因んだビューファイルが必要(もしくはリダイレクトするかrenderする)

振り返り学習内容

ここでの学習内容

  • パッケージ管理ツール
  • enum
  • ルーティングのnamespace
  • 継承とコールバック
  • AdminLTEでの管理画面実装

🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥

🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥

🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥

・yarnをインストール


$ yarn add admin-lte@^3.1

usersテーブルにroleというカラムを追加し、そこの値で権限を判定できるようにするために roleカラムを追加する

rails g migration add_role_to_users role:integer

db/migrate/作成した日付_add_role_to_users.rb

class AddRoleToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :role, :integer, null: false, default: 0
  end                                   # ↑ 追記 nullはNGで,デフォルトは0の判定にする
end                                              # 0はgeneralとするので一般

作成したカラムに対しenumを使用するためにuserモデルに記入する

  • general・・・一般
  • admin・・・管理者
# app/models/user.rb

enum role: { general: 0, admin: 1 }
  • ちなみに、、、

    こう言った書き方でも良い

    # 配列で数値を明示しない
    class User < ApplicationRecord
      enum role: [:general, :admin]
    end
    
    # rails c
    User.roles # => {"general"=>0, "admin"=>1}
    

roleカラムを追加した時にinteger型にしたので、0か1といった整数が保存されています。 配列は[0,1,2,3...] というように割り振られているので上記のちなみに、、、でも間違いではありませんが、配列形式で0と1の間にguestなどを追加した際に

enum role: [:general, :editor, :admin]
だと
# rails c
User.roles # => {"general"=>0, "editor"=>1, "admin"=>2}

上記のように、adminが2になってしまい「管理者 は 1 だと思っていた」では無くなってしまう問題が発生してしまいます。

それを解消するためにできるだけ最初に書いた方を優先すべきなのです。

さて、

これで現ユーザーがadmin(管理者)か判定したいとき、current_user.admin?といった記載ができるようになります。

enumとは


Active Record クエリインターフェイス - Railsガイド

enumチュートリアル

【Rails】Enumってどんな子?使えるの? - Qiita

Rubyであったarrayに似ていますね。ざっくりの理解では定数化したarrayみたいな感じで進めていきます。

忘れないうちに

$ rails db:migrate

schema.rbを見るとusersテーブル内にroleカラムが追加されています

f:id:michimo_10:20210607131129p:plain

roleカラム



・コントローラーとルーティングを作る


個人的に第一の難関

Admin::BaseController という Admin:: の部分

まずはadminというフォルダをcontrollersの中に作りましょう

$ mkdir app/controllers/admin

続いてAdmin::という部分。

ヒントにはこう書いてありました

今まではapp/controllers/hoge_controller.rbというディレクトリ構造でコントローラを作ってきたと思いますが、アドミン系のコントローラはapp/controllers/admin/base_controller.rbのようにadminというディレクトリを作って管理したほうが保守性が高いですよね。 そういったディレクトリ構造にした場合にはコントローラのクラス名にはAdmin::というネームスペースが必要です。

ネームスペースとは…?

とりあえず"rails ネームスペース"、"rails namespace"で検索して出てくるのは、ほぼルーティングに関する記事。

Railsのroutingにおけるscope / namespace / module の違い - Qiita

Railsのroutesでnamespaceとscopeの設定 - Qiita

DIVE INTO CODE | Railsのルーティングを学ぼう③

Railsでネームスペース(namespace)を使ってアドミン画面を開発する方法 | Boys Be Engineer 非エンジニアよ、エンジニアになれ

つまり、今までroutes.rbの中でやってきたルーティングの

get 'login', to: 'user_sessions#new'
root 'xxx#top'...
resources :users...
collection ... などなど...

f:id:michimo_10:20210607131232p:plain

ルーティング



上記のようなルーティングの他に新たな表記の仕方があるということのようだ。

なので、ヒントにあった

Admin::というネームスペースが必要です。

namespace / 指定ディレクトリ以下にコントローラーを作成 - Qiita

上の記事が良さそうである。

なので

$ rails g controller Admin::Base

すると、

今までは↓だったが

f:id:michimo_10:20210607131430p:plain

今回は↓ これでApplicationControllerを継承したAdmin::Base_controllerが生成された!

f:id:michimo_10:20210607131542p:plain

これが出来上がるとルーティングでも、urlが'/admin'から始まり、振り分けができるようになる。

user_sessions_controller.rbとdashboards_controller.rbを作成しルーティングを以下のようにする

routes.rb

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
...

上記のように記入することでadminのルーティングが新たに増えました

f:id:michimo_10:20210607131616p:plain



そうしたらまず、admin/base_controllerを以下のように書き込む

class Admin::BaseController < ApplicationController
   before_action :check_admin #ここにプライベート内のメソッドを置き、チェックしている
   layout 'admin/layouts/application' # レイアウトについては次節で
 
   private
 
   def not_authenticated  # sorceryのメソッドで認証を確認
     flash[:warning] = t('defaults.message.require_login') # 'ログインしてください' が表示される
     redirect_to admin_login_path # ログイン画面にリダイレクトする
   end
 
   def check_admin # 作成したメソッドで管理者かを判断する
     redirect_to root_path, warning: t('defaults.message.not_authorized') unless current_user.admin?
   end # unlessという文は if とは逆の意味になり、さらに最後に?がついているので管理者でない場合はルートパスにリダイレクトする
 end

sorceryのメソッドはこちら

【Sorcery】Sorceryで使えるようになるメソッドとその活用例 - Qiita

unless とは?

【Rails入門】unless文の条件分岐を初心者向けに解説 | 侍エンジニアブログ

続いてadmin/user_sessions_controller.rb

class Admin::UserSessionsController < Admin::BaseController
# < によってbaseコントローラーを継承している ↑
   skip_before_action :require_login, only: %i[new create] 
# sorceryのrequire_loginメソッドをnew,createだけに使用している
   skip_before_action :check_admin, only: %i[new create]
# 継承しているのでbase_controllerで作成したメソッドも使える
   layout 'layouts/admin_login' # レイアウトは次節
 
   def new; end # ログイン画面
 
   def create # ログイン画面内のフォームで送信後の処理
     @user = login(params[:email], params[:password]) # newのフォーム内にメールとパスを入れてログインに成功すれば
     if @user
       redirect_to admin_root_path, success: t('.success')
     else  # admin_rootパスであるdashbooards#indexにリダイレクトし、'ログインしました'が表示される
       flash.now[:danger] = t('.fail')
       render :new # ログインに失敗した場合,ログイン画面にリダイレクトされ'ログインに失敗しました'が表示される
     end
   end
 
   def destroy # ログアウト
     logout
     redirect_to admin_login_path, success: t('.success')
   end  # 'ログアウトしました' が表示されログアウトする
 end

続いてdashboards_controller.rb

class Admin::DashboardsController < Admin::BaseController # ← 継承されている
   def index; end # 今回はviewファイルの中に'ダッシュボードです'を表示させればいいだけなのでこれで良い
 end

・レイアウト と ビューファイル


第二の難関

Railsではlayoutsディレクトリ内にあるapplication.html.erbというファイルがが基本的なレイアウトになっており、views/layouts/application.html.erbで指定しなければページ全体のレイアウトが読み込まれる。

<%= yield %>という部分ではコントローラでなどで作成したアクションに対するnew.html.erbやshow.html.erbなどが読み込まれるところである。

し か し !

今回はレイアウトをapplicationとadmin_loginの2つ作る。

理由としては管理者としてのログイン前と後でレイアウトを変更するため。 ログイン前にはログインフォーム以外は必要ない。

ログイン後はデフォルトのレイアウトであるlayouts/application.html.erbで反映される。これは「adminの管理画面ですよ」という意味でもあるのでadminの根幹となるbase_controllerにレイアウト指定の記載をしておく。

app/controllers/admin/user_sessions_controller.rb

layout 'admin/layouts/admin_login' #ログイン画面のレイアウト

app/controllers/admin/base_controller.rb

layout 'admin/layouts/application' #ログイン後のレイアウト

baseコントローラーで使われておりデフォルトのレイアウトはapplication、 user_sessionコントローラーの中でレイアウトはログイン画面で使用するので個別でレイアウトを設定してあげる

レイアウトの中身はyarnでインストールしたAdminlteの"node_modules/admin-lte/pages/example/login.html"を参考にという指示があったので必要な部分を引っ張ってくる。

まずはログイン後のレイアウトである/views/admin/layouts/application.html.erbを作成

<!DOCTYPE html>
<html>
  <head>
    <title><%= page_title(yield(:title), admin: true) %></title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'admin', media: 'all' %>
    <%= javascript_include_tag 'admin' %>
    <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 layout-fixed">
   <div class="wrapper">
      <%= render 'admin/shared/header' %>
      <%= render 'admin/shared/sidebar_menu' %>
    <div class="content-wrapper">
      <%= render 'admin/shared/flash_message' %>
      <%= yield %>
    </div>
      <%= render 'admin/shared/footer' %>
   </div>
  </body>
</html>

回答はこうだった

<!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 '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>

回答例ではbodyの閉じタグの前でjsを読み込んでいる。

さらにキャラセットやランゲージ(表示する言語)の設定もしていた

<meta charset="UTF-8">
<meta lang='ja'>

続いてログイン画面のレイアウトviews/layouts/admin_login.html.erbの作成

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="robots" content="noindex, nofollow">
    <title><%= page_title(yield(:title), admin: true) %></title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-param" content="authenticity_token" />
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag 'admin', media: 'all' %>
  </head>
  <body class="hold-transition login-page">
    <%= render 'shared/flash_message' %>
    <%= yield %>
  </body>
</html>

回答はこうだった(あまり変わらず)

<!DOCTYPE html>
 <html>
 <head>
   <meta charset="utf-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="robots" content="noindex, nofollow">
   <title><%= page_title(yield(:title), admin: true) %></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>

回答では、ログイン前のレイアウトはadminフォルダではなく今までのレイアウトフォルダに入っていた。

自分はuser_sessions_controllerでadmin/layouts/admin_login'と書いていたのはadminのレイアウトに入れていたからだ。しかし、そもそもadminのフォームは管理者でも一般でもないし、管理者側からすればログアウトされている状態なので、今までのレイアウトでも問題ないことが分かった。なのでadmin_loginのファイルは今までのレイアウトフォルダに移動した。

続いてビューファイルというか部分テンプレート(パーシャル化する)

部分テンプレートはapp/views/admin/の下にsharedフォルダを作成し、そこにぶち込む

ヘッダー部分 admin/shared/_header.html.erb

# 自分はこうしてた
<header>
  <!-- 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">
    <li class="nav-item">
      <a class="nav-link" rel="nofollow" data-method="delete" href="/admin/logout">ログアウト</a>
    </li>
  </ul>
</nav>
<!-- /.navbar -->
</header>

回答例はこうだった

<!-- 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">
     <li class="nav-item">
       <%= link_to t('defaults.logout'), admin_logout_path, class: 'nav-link', method: :delete %>
     </li>  # ↑ ちゃんとlinkタグで覆ってymlで翻訳してるからエライ
   </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="#" class="brand-link">
    <img class="brand-image img-circle elevation-3" src="#" />
    <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">
        <img class="img-circle elevation-2" src="#" />
      </div>
      <div class="info">
        <a href="#" class="d-block">あどみん たろう</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">
            <a href="#" class="nav-link active">
              <i class="far fa-file"></i>
              <p>掲示板一覧</p>
            </a>
          </li>
          <li class="nav-item">
            <a href="#" class="nav-link">
              <i class="far fa-user"></i>
              <p>ユーザー一覧</p>
            </a>
          </li>
        </ul>
      </nav>
    <!-- /.sidebar-menu -->
  </div>
  <!-- /.sidebar -->
</aside>

回答はこう

<!-- Main Sidebar Container -->
 <aside class="main-sidebar sidebar-dark-primary elevation-4">
   <!-- Brand Logo -->
   <a href="index3.html" class="brand-link"> # ←と↓のリンク先とimage_tagでエラーになったら上のhtmlにしてみて
     <%= 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.avatar_url, class: 'img-circle elevation-2' %>
       </div>
       <div class="info">
         <a href="#" class="d-block"><%= current_user.decorate.full_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>
         <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

<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>

回答はこう

<footer class="main-footer">
   <strong>Copyright &copy; 2019 RUNTEQ.</strong>
   All rights reserved.
 </footer>

違いはあれど、パーシャル部分は大体こんな感じだった。

そして上は埋め込み部分のレイアウトなので実際にコントローラーで作成されたビューファイルがログイン後のyieldに表示される部分

app/views/admin/dashboards/index.html.erb

<% content_for(:title, t('.title')) %>  # ← タイトルの表示
<div class="container">
  <div class="row">
    ダッシュボードです
  </div>
</div>

これでビューは完成。しかし、上の #タイトルの表示 部分。

これは何度もつまづいた部分だがきちんと設定しなければならない。でないと

ブラウザのタブの部分が<span ~~>と表示されてしまうからだ。

・タイトルの表示


タイトルをきちんと表示するためにヘルパーの編集をする。しかしこれは全てのページに関する事なのでわざわざadmin用のhelperを作成する必要はなく、元々の中で条件分岐をすれば良い

元々今まではこうだった ↓ app/helpers/application_helper.rb

module ApplicationHelper
  def full_title(page_title = '')
    base_title = 'RUNTEQ BOARD APP'
    if page_title.empty?
      base_title
    else
      page_title + ' | ' + base_title
    end
  end
end

それがこうなった ↓

module ApplicationHelper
  def page_title(page_title = '', admin = false)
    base_title = if admin
                   'RUNTEQ BOARD APP(管理画面)'
                 else
                   'RUNTEQ BOARD APP'
                 end

    page_title.empty? ? base_title : page_title + ' | ' + base_title
  end
end

admin = false を使うことで今まで作ってきたタイトルはadmin: falseなので書き換えが不要です。

full_titleだったのをpage_titleにしたのは参考にしたサイトにはそのように記載があり、実際full_titleのままではうまくいかなかった(再度検証していない)

もちろん今までfull_titleとしていた部分もpage_titleに変更するのを忘れないようにしなければならない。