Chapter 07

認証機能を実装しよう

FarStep
FarStep
2023.02.11に更新

はじめに

本 Chapter では、devise を使った認証機能の実装を行います。
管理者・顧客の認証機能を作っていきましょう。
認証画面については次の Chapter で行います。

devise のセットアップ

最初に行うのは devise のセットアップです。

下記コマンドを実行して、コンテナが立ち上がっていることを確認してください。

$ docker ps
CONTAINER ID   IMAGE                           COMMAND                   CREATED      STATUS                  PORTS                                NAMES
4121e2a29d55   ecommerce_cable                 "puma -p 28080 cable…"    2 days ago   Up 12 hours             8000/tcp, 0.0.0.0:28080->28080/tcp   ecommerce_cable_1
a6427279ff7d   ecommerce_worker                "bundle exec sidekiq…"    2 days ago   Up 12 hours             8000/tcp                             ecommerce_worker_1
af555970e305   ecommerce_web                   "/app/bin/docker-ent…"    2 days ago   Up 12 hours (healthy)   0.0.0.0:8000->8000/tcp               ecommerce_web_1
4bfcc2ecd604   ecommerce_js                    "yarn build"              2 days ago   Up 12 hours                                                  ecommerce_js_1
d2f818dc734a   ecommerce_css                   "yarn build:css"          2 days ago   Up 12 hours                                                  ecommerce_css_1
80d68a169888   postgres:15.0-bullseye          "docker-entrypoint.s…"    2 days ago   Up 12 hours             5432/tcp                             ecommerce_postgres_1
85ff5e841998   redis:7.0.5-bullseye            "docker-entrypoint.s…"    2 days ago   Up 12 hours             6379/tcp                             ecommerce_redis_1

コンテナが立ち上がっていることが確認できましたら、コンテナ内に入りましょう。

$ docker-compose run --rm web bash

下記のように、app フォルダにログインできたら OK です。
ここで、rails のコマンドを実行することができます。

Creating ecommerce_web_run ... done
ruby@c5f61373fefc:/app$

最初に、下記コマンドを実行して、devise の設定ファイル等を Rails アプリケーションにインストールしましょう。

$ rails g devise:install

コマンドを実行した結果、下記のようなログが表示されていれば OK です。

      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Depending on your application's configuration some manual setup may be required:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

     * Required for all applications. *

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

     * Not required for API-only Applications *

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

     * Not required for API-only Applications *

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

     * Not required *

===============================================================================

上記ログに表示されているセットアップは後ほど行います。

モデル・テーブルの作成

続いて、Admin モデルと Customer モデルを作成し、それぞれのテーブルを作成します。
下記二つのコマンドを実行してください。

$ rails g devise Admin
$ rails g devise Customer

実行が完了しましたら、customers テーブルを作成するためのマイグレーションファイルを開いてください。
customers テーブルには、メールアドレスとパスワードを格納するカラムの他に、名前とステータスを格納するカラムを持たせます。よって、下記コードを追加しましょう。

db/migrate/xxxxxxxxxxxxxx_devise_create_customers.rb
# frozen_string_literal: true

class DeviseCreateCustomers < ActiveRecord::Migration[7.0]
  def change
    create_table :customers do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at

+     t.string :name, null: false
+     t.integer :status, null: false, default: 0

      t.timestamps null: false
    end

    add_index :customers, :email,                unique: true
    add_index :customers, :reset_password_token, unique: true
    # add_index :customers, :confirmation_token,   unique: true
    # add_index :customers, :unlock_token,         unique: true
  end
end

ステータスは enum で管理しますので、integer 型であることに注意です。

それでは、admins テーブルと customers テーブルを作成するためにマイグレーションを実行します。

$ rails db:migrate

マイグレーション実行後、db/schema.rb に下記二つのテーブルが追加されていれば OK です。
customers テーブルには、デフォルトで生成されるカラムに加えて、namestatus が追加されていますね。

db/schema.rb
create_table "admins", force: :cascade do |t|
  t.string "email", default: "", null: false
  t.string "encrypted_password", default: "", null: false
  t.string "reset_password_token"
  t.datetime "reset_password_sent_at"
  t.datetime "remember_created_at"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["email"], name: "index_admins_on_email", unique: true
  t.index ["reset_password_token"], name: "index_admins_on_reset_password_token", unique: true
end

