Devise のモジュール（confirmable, lockable, omniauthable 等の機能）を後から有効化する

成り行き任せで作っていない限りそんなことはあまり起こらないかとは思うのですが、
成り行き任せで作っていてデフォルト（`rails g devise:install` しただけ）では有効化されていない Devise の下記モジュールを後から有効化することになったのでその顛末を記しておきます。

```
:confirmable, :lockable, :timeoutable, :trackable, :omniauthable
```

環境は Windows 10 Pro、Ruby3.2.2、Rails7.1.2、devise4.9.3。
認証機能を入れる（`rails g devise` する）モデルは本稿では `User` としていますので異なる場合は適宜読み替えてください。


## 通常（Devise導入時に同時に有効化）との比較

Devise を導入する際の手順は大雑把に以下のような感じですが（Devise導入手順を仔細に解説した記事はごまんとありますので詳細はそれらをご覧ください）、

> 1. gem を `bundle install`
> 2. インストール（`$ rails g devise:install`）、認証対応モデル作成（`rails g devise User`）
> 3. `User` モデルの `devise` 以下の使いたいモジュール（シンボル）を有効化（コメントアウトを外す）
> 4. 設定ファイル（config/initializers/devise.rb）を編集し各モジュールの動作をカスタマイズ
> 5. 自動生成されたマイグレーションファイル（yyyyMMddHHmmss_add_devise_to_users.rb）の
各モジュール関連カラム `t.型 :カラム名` を必要に応じて有効化しマイグレーション実行
> 6. （必要に応じ）ビュー・コントローラーのカスタマイズ

後からモジュールを有効化する場合、2.までは最初に Devise を導入する際のみ必要な手順なので不要、3.4.(6.) も最初から有効化する場合とやることに特段の違いはありません。
少々注意が必要なのは 5. のマイグレーションと、加えて追加する時点までの作り込み度合い次第では他にも幾つか細部にポイントがありましたので、以下それらについて詳述します。


## マイグレーション

`$ rails g devise:install` した際に同時にモジュールを有効化するなら自動生成されたマイグレーションファイル（yyyyMMddHHmmss_add_devise_to_users.rb）の
当該機能関連カラム（`t.型 :カラム名`）を必要に応じて有効化（コメントアウトを外す）するだけで事足りますが
後から有効化する場合は別途マイグレーションを作らないといけないので、上記の自動生成ファイルでコメントアウトされているところを参考に手動で作成します。

