【この記事はもう古いので読まないこと！！】Turboを有効化したままRails 7.0でDeviseを使う方法


:::note alert
**この記事は古いです！！**
Devise 4.9.0が正式リリースされたため、この記事の内容はすでに古くなっています。
Devise 4.9の導入方法は以下の記事で説明しています。ですので、こちらを読むようにしてください。
[Devise 4.9をインストールしてRails 7.0 （Hotwire/Turbo）に対応する - Qiita](https://qiita.com/jnchito/items/20fad64ab162b2c49bb9)
:::

## はじめに
Rails 7.0がリリースされて1年以上経ちましたが、Railsのメジャーな認証ライブラリであるDeviseは未だにRails 7.0に部分的にしか対応していません（[参考](https://github.com/heartcombo/devise/blob/main/CHANGELOG.md#481---2021-12-16)）。
そのため、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://rubygems.org/gems/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に組み込んだ状態を想定します。

https://github.com/heartcombo/devise/tree/v4.8.1

まず、`app/controllers/turbo_devise_controller.rb` に以下のコードを追加します。

```ruby: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` のすぐ下あたりが良いでしょう。

```ruby:config/initializers/devise.rb 
# 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つの設定を変更します。

```diff_ruby:config/initializers/devise.rb 
-  # config.parent_controller = 'DeviseController'
+  config.parent_controller = 'TurboDeviseController'
```

```diff_ruby:config/initializers/devise.rb 
-  # config.navigational_formats = ['*/*', :html]
+  config.navigational_formats = ['*/*', :html, :turbo_stream]
```


```diff_ruby:config/initializers/devise.rb 
-  # 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 }`にします。

```diff_erb  
-<%= 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をカスタマイズ可能になっていることが前提です。

```diff_erb:app/views/devise/registrations/edit.html.erb
-<%= 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が使えるはずです！

## 動作検証用に作ったサンプルアプリ

この記事を書くにあたって、以下のサンプルアプリケーションで動作確認しています。
実際の動きやコード全体を見てみたい人はこちらを参考にしてください。

https://github.com/JunichiIto/devise-rails-7-sandbox/tree/japanese

![Screenshot 2023-01-04 at 17.52.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/0096f599-4b1b-7944-de0d-959293d7d7cf.png)
![Screenshot 2023-01-04 at 17.52.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/8653ba84-31ec-3360-466a-5df0a4afd6dc.png)
![Screenshot 2023-01-04 at 17.53.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/0ec8ffd0-393b-0aae-9473-8e1e73fb1560.png)

## テストコード
上記のサンプルアプリにはテストコードもあります。
テストコードでは以下の挙動をテストしています。

- アカウント登録
- ログイン・ログアウト
- アカウント編集
- パスワードリセット
- アカウント削除

https://github.com/JunichiIto/devise-rails-7-sandbox/tree/japanese/test/system

## 参考：OmniAuthでOAuth認証する場合
OmniAuthを利用して、TwitterやGitHubなどのOAuth認証する場合は認証ボタン（認証リンク）を`link_to`ではなく`button_to`に変更し、その上でTurboを無効化（`data: { turbo: false }`を指定）します。
僕が調べた限り、Turboを有効にしたまま認証ボタンを設置する方法はありませんでした。

```diff_erb:app/views/devise/shared/_links.html.erb
-<%= 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リクエストを送る必要があるので、こちらもお忘れなく。

https://rubygems.org/gems/omniauth-rails_csrf_protection/