create_table "customers", force: :cascade do |t|
  t.string "email", default: "", null: false
  t.string "encrypted_password", default: "", null: false
  t.string "reset_password_token"
  t.datetime "reset_password_sent_at"
  t.datetime "remember_created_at"
  t.string "name", null: false
  t.integer "status", default: 0, null: false
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["email"], name: "index_customers_on_email", unique: true
  t.index ["reset_password_token"], name: "index_customers_on_reset_password_token", unique: true
end

次に、customer モデルにバリデーションを設定しておきます。
app/models/customer.rb を開いて下記コードを記述してください。

app/models/customer.rb
class Customer < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
+ with_options presence: true do
+   validates :name
+   validates :status
+ end
end

上記の記述により、新規登録やアカウントを編集する際に、name と status が空の場合にバリデーションではじくことができます。

続いて、customer モデルに enum の設定を行いましょう。
enum(enumeration: 列挙)とは、名前を整数の定数に割り当てるのに使われるデータ型です。名前は言語の定数として振る舞う識別子なので、整数を直に扱う場合よりもプログラムの読みやすさとメンテナンス性が向上します。

https://techracho.bpsinc.jp/hachi8833/2022_02_18/115735

それでは、app/models/customer.rb に下記コードを記述してください。

app/models/customer.rb
class Customer < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  with_options presence: true do
    validates :name
    validates :status
  end
+ enum status: {
+   normal: 0,
+   withdrawn: 1,
+   banned: 2
+ }
end

上記の記述により、データベースには 0 や 1 といった数値で status が保存されますが、rails console から値を取得すると、数値ではなくて enum で定義した normal や withdrawn といった文字列で取り出すことができるようになります。

今回、status カラムは

  • normal(通常)
  • withdrawn(退会済み)
  • banned(停止)

のいずれかの状態を表します。

コントローラ・ビューの作成

次に、管理者・顧客の認証・認可を処理するコントローラを作成します。
下記コマンドを実行して、必要なコントローラを作成しましょう。

$ rails g devise:controllers admin
$ rails g devise:controllers customer

コマンド実行後に、admin ディレクトリ内・customer ディレクトリ内にコントローラが生成されていることを確認してください。

├── controllers
│   ├── admin
│   │   ├── confirmations_controller.rb
│   │   ├── omniauth_callbacks_controller.rb
│   │   ├── passwords_controller.rb
│   │   ├── registrations_controller.rb
│   │   ├── sessions_controller.rb
│   │   └── unlocks_controller.rb
│   ├── application_controller.rb
│   ├── concerns
│   ├── customer
│   │   ├── confirmations_controller.rb
│   │   ├── omniauth_callbacks_controller.rb
│   │   ├── passwords_controller.rb
│   │   ├── registrations_controller.rb
│   │   ├── sessions_controller.rb
│   │   └── unlocks_controller.rb
│   ├── pages_controller.rb
│   └── up_controller.rb

続いて、認証に関する画面を作成します。
下記コマンドを実行してビューを生成してください。

$ rails g devise:views admins
$ rails g devise:views customers

コマンド実行後、下記のようにビューが生成されていれば OK です。

└── views
    ├── admins
    │   ├── confirmations
    │   │   └── new.html.erb
    │   ├── mailer
    │   │   ├── confirmation_instructions.html.erb
    │   │   ├── email_changed.html.erb
    │   │   ├── password_change.html.erb
    │   │   ├── reset_password_instructions.html.erb
    │   │   └── unlock_instructions.html.erb
    │   ├── passwords
    │   │   ├── edit.html.erb
    │   │   └── new.html.erb
    │   ├── registrations
    │   │   ├── edit.html.erb
    │   │   └── new.html.erb
    │   ├── sessions
    │   │   └── new.html.erb
    │   ├── shared
    │   │   ├── _error_messages.html.erb
    │   │   └── _links.html.erb
    │   └── unlocks
    │       └── new.html.erb
    ├── customers
    │   ├── confirmations
    │   │   └── new.html.erb
    │   ├── mailer
    │   │   ├── confirmation_instructions.html.erb
    │   │   ├── email_changed.html.erb
    │   │   ├── password_change.html.erb
    │   │   ├── reset_password_instructions.html.erb
    │   │   └── unlock_instructions.html.erb
    │   ├── passwords
    │   │   ├── edit.html.erb
    │   │   └── new.html.erb
    │   ├── registrations
    │   │   ├── edit.html.erb
    │   │   └── new.html.erb
    │   ├── sessions
    │   │   └── new.html.erb
    │   ├── shared
    │   │   ├── _error_messages.html.erb
    │   │   └── _links.html.erb
    │   └── unlocks
    │       └── new.html.erb
    ├── layouts
    │   ├── application.html.erb
    │   ├── mailer.html.erb
    │   └── mailer.text.erb
    └── pages
        └── home.html.erb

