この記事は古いです!!
Devise 4.9.0が正式リリースされたため、この記事の内容はすでに古くなっています。
Devise 4.9の導入方法は以下の記事で説明しています。ですので、こちらを読むようにしてください。
Devise 4.9をインストールしてRails 7.0 (Hotwire/Turbo)に対応する - Qiita
はじめに
Rails 7.0がリリースされて1年以上経ちましたが、Railsのメジャーな認証ライブラリであるDeviseは未だにRails 7.0に部分的にしか対応していません(参考)。
そのため、Rails 7.0でrails new
したRailsアプリケーション(つまり、Turboが有効になっている状態)だと、Deviseを使おうと思っても意図しない挙動になります。
そこで、この記事ではTurboを有効化したままRails 7.0でDeviseを使う方法を紹介します。
なお、ネットを検索するとこの記事と同じテーマで書かれた記事がすでにいくつもありますが、今回紹介する方法はかなりシンプルかつ、実際のサンプルアプリ(とテストコード)で動作確認済みなので、個人的にはかなりお勧めです。
対象バージョン
この記事は以下のバージョンで動作確認しています(それぞれ2023年1月4日時点での最新バージョンです)。
- Rails 7.0.4
- Devise 4.8.1
- turbo-rails 1.3.2
- Ruby 3.2.0
Deviseのバージョンに要注意!
将来的にDeviseがRails 7.0に対応した場合、この記事の内容はおそらく不要なものになります。
この記事を読み進める前に、以下のリンクを開いてDeviseの新しいバージョンがリリースされていないか確認しておきましょう。
参考文献
この記事は以下のリンクで紹介されていた対応例をベースにして、少し修正を加えたものです。
各コードの技術的な説明は以下のリンクを参考にしてください。
- https://gorails.com/episodes/devise-hotwire-turbo
- https://gorails.com/forum/how-to-use-devise-with-hotwire-turbo-js-discussion#forum_post_17983
それでは以下が本編です。
手順
事前条件として、DeviseのREADMEのとおりにDeviseをRailsに組み込んだ状態を想定します。
まず、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
次に、config/initializers/devise.rb
を開いて、TurboFailureAppクラスの定義を追加します。
場所は # frozen_string_literal: true
のすぐ下あたりが良いでしょう。
# 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
# (以下略)
さらに、config/initializers/devise.rb
にある以下の3つの設定を変更します。
- # 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
ログアウトのリンクはmethod: :delete
ではなく、data: { turbo_method: :delete }
にします。
-<%= link_to 'ログアウト', destroy_user_session_path, method: :delete %>
+<%= link_to 'ログアウト', destroy_user_session_path, data: { turbo_method: :delete } %>
アカウント削除のボタンはdata: { confrim: "Are you sure?" }
ではなく、data: { turbo_confirm: "Are you sure?" }
とします。
ただし、この変更はrails generate devise:views
(devise-i18nを使っている場合はrails g devise:i18n:views
)でDeviseのviewをカスタマイズ可能になっていることが前提です。
-<%= 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 %>
手順は以上です。
これでTurboを有効にしたままRails 7.0でDeviseが使えるはずです!
動作検証用に作ったサンプルアプリ
この記事を書くにあたって、以下のサンプルアプリケーションで動作確認しています。
実際の動きやコード全体を見てみたい人はこちらを参考にしてください。
テストコード
上記のサンプルアプリにはテストコードもあります。
テストコードでは以下の挙動をテストしています。
- アカウント登録
- ログイン・ログアウト
- アカウント編集
- パスワードリセット
- アカウント削除
参考:OmniAuthでOAuth認証する場合
OmniAuthを利用して、TwitterやGitHubなどのOAuth認証する場合は認証ボタン(認証リンク)をlink_to
ではなくbutton_to
に変更し、その上でTurboを無効化(data: { turbo: false }
を指定)します。
僕が調べた限り、Turboを有効にしたまま認証ボタンを設置する方法はありませんでした。
-<%= 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 />
リンクがボタンになるので見た目が少し変わりますが、その点は必要に応じてCSSでお化粧してあげてください。
なお、Turboの対応とは無関係ですが、OmniAuth 2.0以上では omniauth-rails_csrf_protection gemを使って認証時にPOSTリクエストを送る必要があるので、こちらもお忘れなく。