iTranslated by AI
Splitting Devise Models in Rails (with docker-compose)
split devise models ...
TL;DR
- The part about setup for
deviseis almost the same as this article:- Trying Rails 6.1 + devise by running rails new under docker-compose | Junya Kitayama | zenn
- Making Rails 6.1 + devise controllers and views customizable | Junya Kitayama | zenn
- The story about splitting
devisemodels is written in this article introduction:- Article Intro: Splitting devise models to introduce the Single Responsibility Principle | Junya Kitayama | zenn
- Here is the repository for this time:
- (The article turned out to be quite long, so looking at the repository might be faster)
- https://github.com/JUNKI555/rails_authentication_practice03
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)
- Model structure for modern user authentication in devise explained by the author of Perfect Rails | joker1007’s diary
- Article Intro: Splitting devise models to introduce the Single Responsibility Principle | Junya Kitayama | zenn
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
[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
#!/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 "$@"
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"]
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:
- Changed the file to
version: '3.8'. - Added
bundle_install:/usr/local/bundle.- By persisting the
bundle installdestination, I removed the need for thebundle install --path foospecification and shortened the startup time by not having to runbundle installevery time. - Behavior of bundler in official Ruby Docker images | freedom-man.com
- My personal best practices for Rails Dockerfiles | kawabatas tech blog
- By persisting the
- Added
platform: linux/x86_64to themysql:8.0image.- This specification seems necessary to pull an image that works on M1 Macs.
- While there are methods online to specify the ID from the DIGEST column, this specification worked for me.
- Is it because the mysql image's multi-CPU architecture support doesn't include ARM...? I don't understand the exact reason yet.
-
platform | Compose file version 2 reference | Docker Documentation
- "This parameter determines which version of the image will be pulled and/or on which platform the service’s build will be performed."
- Docker (Apple Silicon/M1 Preview) MySQL "no matching manifest for linux/arm64/v8 in the manifest list entries" | Stack Overflow
- This specification seems necessary to pull an image that works on M1 Macs.
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:
...(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
{
"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.
...(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.
# 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.
...(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.
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.
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.
# 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.
# 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.
# 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
# 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:
class User < ApplicationRecord
devise :authenticatable
has_one :user_database_authentication, dependent: :destroy
end
class UserRegistration < ApplicationRecord
devise :confirmable
end
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.
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.
- [Rails] A trick to translate scoped views with devise-i18n | Qiita
- devise-i18n doesn't work when customizing Devise View | myMemoBlog by 256hax
Now, let's also set the Japanese locale configuration.
Open config/application.rb and add the following:
...(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:
- Translation of gem devise modules | Junya Kitayama | zenn
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:
<!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:
-
In devise without model split, it operates on the assumption that a
Usermodel exists whenusers/confirmations#createis executed, so we create aUserRegistrationbefore callingsuper. -
The path used after sending the confirmation email is set to root (this is optional).
-
The redirect destination for
/users/confirmation?confirmation_token=abcdefwhen the link in the confirmation email is clicked will benew_user_database_authentication_registration_path.
Taking these into account, the edited file looks like this:
# 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.
<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... 💦
<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:
# 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:
# 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.
Discussion