私は次のように対応しました。
まずマイグレーションの作成コマンドは、`User` テーブルにカラムを追加するのでお作法通りなら `$ rails g migrate add_column_to_user (追加するカラム)` ですが
これだと生成されるマイグレーションファイルが Devise のモジュール追加対応のそれと分かりづらい（モジュールごとにマイグレーションを別個に作る場合は尚更）ので
[参考記事](https://qiita.com/hiroky_814/items/8b155875d25ad06cc8c5)等に即し下記のようなコマンドにして実行しました。

```bash
# 例
$ rails g migrate add_confirmable_to_devise
$ rails g migrate add_lockable_to_devise
```

このコマンドに追加カラム等を含めてしまうと 生成するマイグレーションファイル内の追加カラム名・追加先テーブル名がおかしくなる（直せば済む内容ですが）ので含めず
最初の Devise 導入時に自動生成されたマイグレーションファイルのコメントアウト行をベースに、新たに有効化するモジュールに必要な追加カラムを直接追記していきます。

お手軽に自動生成ファイルからのコピペだけで対応したい場合はこんな感じ🔽
（一例：lockable の場合）

```diff_ruby:db/migrate/yyyyMMddHHmmss_add_lockable_to_devise.rb
  class AddLockableToDevise < ActiveRecord::Migration[7.1]
-   def change
-   end
+   def self.up
+     change_table :users do |t|
+       t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
+       t.string   :unlock_token # Only if unlock strategy is :email or :both
+       t.datetime :locked_at
+     end
+
+     add_index :users, :unlock_token, unique: true
+   end
+
+   def self.down
+     # By default, we don't want to make any assumption about how to roll back a migration when your
+     # model already existed. Please edit below which fields you would like to remove in this migration.
+     raise ActiveRecord::IrreversibleMigration
+   end
  end
```

但しこれだとマイグレーションの down（切り戻し）ができない[^1]ので、ちゃんとやるには
<details><summary>change で書き直した方がいいかもしれません。</summary>

```diff_ruby:db/migrate/yyyyMMddHHmmss_add_lockable_to_devise.rb
  class AddLockableToDevise < ActiveRecord::Migration[7.1]
    def change
+     add_column :users, :failed_attempts, :integer, default: 0, null: false # Only if lock strategy is :failed_attempts
+     add_column :users, :unlock_token, :string # Only if unlock strategy is :email or :both
+     add_column :users, :locked_at, :datetime
+
+     add_index :users, :unlock_token, unique: true
    end
  end
```
</details>

このようにしてマイグレーションファイルを必要分だけ作り終えたら
あとはいつも通り `$ rails db:migrate` して完了です。


## confirmable：二段階認証導入に際し必要な対応

confirmable モジュールをONにすると、`User` テーブルの `confirmed_at` カラムの値で二段階認証済か否か判定され（NULLなら未認証）、未認証だとログインができなくなります。

develop 環境のDBに既に作成・使用している `User` データがある場合、生SQLもしくはGUIクライアント等で
`confirmed_at` カラムにNULL以外の適当な日付時刻[^2]を設定してやることでログイン、並びにログインしてないと行えない操作が通常通り行えるようになります。
`rails db:reset` で間々データ整理を行う運用なら seed の初期データも同様に設定してください。

ログインを伴うテストも既にある程度作り込まれていれば `User` が有効：即ちログイン可能である前提の下で書かれたテストが尽く失敗するので
テストでログインに用いる `User` データにも同様に `confirmed_at` カラムに適当な値を設定します。

```diff_yaml:test/fixtures/users.yml
  one:
    name: UserOneName
    profile: UserOneProfile
    email: user1.em@i.l
+   confirmed_at: 2022-02-22 22:22:22  # 適当な過去の日付でOK
```

fixture だけでなく、テスコトード内で create する `User` データへの追加もお忘れなく。

```diff_ruby
  @user = User.create({
-   name: "UserName1", profile: "UserOneProfile", email: "user1.em@i.l"
+   name: "UserName1", profile: "UserOneProfile", email: "user1.em@i.l", confirmed_at: "2022-02-22"
  })
```

confirmable を有効化するとユーザー登録完了後、即ち `RegistrationsController` のユーザー登録アクション実行後のリダイレクト設定：`after_sign_up_path_for` が効かなくなる[^3]ので
当該アクションのテストでリダイレクト先も検証（`assert_redirected_to`）していればテストが失敗します。
仮登録後のリダイレクト先を特に設定しない場合はリダイレクト先期待値をルートのパスに（下記例）、

```diff_ruby:例
  assert_difference 'User.count', 1 do
    post user_path, params: { user: { name: "UserName1", email: "user1.em@i.l" } }
  end
- assert_redirected_to edit_user_path
+ assert_redirected_to root_path
```

設定する場合は `after_inactive_sign_up_path_for` に設定し直した上でリダイレクト先期待値もそれに合わせればテストが通るようになります。

参考記事

https://qiita.com/Tommydevelop/items/606975060c94076980fa


## `resource_class` の設定

もし `User` 以外のコントローラーから Devise のビューを `render` していれば
ヘルパー（そのモデルのヘルパーもしくはapplication_helper）辺りに `resource_class` `resource_name` `resource` `devise_mapping` の定義が必要になります。
基本的に4つセットで定義すると思うのでその場合は Devise モジュールを後から追加する際も特に何もする必要はないんですが
未追加＝デフォルト6モジュールのみの時は `resource_class` だけは未定義でも動くようで、私の手元ではモジュール追加した際に `resource_class` の定義がない旨怒られました。
```
ActionView::Template::Error: undefined local variable or method `resource_class'
```

普通にやっていればあまりこういうことにはならなそうな気はするんですが（私の手元でどういう経緯でそうなってたのかは不明）、
ともかく必要ということなので下記のように定義したらエラーが消えました。

```diff_ruby:app/helpers/application_helper.rb
  module ApplicationHelper
    # 略

    def devise_mapping
      @devise_mapping ||= Devise.mappings[:user]
    end
    def resource_name
      devise_mapping.name
    end
+   def resource_class
+     devise_mapping.to
+   end
    def resource
      @resource ||= (current_user || User.new)
    end
  end
```


## `routes.default_url_options[:host]` の設定

テストを実行した際に下記のようなエラーも出たので調べたところ、

```
ActionView::Template::Error: Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true
```

`Rails.application.routes.default_url_options[:host]` の定義が `config/environments` に
必要ということのようなので
ひとまず test.rb に下記のような定義を入れたらテストは無事通りました。

```ruby:config/environments/test.rb
host = 'localhost'
Rails.application.routes.default_url_options[:host] = host
```

develop ではこれを入れなくとも一通り動いている様子ではあるのですが、test のように網羅的には確認が難しい＆要否の確たる情報も見つけられなかったので
一応保険のため production 共々同様の設定を入れておきました（production では勿論 `localhost` ではなく本番環境のホスト名）。
この辺り確たる情報をお持ちの方いましたら補足・ご教示頂けますと幸いです。


## OAuth 導入に際し必要な対応

下記参考資料通り対応したら問題なく動いたので基本はそれで問題なさそうです。

https://qiita.com/kazuooooo/items/47e7d426cbb33355590e

https://qiita.com/cigalecigales/items/16ce0a9a7e79b9c3974e#twitter%E3%81%A7%E8%AA%8D%E8%A8%BC%E3%81%99%E3%82%8B

https://fuga-ch85.hatenablog.com/entry/2021/09/10/163606

ただ少々補足が必要な箇所もありますので以下に記します。


### unique 制約付与に伴う対処

この対応で新たに追加する `provider`, `uid` カラムについて、上記参考記事のうち3つ目だけ
両カラム組合せにunique 制約を付していますが
その目的：同じサービスプロバイダから何度も登録できないようにする――というのは公開アプリなら必須と思われるのでこれに倣いました。

今後新規に作成されるレコードへの対処（記事内にある `build_resource` 実装）の他
既存レコードもユニーク制約を満たさないとDBがエラーで起動できなくなるので、
前述の二段階認証：confirmable 導入に伴う対応と同様に既存レコードは生SQLやGUIクライアント等で直接修正、
テストコード内での生成レコードや fixture・seeds についてはファイルを修正し制約を満たすようにします。

ユニーク制約が満たせれば両カラムの値はどのような組合せでも動きますが、意味合いも鑑みれば
`provider` は空のまま（自サービス内で登録完結＝OAuthプロバイダーは存在しない）[^4]、
uid に連番やidと同じ値を設定するのが分かりやすくてよいのではないかと思います。


### X(twitter) の設定画面

X(twitter) のOAuth設定についても、買収やら何やらの絡みもあってかX側の設定画面等々の仕様が上記の参考記事とはだいぶ異なっていました。
無料でOAuth認証を使うための最新（2024年7月現在）の設定画面・操作方法は下記記事が参考になります。

https://qiita.com/neru-dev/items/857cc27fd69411496388

概ねこれに従って設定すれば問題ない[^5]ですが、OAuth1.0の認証を利用するだけ[^6]なら

- 必要なキーは「Consumer API Key」「Consumer API Secret」のみ
- App permissions は「Read」でよいが、「Request email from users」はONにする

で大丈夫でした。API Key/Secret を Credentials や環境変数等を介して下記のように
`config.omniauth` に設定してやれば動きます（下記はCredentialsで設定する場合の一例）。

```ruby:config/initializers/devise.rb
config.omniauth :twitter,
                Rails.application.credentials.oauth.twitter.api_key,
                Rails.application.credentials.oauth.twitter.api_secret
```

```yml:config/credentials/production.yml(.enc)
oauth:
  twitter:
    api_key: <<<Consumer API Key の値を記載>>>
    api_secret: <<<Consumer API Secret の値を記載>>>

# 他項目は略
```

なおOAuth導入全般の参考記事2つ目には `new_with_session` の実装が必須：ないとtwitterから送られてくる情報が登録されず毎回サインアップ＝新規登録になってしまう
――とありますが、実装していなくとも特段そのようなことはなく登録できている様子です。これも古い仕様の話かもしれません。


## おまけ

後で気付きましたが、confirmable, lockable, trackable を後から追加する方法については
本家 Github の Wiki に記事がありました。本稿 [マイグレーション](#%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3) で記載の内容＋α
相当の記載がありましたので、英語ではありますがこちらも参考になりそうです。

https://github.com/heartcombo/devise/wiki/How-To:-Add-:confirmable-to-Users

https://github.com/heartcombo/devise/wiki/How-To:-Add-:lockable-to-Users

https://github.com/heartcombo/devise/wiki/How-To:-Add-:trackable-to-Users



[^1]: `self.down` を実装していない（`ActiveRecord::IrreversibleMigration` 例外を送出している）ためですが、自動生成ファイルからのコピペした結果がこれということはつまり Devise 初期導入時のマイグレーションも切り戻し不可なので、それに合わせた体ならこれでも特に問題はないかと
[^2]: 試したところ未来の日付でも何ら変わりなく動いたので本当にNULLか否かでしか判定してないようですが、意味（及び今後の仕様変更の可能性）を考えると大人しく過去の日付にしといた方がよいかと
[^3]: 仮登録しか完了してない状態を本登録済と同等に扱うのはそぐわないため…か?（詳細不明）
本登録完了（認証メールリンク押下）後のリダイレクト先は `after_confirmation_path_for` で設定できる
[^4]: OAuth導入後にOAuthを使わず自サービス上で登録したユーザーも
参考記事に即した実装の他に特に設定を入れてない限り `provider` カラムはnilになる
[^5]: 250字以上の英作文は少々ハードルが高いが参考記事にあるようにAIの力を借りるなり頑張るなりして書く
[^6]: OAuth2.0対応はまだ試してないので試して何か知見が得られたら追記or別途記事作成します