🛤️

devise の model 分割をチャレンジしてみた(with docker-compose)

2021/02/18に公開

split devise models ...

TL;DR

devise の model 分割 について

以前記事紹介の記事あげたんですが、メリットなどはそちらを参照してもらえると。
以前記事紹介した時は腹落ちしてなかったんですがちょっとわかってきたんで今回記事にしました。
ただ、まだまだうまくいかない部分も残ってます……(後述)

今回の記事でこの話が始まるのは
「認証認可用のモデルを作成」の頁からなんで、そこまで読み飛ばしてもらってOKとも思います。
作成するmodelの名前とかはなるべく記事に合うような形にしています。

docker-compose の準備, bundle init して rails new する

前述の過去記事と同じですが今回は久しぶりに手順を追っていってみます。

サクッとやっていきましょう。

cd myapp
mkdir -p forDocker/mysql/conf.d/
touch forDocker/mysql/conf.d/mysql.cnf
mkdir -p forDocker/rails/
touch forDocker/rails/entrypoint.sh
touch Dockerfile
touch docker-compose.yml
forDocker/mysql/conf.d/mysql.cnf
[mysqld]
default_authentication_plugin = mysql_native_password
skip-host-cache
skip-name-resolve

character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init-connect = SET NAMES utf8mb4
skip-character-set-client-handshake

[client]
default-character-set = utf8mb4

[mysqldump]
default-character-set = utf8mb4

[mysql]
default-character-set = utf8mb4
forDocker/rails/entrypoint.sh
#!/bin/bash
set -e

# Reference by https://matsuand.github.io/docs.docker.jp.onthefly/compose/rails/
# Remove a potentially pre-existing server.pid for Rails.
rm -f /app/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
Dockerfile
FROM ruby:2.7
RUN set -x && curl -sL https://deb.nodesource.com/setup_14.x | bash -

RUN set -x && apt-get update -y -qq && apt-get install -yq less lsof vim default-mysql-client

RUN set -x && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
    echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list

RUN set -x && apt-get update -y -qq && apt-get install -yq nodejs yarn

RUN mkdir /app
WORKDIR /app
# ※docker-compose up で rails s するときはここのコメントを外す
# COPY Gemfile /app/Gemfile
# COPY Gemfile.lock /app/Gemfile.lock
# RUN bundle install
COPY . /app

# Add a script to be executed every time the container starts.
COPY ./forDocker/rails/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start the main process.
# ※docker-compose up で rails s するときはここのコメントを外す
# CMD ["rails", "server", "-b", "0.0.0.0"]
docker-compose.yml
version: '3.8'

services:
  app:
    container_name: app
    build: .
    tty: true
    stdin_open: true
    volumes:
      - .:/app
      - bundle_install:/usr/local/bundle
    ports:
      - "3000:3000"
    depends_on:
      - db

  db:
    platform: linux/x86_64
    image: mysql:8.0
    container_name: db
    restart: always
    volumes:
      - ./forDocker/mysql/conf.d:/etc/mysql/conf.d
      - dbvol:/var/lib/mysql
    ports:
      - "3306:3306"
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      TZ: "Asia/Tokyo"

volumes:
  bundle_install:
  dbvol:

Dockerfile で CMD ["rails", "server", "-b", "0.0.0.0"] の部分をコメントアウトしてるので
正確にはこのタイミングではまだ forDocker/rails/entrypoint.sh は不要なんですが
あっても特に問題ないので大丈夫です。

docker-compose.yml はこれまでの記事から少し変えています

  • ファイルを version: '3.8'
  • bundle_install:/usr/local/bundle を追加
  • mysql:8.0 image に platform: linux/x86_64 を追記

では初期設定をしていきましょう。
この時点で作業ディレクトリを VSCode とかで開いておきながら以下ターミナルの作業を開始すると楽です。

# サービス用のポート(3000)を有効化し、ホスト側に割り当て可能にして起動
docker-compose run --rm --service-ports app bash
# bashログインできてもちょっと待つ(MySQLが立ち上がってくるのを待つ)
# mysql にパスワードなし root で接続できるか確認
mysql -u root -h db -e 'select version();'
# bundle config 確認 BUNDLE_APP_CONFIG: "/usr/local/bundle" なのを確認
bundle config
# Gemfile 作成
bundle init
# rails 追加 (Gemfile を直接編集してから bundle install でもOK)
bundle add rails --version "~> 6.1.2.1"
bundle install
# rails new
#   -B:bundle installしない, -S:sprocketsを組み込まない
#   -T:test::unitを組み込まない, -J:javascriptを組み込まない
#   -d myaql:データベースの種類, --force:ファイルが存在する場合に上書き
rails new . -B -S -T -J -d mysql --force
bundle install

