iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🛤️

Splitting Devise Models in Rails (with docker-compose)

に公開

split devise models ...

TL;DR

About splitting devise models

I posted an article introduction before; please refer to that for the benefits and such.
When I introduced the article previously, I hadn't fully grasped it, but now that I understand it a bit better, I decided to write this article.
However, some parts still don't work quite right... (described later)

This topic starts from the section "Creating models for authentication and authorization" in this article, so feel free to skip ahead to there.
I've named the models to match the article as much as possible.

Preparing docker-compose, running bundle init and rails new

This is the same as the previous articles mentioned, but I'll go through the steps again since it's been a while.

Let's get through it quickly.

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
# * Uncomment the following lines when running rails s with docker-compose up
# 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.
# * Uncomment the following line when running rails s with docker-compose up
# 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:

I commented out the CMD ["rails", "server", "-b", "0.0.0.0"] part in the Dockerfile, so to be precise, forDocker/rails/entrypoint.sh isn't strictly necessary at this timing, but it's fine since it doesn't cause any issues.

I've changed docker-compose.yml a bit from previous articles:

Now, let's proceed with the initial setup.
It's easier to start the terminal work below while opening the working directory in VSCode or a similar editor.

# Enable the port for the service (3000), make it assignable to the host, and start it up
docker-compose run --rm --service-ports app bash
# Wait a bit even after the bash login (waiting for MySQL to start up)
# Confirm connection to mysql as root without a password
mysql -u root -h db -e 'select version();'
# Check bundle config. Ensure BUNDLE_APP_CONFIG: "/usr/local/bundle"
bundle config
# Create Gemfile
bundle init
# Add rails (OK to edit Gemfile directly then run bundle install)
bundle add rails --version "~> 6.1.2.1"
bundle install
# rails new
#   -B: skip bundle install, -S: skip sprockets
#   -T: skip test::unit, -J: skip javascript
#   -d mysql: database type, --force: overwrite if files exist
rails new . -B -S -T -J -d mysql --force
bundle install

OK. I'll add simpacker while I'm at it.
Keep the terminal open, add simpacker to the Gemfile:

Gemfile
...(omitted)
gem "simpacker"
...(omitted)

Then back in the terminal:

# Install simpacker
bundle install
# Initialize simpacker
rails simpacker:install

I'll remove webpack.
Keep the terminal open and clear the devDependencies in package.json for now.

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

Since running rails simpacker:install installs packages with npm, I'll delete the unnecessary files and re-run yarn.
Back to the terminal again:

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