さて、もうお気づきの方もおられると思いますが、コントローラとビューの名前空間が一致していません。
コントローラは admin・customer ですが、ビューは admins・customers となっています。

例えば、app/controllers/admin/sessions_controller.rb の中身を見てみると次のような記述があるはずです。

class Admin::SessionsController < Devise::SessionsController

Admin::SessionsController:: の左側に記載されている Admin が名前空間です。
今回は名前空間をコントローラに合わせることにしましょう。

よって、views フォルダ配下に生成された admins を admin に、customers を customer に修正してください。

└── views
    ├── admin
    │   ├── confirmations
    │   │   └── new.html.erb
    │   ├── mailer
    │   │   ├── confirmation_instructions.html.erb
    │   │   ├── email_changed.html.erb
    │   │   ├── password_change.html.erb
    │   │   ├── reset_password_instructions.html.erb
    │   │   └── unlock_instructions.html.erb
    │   ├── passwords
    │   │   ├── edit.html.erb
    │   │   └── new.html.erb
    │   ├── registrations
    │   │   ├── edit.html.erb
    │   │   └── new.html.erb
    │   ├── sessions
    │   │   └── new.html.erb
    │   ├── shared
    │   │   ├── _error_messages.html.erb
    │   │   └── _links.html.erb
    │   └── unlocks
    │       └── new.html.erb
    ├── customer
    │   ├── confirmations
    │   │   └── new.html.erb
    │   ├── mailer
    │   │   ├── confirmation_instructions.html.erb
    │   │   ├── email_changed.html.erb
    │   │   ├── password_change.html.erb
    │   │   ├── reset_password_instructions.html.erb
    │   │   └── unlock_instructions.html.erb
    │   ├── passwords
    │   │   ├── edit.html.erb
    │   │   └── new.html.erb
    │   ├── registrations
    │   │   ├── edit.html.erb
    │   │   └── new.html.erb
    │   ├── sessions
    │   │   └── new.html.erb
    │   ├── shared
    │   │   ├── _error_messages.html.erb
    │   │   └── _links.html.erb
    │   └── unlocks
    │       └── new.html.erb
    ├── layouts
    │   ├── application.html.erb
    │   ├── mailer.html.erb
    │   └── mailer.text.erb
    └── pages
        └── home.html.erb

また、それに伴い、ビューファイルに記述されている部分テンプレートのパスも修正する必要があります。部分テンプレートを呼び出している部分は、下記のような修正を加えてください。

- <%= render "admins/shared/links" %>
+ <%= render "admin/shared/links" %>
- <%= render "admins/shared/error_messages", resource: resource %>
+ <%= render "admin/shared/error_messages", resource: resource %>
- <%= render "customers/shared/links" %>
+ <%= render "customer/shared/links" %>
- <%= render "customers/shared/error_messages", resource: resource %>
+ <%= render "customer/shared/error_messages", resource: resource %>

テキストエディタとして Visual Studio Code を使っている方でしたら、一気に置換する機能を使いましょう。

これで、コントローラとビューの名前空間の整合性がとれました。
(なぜ最初からビューのディレクトリ名を単数系にしなかったのかというと、devise の仕様上、単数系で作成しようとしても、複数形のディレクトリ名になってしまうためです。)

ルーティングの設定

続いて、ルーティングの設定を行なっていきます。
今回のアプリケーションでは、

  • 管理者はログイン・ログアウト
  • 顧客は新規登録・アカウント情報編集・ログイン・ログアウト

といった機能しか実装しません。したがって、ルーティングの設定は最低限にしておきます。

下記コードを追加してください。

config/routes.rb
Rails.application.routes.draw do
+ devise_for :admins, controllers: {
+   sessions: 'admin/sessions'
+ }
+ devise_for :customers, controllers: {
+   sessions: 'customer/sessions',
+   registrations: 'customer/registrations'
+ }
  root to: 'pages#home'

  get '/up/', to: 'up#index', as: :up
  get '/up/databases', to: 'up#databases', as: :up_databases
end

念の為、どんな URL が生成されたか確認しましょう。
下記コマンドを実行してください。

$ rails routes | grep -e admin/ -e customer/