OKです。今回はついでに simpacker を入れちゃいます。
ターミナルはそのままにしておいて、Gemfile に simpacker を追記して

Gemfile
...(前略)
gem "simpacker"
...(後略)

でまたターミナルで

# simpacker を入れる
bundle install
# simpakcer 初期化
rails simpacker:install

webpack は消します。
ターミナルはそのままにしておいて、 package.json の devDependencies はいったん空に。

package.json
package.json
{
  "private": true,
  "devDependencies": {
  }
}

rails simpacker:install すると npm で入ってしまうので
不要なファイルを消して yarn しなおします。
でまたターミナルに戻って

rm -rf node_modules/
rm package-lock.json
rm webpack.config.js
yarn install

これで余計な package のない Simpacker がセットアップできます。
git 管理しているときは .gitignore に /node_modules/* も追記しておきましょう。

ここまで終わったら次は Rails アプリケーションの準備を整えましょう
ターミナルはそのままにしておいて、 config/database.yml を編集します。

config/database.yml
...(前略)
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  url: mysql2://root:@db:3306
  # username: root
  # password:
  # host: localhost
...(後略)

config/database.yml の編集を行ったら、データベースの準備をしてrailsを起動してみます。
でまたターミナルに戻って

rails db:create
rails db:migrate
rails s -b "0.0.0.0"

http://localhost:3000/ で "Yay! You’re on Rails!" っていういつものアレが出ましたでしょうか。
OKですね。Ctrl+c でサーバを閉じましょう。

devise を導入する

ターミナルはそのままにしておいて、 Gemfile に以下を追加。
ついでに日本語化に必要な gem と開発環境用のメール確認gem、
スキーマ情報をmodelに出力するgem、action_args も入れちゃいます。

Gemfile
# 認証認可
gem 'devise'
gem 'devise-i18n'

# コントローラーアクション引数パラメーター化
gem 'action_args'

group :development do
  # 開発環境用送信メール確認
  gem 'letter_opener_web'
  # スキーマ情報出力
  gem 'annotate'
...(後略)

Gemfile を編集したら、またターミナルに戻って

bundle install
# setup annotate
rails g annotate:install
# setup devise
rails g devise:install

bundle exec rails g devise:install すると
以下のように手作業での導入方法が表示されますね。

Running via Spring preloader in process 1022
      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 *

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

では提示された手順どうり準備を進めていきましょう。

1. 開発環境用のメール送信の設定を行う

ターミナルはそのままにしておいて、 config/environments/development.rb に以下を追記します。
letter_opener_web の設定も一緒に入れちゃいます。
どこでも良いですが私はこの辺に追記します。

config/environments/development.rb
...(前略)
  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

  # ----- ここを追記 -----
  # Devise mailer setting
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
  config.action_mailer.delivery_method = :letter_opener_web
  # ----- ここまで -----

  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = false
...(後略)

2の準備. rails g controller で生成するファイルの種類を変更

root_url となる View を作成する前に、
rails g controller で生成するファイルの種類を変更しておきましょう。
helper とかいらんねん。(使うときは自分で作ればいい)
config/application.rb を編集します。

config/application.rb
module App
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.1
  ...(中略)
    # ----- ここを追記 -----
    # rails generate で生成するファイルを変更
    config.generators do |g|
      g.assets false             # CSS/JSファイル生成せず
      g.skip_routes false        # trueなら routes.rb変更せず
      g.helper false             # ヘルパー生成せず
    end
    # ----- ここまで -----
  end

2. root_url となる View を作成する

今回は home にしましょう。
またターミナルに戻って

rails g controller Home index
# 出力
Running via Spring preloader in process 1143
      create  app/controllers/home_controller.rb
       route  get 'home/index'
      invoke  erb
      create    app/views/home
      create    app/views/home/index.html.erb

先の設定で helper や CSS が出力されませんね。OKです。
ターミナルはそのままにしておいて、 config/routes.rb で
ルートの定義を編集します。
ついでに letter_opener_web の設定も入れちゃいます。

config/routes.rb
Rails.application.routes.draw do
  get 'home/index'

  root to: "home#index"
  
  ## 開発環境用letter_opener
  mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end

3. app/views/layouts/application.html.erb にメッセージ追加

……と思いましたが、
後で routes.rb でルーティングの設定を行うのでその後にしましょう。
先に config/initializers/devise.rb を少し編集しておきます

config/initializers/devise.rb
  # The default HTTP method used to sign out a resource. Default is :delete.
  config.sign_out_via = :get # ここを :delete から :get に編集

認証認可用のモデルを作成

さてようやく本題です。
ターミナルに戻って作業を進めます。
まずは rails g devise User で下地を作っちゃいます。

rails g devise User
## 出力
Running via Spring preloader in process 1186
      invoke  active_record
      create    db/migrate/20210212083404_devise_create_users.rb
      create    app/models/user.rb
      insert    app/models/user.rb
       route  devise_for :users

migration ファイルのファイル名は各々変わると思うので rails g devise User した時の出力などを見てみてください。
で、今回はこの db/migrate/20210212083404_devise_create_users.rb を
めっちゃシンプルにしちゃいます。こんな感じ。ほぼ情報ないテーブルですね。

db/migrate/20210212083404_devise_create_users.rb
# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      t.string :nickname, null: false

      t.timestamps null: false
    end
  end
end

では devise として必要な情報はどうするのか?
他のmodelに用意していきます。またターミナルに戻って、

# サインアップ用のmodel 
rails g devise UserRegistration
# 出力
      invoke  active_record
      create    db/migrate/20210212093314_devise_create_user_registrations.rb
      create    app/models/user_registration.rb
      insert    app/models/user_registration.rb
       route  devise_for :user_registrations
# email+password 認証model
rails g devise UserDatabaseAuthentication
# 出力
      invoke  active_record
      create    db/migrate/20210212093400_devise_create_user_database_authentications.rb
      create    app/models/user_database_authentication.rb
      insert    app/models/user_database_authentication.rb
       route  devise_for :user_database_authentications

それぞれの migration ファイルはこんな感じに編集します。

db/migrate/20210212093314_devise_create_user_registrations.rb
# frozen_string_literal: true

class DeviseCreateUserRegistrations < ActiveRecord::Migration[6.1]
  def change
    create_table :user_registrations do |t|
      # user_registrations は users と関連付いている必要はない
      ## Confirmable
      t.string   :confirmation_token,  null: false
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable
      t.string   :email

      t.timestamps null: false
    end

    add_index :user_registrations, :confirmation_token, unique: true
    add_index :user_registrations, :unconfirmed_email,  unique: true
  end
end
db/migrate/20210212093400_devise_create_user_database_authentications.rb
# frozen_string_literal: true

class DeviseCreateUserDatabaseAuthentications < ActiveRecord::Migration[6.1]
  def change
    create_table :user_database_authentications do |t|
      t.references :user, foreign_key: true, dependent: :destroy,  index: {unique: true}

      ## Database authenticatable
      t.string :email,              null: false
      t.string :encrypted_password, null: false

      t.timestamps null: false
    end

    add_index :user_database_authentications, :email, unique: true
  end
end

devise の各モジュールごとの責任を追った model を定義することで
それぞれの model が管理する情報を極力絞っています。
で、各modelのrbファイルはこんな感じに。

app/models/user.rb
class User < ApplicationRecord
  devise :authenticatable

  has_one :user_database_authentication, dependent: :destroy
end
app/models/user_registration.rb
class UserRegistration < ApplicationRecord
  devise :confirmable
end
app/models/user_database_authentication.rb
class UserDatabaseAuthentication < ApplicationRecord
  devise :database_authenticatable, :validatable, :registerable, :recoverable

  belongs_to :user
end

これでOKです。ではターミナルに戻って rails db:migrate しましょう。

rails db:migrate

OKですね。
なお、rails db:migrate を実行すると gem 'annotate' を導入してるので、model にはスキーマ情報が出力されているはずです。
便利ですね。

view と controller を準備する

続けてターミナルで view と controller を準備していきます。

# i18n 対応の view 生成
rails g devise:i18n:views users
# 出力
      invoke  Devise::I18n::SharedViewsGenerator
      create    app/views/users/shared
      create    app/views/users/shared/_error_messages.html.erb
      create    app/views/users/shared/_links.html.erb
      invoke  Devise::I18n::MailerViewsGenerator
      create    app/views/users/mailer
      create    app/views/users/mailer/confirmation_instructions.html.erb
      create    app/views/users/mailer/email_changed.html.erb
      create    app/views/users/mailer/password_change.html.erb
      create    app/views/users/mailer/reset_password_instructions.html.erb
      create    app/views/users/mailer/unlock_instructions.html.erb
      invoke  i18n:form_for
      create    app/views/users/confirmations
      create    app/views/users/confirmations/new.html.erb
      create    app/views/users/passwords
      create    app/views/users/passwords/edit.html.erb
      create    app/views/users/passwords/new.html.erb
      create    app/views/users/registrations
      create    app/views/users/registrations/edit.html.erb
      create    app/views/users/registrations/new.html.erb
      create    app/views/users/sessions
      create    app/views/users/sessions/new.html.erb
      create    app/views/users/unlocks
      create    app/views/users/unlocks/new.html.erb
# controller 生成
rails g devise:controllers users
# 出力
      create  app/controllers/users/confirmations_controller.rb
      create  app/controllers/users/passwords_controller.rb
      create  app/controllers/users/registrations_controller.rb
      create  app/controllers/users/sessions_controller.rb
      create  app/controllers/users/unlocks_controller.rb
      create  app/controllers/users/omniauth_callbacks_controller.rb
===============================================================================

Some setup you must do manually if you haven't yet:

  Ensure you have overridden routes for generated controllers in your routes.rb.
  For example:

    Rails.application.routes.draw do
      devise_for :users, controllers: {
        sessions: 'users/sessions'
      }
    end

===============================================================================
# view の日本語訳をいじれるように locale ファイルを出力しておく
rails g devise:i18n:locale ja
      create  config/locales/devise.views.ja.yml

続いて先ほど生成した config/locales/devise.views.ja.yml を少し編集します。

config/locales/devise.views.ja.yml
ja:
  activerecord:
    attributes:
      user:
        confirmation_sent_at: パスワード確認送信時刻
        confirmation_token: パスワード確認用トークン
	nickname: 名前     #← nickname を user 配下に追加。このコメントは実際は不要
...(中略)
  users:           #← devise を users に変更。このコメントも実際は不要
    confirmations:
      confirmed: メールアドレスが確認できました。

config/locales/devise.views.ja.yml の deviseusers に変えるのは
エイリアスなどでも対応できるはずですが、自分はうまく動きませんでした。
以下記事を参考に試してみるのも良いでしょう。

では次に日本語のlocale設定も入れておきましょう。
config/application.rb を開いて

config/application.rb
...(前略)
module App
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.1

    # ----- ここを追記 -----
    # Devise i18n japanese
    config.i18n.default_locale = :ja
    # ----- ここまで -----
...(後略)

routes.rb を編集する

devise の model を分割し、
devise の module はそれぞれの model が持っている形になっているので
routes.rb の書き方が通常とは変わります。
devise の module についてはこの記事にまとめてあります。

ではやっていきましょう。こんな感じです。

Rails.application.routes.draw do
  devise_for :user_registrations, path:'users', :controllers => {
    :confirmations => 'users/confirmations',
  }

  devise_for :user_database_authentications, path:'users', :controllers => {
    :passwords => 'users/passwords',
    :registrations => 'users/registrations',
    :sessions => 'users/sessions',
  }

  # これは実際いらないかも……?
  devise_for :users

  get 'home/index'

  root to: "home#index"
  
  ## 開発環境用letter_opener
  mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end

確認メールを送信する devise :confirmableUserRegistration model が持っているので、 'users/confirmations'UserRegistration と紐付けます。
メール認証やパスワード確認を行う devise :database_authenticatable, :validatableUserDatabaseAuthentication model が持っているので紐付けています。

この状態の routes.rb にすると
例えば通常の devise setup で new_use_confirmation_path となるものが
new_user_registration_confirmation_path となっています。
rails routes コマンドや http://localhost:3000/rails/info/routes などでルーティングの割り当て状況が確認できるので確認しておきましょう。

app/views/layouts/application.html.erb にメッセージ追加

ルーティングが決定したのでリンクを貼っていきます。
こんな感じです。

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>App</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all' %>
  </head>

  <body>
    <header style="margin-top:30px;">
      <nav>
        <!-- user_signed_in? はユーザがログインしているか調べる devise の Helper メソッド -->
        <% if user_signed_in? %> 
          <!-- current_user は現在ログインしているUserオブジェクトを返す devise の Helper メソッド -->
          <!-- *_path はUserモデルを作成したときに、deviseにより自動で作成されてますので、rake routes で確認できます -->
          Logged in as <strong><%= current_user.user_database_authentication.email %></strong>.
          <%= link_to 'プロフィール変更', edit_user_database_authentication_registration_path %> |
          <%= link_to "ログアウト", destroy_user_database_authentication_session_path %>
        <% else %>
          <%= link_to "サインアップ", new_user_registration_confirmation_path %> |
          <%= link_to "ログイン", new_user_database_authentication_session_path %>
        <% end %>
      </nav>
    </header>

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

    <%= yield %>
  </body>
</html>

パスの指定が通常のdevise setup 時と違うのがわかると思います。
なお、パス指定が変わっているので生成された各view に存在する
<%= link_to t('devise.shared.links.back'), :back %> の部分は
一旦消しといてやると作業が楽です。
(もちろん <%= link_to t('devise.shared.links.back'), :back %> を動くように改修してもよい)

controller と view の設定をしていく

では controller と view をmodel分割に対応した形に設定していきます。

確認メール送信部分:confirmations_controller.rb

app/controllers/users/confirmations_controller.rb です。

ポイントとしては以下3点で、

  1. model 分割しない devise では、users/confirmations#create が実行される時に
    User model があることを前提とした動きをするので、
    super を呼ぶ前に UserRegistration を作っておいてやります。

  2. 確認メールを送信した後に使用する path は root にする(これは必須ではない)

  3. 確認メールのリンクをクリックした時に参照される /users/confirmation?confirmation_token=abcdef のリダイレクト先は
    new_user_database_authentication_registration_path となります。

以上を踏まえて編集するとこんな感じです。

app/controllers/users/confirmations_controller.rb
# frozen_string_literal: true

class Users::ConfirmationsController < Devise::ConfirmationsController
  # GET /users/confirmation/new
  def new
    super
  end

  # POST /users/confirmation
  def create()
    registered = UserDatabaseAuthentication.where(email: params[:user_registration][:email]).exists?

    if registered
      flash[:error] = '既に登録されているメールアドレスです。'
      return render :new
    end

    user_registration = UserRegistration.find_or_initialize_by(unconfirmed_email: params[:user_registration][:email])
    if user_registration.save
      super
    else
      flash[:error] = '処理に失敗しました。もう一度お試しください。'
      return render :new
    end
  end

  # GET /users/confirmation?confirmation_token=abcdef
  def show
    # super 側で confirmation_token の検証をしてリダイレクト
    super
  end

  protected

   # 確認メールを送信した後に使用する path
  def after_resending_confirmation_instructions_path_for(resource_name)
    root_path
  end

  # 確認後に使用する path
  def after_confirmation_path_for(resource_name, resource)
    new_user_database_authentication_registration_path
  end
end

users/confirmations#new 部分の view も少し編集しておきます。
confirmation_path(resource) の部分がどうしてもエラーになってしまったので
パスを直接記述するようにしました。

app/views/users/confirmations/new.html.erb
<h2><%= t('.resend_confirmation_instructions') %></h2>

<%= form_for(resource, as: resource_name, url: user_registration_confirmation_path, html: { method: :post }) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %>
  </div>

  <div class="actions">
    <%= f.submit t('.resend_confirmation_instructions') %>
  </div>
<% end %>

なお、 gem 'letter_opener_web' を導入しているので、
確認メール送信後は http://localhost:3000/letter_opener で確認できるはずです。

アカウント作成部分:registrations_controller.rb

先に users/registrations#new の view を編集して nickname をコントローラー側で受け取れるようにしてやります。

余談ですが、こっちの registration_path(resource_name) は動きました。
さっきとの違いがわからない……💦

app/views/users/registrations/new.html.erb
<h2><%= t('.sign_up') %></h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

  <div class="field">
    <%= f.label :nickname %><br />
    <%= text_field_tag(:nickname) %>
  </div>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em><%= t('devise.shared.minimum_password_length', count: @minimum_password_length) %></em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <div class="actions">
    <%= f.submit t('.sign_up') %>
  </div>
<% end %>

では app/controllers/users/registrations_controller.rb です。

users/registrations#create
nickname を受け取り User を作成し、UserDatabaseAuthentication も作成してやります。パスワードの検証は UserDatabaseAuthenticationsave するときに devise がうまくやってくれます。(この辺りの devise のコードを読み解くのが大変でした……)

また、sign_in()UserUserDatabaseAuthentication 両方で実施するように変更します。

こんな感じです。

app/controllers/users/registrations_controller.rb
# frozen_string_literal: true

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

  # GET /users/sign_up
  def new
    super
  end

  # POST /users
  def create
    ActiveRecord::Base.transaction do
      @user = User.new(nickname: params[:nickname])
      @user.save!
      @user_database_authentication = \
        UserDatabaseAuthentication.new( \
          user: @user, \
          email: params[:user_database_authentication][:email], \
          password: params[:user_database_authentication][:password], \
          password_confirmation: params[:user_database_authentication][:password_confirmation])
      @user_database_authentication.save!
    end

    sign_in(:user, @user)
    sign_in(:database_authentication, @user_database_authentication)

    redirect_to root_path
  rescue
    flash[:alert] = '処理に失敗しました。もう一度お試しください。'
    return render :new
  end

  # GET /users/edit
  def edit
    super
  end

  # PUT /users
  def update
  # 更新処理しようとするとなぜか user をSELECTして例外になる
  #   if !resource.valid_password?(account_update_params[:current_password])
  #     flash[:alert] = 'Current password が間違っています'
  #     return render :edit
  #   end

  #   resource.update_with_password(account_update_params)

  #   flash[:notice] = '更新しました'
  #   redirect_to root_path

  # rescue
  #   flash[:alert] = '処理に失敗しました。もう一度お試しください。'
  #   return render :edit
  end

  # DELETE /users
  def destroy
    super
  end

  # GET /users/cancel
  # 通常はサインイン後に
  # 期限切れになるセッションデータを強制的に今すぐ期限切れにします。
  # これは、ユーザーがすべての OAuth セッションデータを削除して、
  # 途中で oauth サインイン/アップをキャンセルしたい場合に便利です。
  def cancel
    super
  end

  protected

  # 許可するための追加のパラメータがある場合は、sanitizer に追加してください
  def configure_sign_up_params
    devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
  end

  # 許可するための追加のパラメータがある場合は、sanitizer に追加してください
  def configure_account_update_params
    devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
  end

  # サインアップ後に使用する path
  def after_sign_up_path_for(resource)
    super(resource)
  end

  # アクティブでないアカウントのサインアップ後に使用する path
  def after_inactive_sign_up_path_for(resource)
    super(resource)
  end
end

なお、 パスワード変更画面からPUTされる users/registrations#update については
どうしてもうまく実装する事ができませんでした……🙇‍♂️ ので、コメントアウトしてあります。

ログイン機能:sessions_controller.rb

最後に app/controllers/users/sessions_controller.rb を改変します。
ここの改変は簡単で、
先ほどと同様に また、sign_in()UserUserDatabaseAuthentication 両方で実施するように変更します。
routes.rb の設定で sessions_controller.rb での devise としての resource は UserDatabaseAuthentication model になっているので、super 側で
UserDatabaseAuthenticationsign_in() は実行されています。
なので、 User 側の sign_in() だけ追加してやりましょう。こんな感じです。

# frozen_string_literal: true

class Users::SessionsController < Devise::SessionsController
  before_action :configure_sign_in_params, only: [:create]

  # GET /users/sign_in
  def new
    super
  end

  # POST /users/sign_in
  def create
    super do |resource|
      sign_in(:user, resource.user)
    end
  end

  # DELETE /users/sign_out
  def destroy
    super
  end

  protected

  # If you have extra params to permit, append them to the sanitizer.
  def configure_sign_in_params
    devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
  end
end

以上

これで最低限「確認メール送信」「アカウント作成」「ログイン」「ログアウト」は動くようになりました!

今回、なるべく devise で生成される controller と view を生かした感じにするよう
devise の model 分割にチャレンジしてみましたが、
かなり難しかったです…… パスワード更新の部分などうまくいってない部分もあるので、
また時間を見つけて再チャレンジしたいなと思っています。
devise の model 分割に詳しい方はコメントももらえると とても助かります💦

長い記事を読んでくださりありがとうございました。

今回のリポジトリはこちらです。
https://github.com/JUNKI555/rails_authentication_practice03

Discussion