DEV Community

Junichi Ito
Junichi Ito

Posted on • Updated on

[DON'T READ] How to customize Devise for Rails 7.0 and Turbo

πŸ”” 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!):

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
Enter fullscreen mode Exit fullscreen mode

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
# ...
Enter fullscreen mode Exit fullscreen mode

Plus, apply the following change to config/initializers/devise.rb:

-  # config.parent_controller = 'DeviseController'
+  config.parent_controller = 'TurboDeviseController'
Enter fullscreen mode Exit fullscreen mode
-  # config.navigational_formats = ['*/*', :html]
+  config.navigational_formats = ['*/*', :html, :turbo_stream]
Enter fullscreen mode Exit fullscreen mode
-  # 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
Enter fullscreen mode Exit fullscreen mode

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 } %>
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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

Sign in
Signed in
Signed out

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 />
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)