This sets up Simpacker without any extra packages.
If you are using git, make sure to add /node_modules/* to your .gitignore as well.

Once this is finished, let's get the Rails application ready.
Keep the terminal open and edit config/database.yml.

config/database.yml
...(omitted)
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: 6lt;%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %6gt;
  url: mysql2://root:@db:3306
  # username: root
  # password:
  # host: localhost
...(omitted)

After editing config/database.yml, prepare the database and try starting Rails.
Back to the terminal once more:

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

Did you see the usual "Yay! You’re on Rails!" at http://localhost:3000/?
Great. Close the server with Ctrl+c.

Introducing devise

Keep the terminal open and add the following to your Gemfile.
Along with it, I'll also add gems needed for Japanese localization, a gem for checking emails in the development environment, a gem to output schema information to models, and action_args.

Gemfile
# Authentication and Authorization
gem 'devise'
gem 'devise-i18n'

# Parameterization of controller action arguments
gem 'action_args'

group :development do
  # Email confirmation for development environment
  gem 'letter_opener_web'
  # Schema information output
  gem 'annotate'
...(omitted)

After editing the Gemfile, return to the terminal:

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

When you run bundle exec rails g devise:install, the manual setup instructions will be displayed as follows:

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 *

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

Now, let's proceed with the preparation according to the suggested steps.

1. Configure email sending settings for the development environment

With the terminal open, add the following to config/environments/development.rb. I'll also include the settings for letter_opener_web together.
You can put it anywhere, but I'll add it around here.

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

  # ----- Add this -----
  # Devise mailer setting
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
  config.action_mailer.delivery_method = :letter_opener_web
  # ----- End addition -----

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

Preparation for 2. Changing the type of files generated by rails g controller

Before creating the View that will serve as the root_url, let's change the types of files generated by rails g controller. I don't need helpers and such. (If I need them, I can create them myself.)
Edit 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
  ...(omitted)
    # ----- Add this -----
    # Change files generated by rails generate
    config.generators do |g|
      g.assets false             # Do not generate CSS/JS files
      g.skip_routes false        # If true, do not modify routes.rb
      g.helper false             # Do not generate helpers
    end
    # ----- End addition -----
  end

2. Create the View for root_url

Let's use home this time.
Back to the terminal:

rails g controller Home index
# Output
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

Thanks to the previous settings, helpers and CSS are not generated. Perfect.
Keep the terminal open and edit the route definition in config/routes.rb.
I'll also include the letter_opener_web settings while I'm at it.

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

  root to: "home#index"
  
  ## letter_opener for development environment
  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. Add messages to app/views/layouts/application.html.erb

...I thought about it, but let's do it after configuring the routing in routes.rb later.
First, let's edit config/initializers/devise.rb slightly.

config/initializers/devise.rb
  # The default HTTP method used to sign out a resource. Default is :delete.
  config.sign_out_via = :get # Edit this from :delete to :get

Creating models for authentication and authorization

Now, finally, to the main topic.
Back to the terminal to continue the work.
First, let's create the foundation with rails g devise User.

rails g devise User
## Output
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

The migration filename will vary for everyone, so check the output when you run rails g devise User.
For this time, we will make db/migrate/20210212083404_devise_create_users.rb very simple. Like this. It's a table with almost no information.

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

So, what about the information required for devise?
We'll prepare that in other models. Back to the terminal again:

# Model for sign up 
rails g devise UserRegistration
# Output
      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
# Model for email+password authentication
rails g devise UserDatabaseAuthentication
# Output
      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

Edit each migration file as follows.

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 does not need to be associated with 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

By defining models that take responsibility for each devise module, we limit the information managed by each model as much as possible.
Now, the .rb files for each model will look like this:

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

This looks good. Now, return to the terminal and run rails db:migrate.

rails db:migrate

Success.
Also, since gem 'annotate' is installed, running rails db:migrate should have output the schema information into the models.
Very convenient.

Preparing views and controllers

Next, let's prepare the views and controllers in the terminal.

# Generate i18n-supported views
rails g devise:i18n:views users
# Output
      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
# Generate controllers
rails g devise:controllers users
# Output
      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

===============================================================================
# Output locale file to allow modifying view translations
rails g devise:i18n:locale ja
      create  config/locales/devise.views.ja.yml

Next, edit the recently generated config/locales/devise.views.ja.yml slightly.

config/locales/devise.views.ja.yml
ja:
  activerecord:
    attributes:
      user:
        confirmation_sent_at: Confirmation sent at
        confirmation_token: Confirmation token
	nickname: Name     # <- add nickname under user. This comment is actually unnecessary
...(omitted)
  users:           # <- change devise to users. This comment is also unnecessary
    confirmations:
      confirmed: Your email address has been successfully confirmed.

Changing devise to users in config/locales/devise.views.ja.yml should be possible with aliases, but I couldn't get it to work correctly.
You might want to refer to the following articles and try it out.

Now, let's also set the Japanese locale configuration.
Open config/application.rb and add the following:

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

    # ----- Add this -----
    # Devise i18n japanese
    config.i18n.default_locale = :ja
    # ----- End addition -----
...(omitted)

Editing routes.rb

Since we've split the devise models and each module is now held by its respective model, the way routes.rb is written will differ from the usual setup.
I have summarized the devise modules in this article:

Let's get to it. It looks like this:

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',
  }

  # This might actually be unnecessary...?
  devise_for :users

  get 'home/index'

  root to: "home#index"
  
  ## letter_opener for development environment
  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

The devise :confirmable module, which sends confirmation emails, is held by the UserRegistration model, so we associate 'users/confirmations' with UserRegistration.
The modules for email authentication and password confirmation, devise :database_authenticatable, :validatable, are held by the UserDatabaseAuthentication model, so we associate them accordingly.

With this routes.rb setup, for example, what would normally be new_user_confirmation_path in a standard devise setup becomes new_user_registration_confirmation_path.
You can check the routing assignments using the rails routes command or by visiting http://localhost:3000/rails/info/routes.

Adding messages to app/views/layouts/application.html.erb

Now that the routing is decided, let's add the links.
It looks like this:

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? is a devise Helper method to check if the user is logged in -->
        <% if user_signed_in? %> 
          <!-- current_user is a devise Helper method that returns the currently logged-in User object -->
          <!-- *_path is automatically created by devise when the User model is created, which can be confirmed with rake routes -->
          Logged in as <strong><%= current_user.user_database_authentication.email %></strong>.
          <%= link_to 'Change Profile', edit_user_database_authentication_registration_path %> |
          <%= link_to "Logout", destroy_user_database_authentication_session_path %>
        <% else %>
          <%= link_to "Sign Up", new_user_registration_confirmation_path %> |
          <%= link_to "Login", new_user_database_authentication_session_path %>
        <% end %>
      </nav>
    </header>

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

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

You can see that the path specification is different from the standard devise setup.
By the way, since the path names have changed, it's easier to temporarily delete the parts containing <%= link_to t('devise.shared.links.back'), :back %> present in each generated view.
(Of course, you could also modify <%= link_to t('devise.shared.links.back'), :back %> to make it work.)

Setting up controllers and views

Now, let's configure the controllers and views to support the model split.

Confirmation email sending part: confirmations_controller.rb

This is app/controllers/users/confirmations_controller.rb.

The key points are the following three:

  1. In devise without model split, it operates on the assumption that a User model exists when users/confirmations#create is executed, so we create a UserRegistration before calling super.

  2. The path used after sending the confirmation email is set to root (this is optional).

  3. The redirect destination for /users/confirmation?confirmation_token=abcdef when the link in the confirmation email is clicked will be new_user_database_authentication_registration_path.

Taking these into account, the edited file looks like this:

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] = 'This email address is already registered.'
      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] = 'Process failed. Please try again.'
      return render :new
    end
  end

  # GET /users/confirmation?confirmation_token=abcdef
  def show
    # The super side validates the confirmation_token and redirects
    super
  end

  protected

   # Path to use after resending confirmation instructions
  def after_resending_confirmation_instructions_path_for(resource_name)
    root_path
  end

  # Path to use after confirmation
  def after_confirmation_path_for(resource_name, resource)
    new_user_database_authentication_registration_path
  end
end

We will also edit the view part of users/confirmations#new slightly.
Since the confirmation_path(resource) part kept resulting in an error, I decided to write the path directly.

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

By the way, since gem 'letter_opener_web' is installed, you should be able to check the confirmation email at http://localhost:3000/letter_opener after it is sent.

Account creation part: registrations_controller.rb

First, edit the users/registrations#new view so that the nickname can be received on the controller side.

As a side note, registration_path(resource_name) worked here.
I'm not sure what the difference was from earlier... 💦

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

Now, let's look at app/controllers/users/registrations_controller.rb.

In users/registrations#create, we receive the nickname, create a User, and also create a UserDatabaseAuthentication. For password validation, devise handles it appropriately when save is called on UserDatabaseAuthentication. (Deciphering the devise code in this area was quite a task...)

Also, we change sign_in() to be executed for both User and UserDatabaseAuthentication.

It looks something like this:

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] = 'Process failed. Please try again.'
    return render :new
  end

  # GET /users/edit
  def edit
    super
  end

  # PUT /users
  def update
  # Attempting to update somehow results in a SELECT for user and throws an exception
  #   if !resource.valid_password?(account_update_params[:current_password])
  #     flash[:alert] = 'Current password is incorrect'
  #     return render :edit
  #   end

  #   resource.update_with_password(account_update_params)

  #   flash[:notice] = 'Updated successfully'
  #   redirect_to root_path

  # rescue
  #   flash[:alert] = 'Process failed. Please try again.'
  #   return render :edit
  end

  # DELETE /users
  def destroy
    super
  end

  # GET /users/cancel
  # Forces the session data which usually expires after sign
  # in to be expired now. This is useful if the user wants to
  # cancel oauth signing in/up halfway and remove all OAuth session data.
  def cancel
    super
  end

  protected

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

  # If you have extra parameters to permit, append them to the sanitizer.
  def configure_account_update_params
    devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
  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

Note that I wasn't able to successfully implement users/registrations#update, which is PUT from the password change screen... 🙇‍♂️ So, I've left it commented out.

Login function: sessions_controller.rb

Finally, let's modify app/controllers/users/sessions_controller.rb.
This modification is simple. Just as before, we'll change it so that sign_in() is executed for both User and UserDatabaseAuthentication.
Since the devise resource in sessions_controller.rb is set to the UserDatabaseAuthentication model through the routes.rb configuration, sign_in() for UserDatabaseAuthentication is already executed on the super side.
So, we just need to add the sign_in() for the User side. It looks like this:

app/controllers/users/sessions_controller.rb
# 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

Conclusion

Now at least "Confirmation email sending", "Account creation", "Login", and "Logout" are working!

This time, I challenged myself to split the devise models while trying to keep the controllers and views generated by devise as intact as possible, but it was quite difficult...
Since there are some parts that aren't working perfectly, such as the password update section, I'm hoping to try again when I find the time.
If anyone is knowledgeable about splitting devise models, it would be a huge help if you could leave a comment 💦

Thank you for reading such a long article.

Here is the repository for this project.
https://github.com/JUNKI555/rails_authentication_practice03

Discussion