grep を用いて表示するルーティングを絞っています。

                       new_admin_session GET    /admins/sign_in(.:format)            admin/sessions#new
                           admin_session POST   /admins/sign_in(.:format)            admin/sessions#create
                   destroy_admin_session DELETE /admins/sign_out(.:format)           admin/sessions#destroy
                    new_customer_session GET    /customers/sign_in(.:format)         customer/sessions#new
                        customer_session POST   /customers/sign_in(.:format)         customer/sessions#create
                destroy_customer_session DELETE /customers/sign_out(.:format)        customer/sessions#destroy
            cancel_customer_registration GET    /customers/cancel(.:format)          customer/egistrations#cancel
               new_customer_registration GET    /customers/sign_up(.:format)         customer/egistrations#new
              edit_customer_registration GET    /customers/edit(.:format)            customer/egistrations#edit
                   customer_registration PATCH  /customers(.:format)                 customer/egistrations#update
                                         PUT    /customers(.:format)                 customer/egistrations#update
                                         DELETE /customers(.:format)                 customer/egistrations#destroy
                                         POST   /customers(.:format)                 customer/egistrations#create

上記のように、想定通りの URL とコントローラが指定されていますね。

認証機能の設定

続いて、実際に新規登録やログインといった認証機能に関する設定を行なっていきます。

まずは、顧客が新規登録・アカウントの編集する際に、name をコントローラ側に送信できるように、パラメータの許可を行います。

app/controllers/customer/registrations_controller.rb を開いて、下記コードを記述してください。

# frozen_string_literal: true

class Customer::RegistrationsController < Devise::RegistrationsController
+ before_action :configure_sign_up_params, only: [:create]
+ before_action :configure_account_update_params, only: [:update]

  # GET /resource/sign_up
  # def new
  #   super
  # end

  # POST /resource
  # def create
  #   super
  # end

  # GET /resource/edit
  # def edit
  #   super
  # end

  # PUT /resource
  # def update
  #   super
  # end

  # DELETE /resource
  # def destroy
  #   super
  # end

  # GET /resource/cancel
  # Forces the session data which is usually expired after sign
  # in to be expired now. This is useful if the user wants to
  # cancel oauth signing in/up in the middle of the process,
  # removing all OAuth session data.
  # def cancel
  #   super
  # end

+ protected

  # If you have extra params to permit, append them to the sanitizer.
+ def configure_sign_up_params
+   devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
+ end

  # If you have extra params to permit, append them to the sanitizer.
+ def configure_account_update_params
+   devise_parameter_sanitizer.permit(:account_update, keys: [:name])
+ end

  # The path used after sign up.
  # def after_sign_up_path_for(resource)
  #   super(resource)
  # end

  # The path used after sign up for inactive accounts.
  # def after_inactive_sign_up_path_for(resource)
  #   super(resource)
  # end
end

configure_sign_up_params メソッドは create アクションが呼ばれた時、configure_account_update_params メソッドは update アクションが呼ばれた時に実行されます。
どちらのメソッドも name パラメータを許可しています。

次に、ログアウトの scope を変更します。
今回、管理者と顧客の認証機能に対して devise を使っているわけですが、デフォルトですと、どちらかがログアウトしたら両方のセッションが切れるという仕様になっています。
「管理者がログアウトしても、顧客のログイン状態は保ちたい」、また、「顧客がログアウトしても、管理者のログイン状態は保ちたい」ため、ログアウトの scope を変更しましょう。

config/initializers/devise.rb 内の config.sign_out_all_scopesfalse にしてください。(255行目あたりにあるはずです。)

config/initializers/devise.rb
# Set this configuration to false if you want /users/sign_out to sign out
# only the current scope. By default, Devise signs out all scopes.
config.sign_out_all_scopes = false

ついでに、config.scoped_views も編集しておきましょう。
config.scoped_viewstrue にすることで認証画面のレンダリングが早くなります。

# Turn scoped views on. Before rendering "sessions/new", it will first check for
# "users/sessions/new". It's turned off by default because it's slower if you
# are using only default views.
config.scoped_views = true

これで、認証機能に関する設定は完了です。

最後に、RuboCop を実行し、必要であればコードを修正しましょう。

$ rubocop

今回は、RuboCop の静的解析を全て PASS したようです。

Inspecting 45 files
.............................................

45 files inspected, no offenses detected

もしも指摘事項があれば修正してください。
コミットしておきましょう。

$ git add . && git commit -m "Implementing Authentication Functions with devise"

おわりに

お疲れ様でした。
本 Chapter では、認証機能に関する実装を行いました。
認証機能自体は完成しています。
次の Chapter では、新規登録画面やログイン画面の UI を作成します。