[DON'T READ] How to customize Devise for Rails 7.0 and Turbo
Junichi Ito
Posted on January 4, 2023
๐ NOTE: Devise 4.9.0 has been released. Please consider upgrading Devise instead of reading this post.
As of Devise 4.8.1, they say "Turbo integration is not fully supported by Devise yet." So you need to customize Devise settings for it. This post describes how to do it.
I tested against the following environments:
- Ruby on Rails 7.0.4
- Devise 4.8.1
- turbo-rails 1.3.2
- Ruby 3.2.0
And my post is a little modified from these articles (They helped me a lot!):
- https://gorails.com/episodes/devise-hotwire-turbo
- https://gorails.com/forum/how-to-use-devise-with-hotwire-turbo-js-discussion#forum_post_17983
You can refer them to understand technical details.
Preconditions
You already install Devise to your Rails app according to Devise's README.
How to customize
Add the following code to app/controllers/turbo_devise_controller.rb
:
class TurboDeviseController < ApplicationController
class Responder < ActionController::Responder
def to_turbo_stream
if @default_response
@default_response.call(options.merge(formats: :html))
else
controller.render(options.merge(formats: :html))
end
rescue ActionView::MissingTemplate => error
if get?
raise error
elsif has_errors? && default_action
if respond_to?(:error_rendering_options, true)
# For responders 3.1.0 or higher
render error_rendering_options.merge(formats: :html, status: :unprocessable_entity)
else
render rendering_options.merge(formats: :html, status: :unprocessable_entity)
end
else
navigation_behavior error
end
end
end
self.responder = Responder
respond_to :html, :turbo_stream
end
Then, open config/initializers/devise.rb
and define TurboFailureApp class:
# frozen_string_literal: true
class TurboFailureApp < Devise::FailureApp
def respond
if request_format == :turbo_stream
redirect
else
super
end
end
alias skip_format? is_navigational_format?
end
# Assuming you have not yet modified this file, each configuration option below
# ...
Plus, apply the following change to config/initializers/devise.rb
:
- # config.parent_controller = 'DeviseController'
+ config.parent_controller = 'TurboDeviseController'
- # config.navigational_formats = ['*/*', :html]
+ config.navigational_formats = ['*/*', :html, :turbo_stream]
- # config.warden do |manager|
- # manager.intercept_401 = false
- # manager.default_strategies(scope: :user).unshift :some_external_strategy
- # end
+ config.warden do |manager|
+ manager.failure_app = TurboFailureApp
+ end
Your sign-out link needs to replace method: :delete
to data: { turbo_method: :delete }
:
-<%= link_to 'Sign out', destroy_user_session_path, method: :delete %>
+<%= link_to 'Sign out', destroy_user_session_path, data: { turbo_method: :delete } %>
If you have "Cancel my account" button in app/views/devise/registrations/edit.html.erb
, you need to use turbo_confirm
instead of confirm
to pop-up confirmation dialog. Please run rails generate devise:views
to generate customizable Devise views:
-<%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>
+<%= button_to "Cancel my account", registration_path(resource_name), data: { turbo_confirm: "Are you sure?" }, method: :delete %>
That's all! Now you are ready to use Devise with Rails 7.0 and Turbo!
You can download and run my codes from here:
https://github.com/JunichiIto/devise-rails-7-sandbox
Testing them
Here are my codes to test Devise's behavior.
You can see whole codes here:
https://github.com/JunichiIto/devise-rails-7-sandbox/tree/main/test/system
Sign up
# frozen_string_literal: true
require 'application_system_test_case'
class SignUpTest < ApplicationSystemTestCase
test 'sign up' do
visit root_path
assert_css 'h2', text: 'Log in'
click_link 'Sign up'
# test validation errors
click_button 'Sign up'
assert_text '2 errors prohibited this user from being saved'
fill_in 'Email', with: 'bob@example.com'
fill_in 'Password', with: 'password'
fill_in 'Password confirmation', with: 'password'
click_button 'Sign up'
assert_text 'Welcome! You have signed up successfully.'
assert_text 'Account: bob@example.com'
assert_css 'h1', text: 'Welcome!'
end
end
Sign in and sign out
# frozen_string_literal: true
require 'application_system_test_case'
class SignInOutTest < ApplicationSystemTestCase
test 'sign in and sign out' do
visit root_path
assert_css 'h2', text: 'Log in'
# test validation errors
fill_in 'Email', with: 'bob@example.com'
fill_in 'Password', with: 'foobar'
click_button 'Log in'
assert_text 'Invalid Email or password.'
fill_in 'Email', with: 'alice@example.com'
fill_in 'Password', with: 'password'
click_button 'Log in'
assert_text 'Signed in successfully.'
assert_css 'h1', text: 'Welcome!'
click_link 'Sign out'
assert_text 'Signed out successfully.'
assert_css 'h2', text: 'Log in'
end
end
Edit account
# frozen_string_literal: true
require 'application_system_test_case'
class EditAccountTest < ApplicationSystemTestCase
setup do
sign_in_as_alice
end
test 'edit account' do
click_link 'Edit account'
# test validation errors
fill_in 'Email', with: ''
click_button 'Update'
assert_text '2 errors prohibited this user from being saved'
fill_in 'Email', with: 'bob@example.com'
fill_in 'Current password', with: 'password'
click_button 'Update'
assert_text 'Your account has been updated successfully.'
assert_text 'Account: bob@example.com'
assert_css 'h1', text: 'Welcome!'
end
end
Reset password
# frozen_string_literal: true
require 'application_system_test_case'
class ResetPasswordTest < ApplicationSystemTestCase
test 'reset password' do
visit root_path
assert_css 'h2', text: 'Log in'
click_link 'Forgot your password?'
# test validation errors
click_button 'Send me reset password instructions'
assert_text '1 error prohibited this user from being saved'
fill_in 'Email', with: 'alice@example.com'
click_button 'Send me reset password instructions'
assert_text 'You will receive an email with instructions on how to reset your password in a few minutes.'
assert_css 'h2', text: 'Log in'
mail = ActionMailer::Base.deliveries.last
m = mail.body.encoded.match(%r{http://example.com/(?<path>[-\w_?/=]+)})
visit m[:path]
assert_css 'h2', text: 'Change your password'
# test validation errors
click_button 'Change my password'
assert_text '1 error prohibited this user from being saved'
fill_in 'New password', with: 'pass1234'
fill_in 'Confirm new password', with: 'pass1234'
click_button 'Change my password'
assert_text 'Your password has been changed successfully.'
assert_css 'h1', text: 'Welcome!'
end
end
Cancel account
# frozen_string_literal: true
require 'application_system_test_case'
class CancelAccountTest < ApplicationSystemTestCase
setup do
sign_in_as_alice
end
test 'cancel account' do
click_link 'Edit account'
accept_alert do
click_button 'Cancel my account'
end
assert_text 'Bye! Your account has been successfully cancelled. We hope to see you again soon.'
assert_css 'h2', text: 'Log in'
end
end
If you're using OmniAuth
If you're using OmniAuth to sign-in, you need to use button_to
instead of link_to
, and disable Turbo by data: { turbo: false }
:
-<%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post %><br />
+<%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), data: { turbo: false } %><br />
Please don't forget to use omniauth-rails_csrf_protection gem for OmniAuth 2.0 or higher. This requirement is not related to Turbo, though.
Posted on January 4, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024