devise_auth_tokenを使った新規登録機能の実装

この記事はプログラミング学習者がアプリ開発中に躓いた内容を備忘録として記事におこしたものです。内容に不備などあればご指摘頂けると助かります。

今回はRails(API) x ReactでX(旧Twitter)のクローンサイトを作る際に実装した新規登録機能について実装中に多々躓いた所があったので、備忘録を兼ねて記事にしていくことにしました。

## 0. 開発環境
今回は以下の開発環境で実装しています。
|言語など|バージョン |
|:-----|:--------:|
|PC    | Mac      |
|Rails |  7.0.6   |
|Ruby  |  3.2.1   |
|React |  19.1    |


## 1. devise_auth_tokenとは？
Railsの認証ライブラリであるDeviseを拡張して、トークン認証で動くライブラリです。Deviseはセッションでの認証となりますので、認証方式が異なります。
APIモードやSPA(React/Vue)・モバイルアプリ用のバックエンド向けとなります。

## 2. ユーザー登録実装の流れ
1, フロントエンド側(React)でPOSTリクエストを使って指定のアドレスに必要な登録情報(Email, 電話番号, パスワードなど)を送信します。
2, バックエンド側(Rails)でデータを受け取り、DBにデータを登録します。
3, データ登録後、confirmationメールをサーバー側からクライアント側へ擬似的に送信

## 3. フロントエンド側でデータの送信

```JavaScript:登録データの送信処理
  import axios from "axios";
  
  const handleSignUp = async () => {
    try {
      const response = await axios.post("http://localhost:3000/api/v1/users", {
        phone_number: user.phone_number,
        email: user.email,
        birthday: user.birthday,
        password: user.password,
        password_confirmation: user.password_confirmation,
        confirm_success_url: "http://localhost:5173",
      });
      console.log(response.data);
      setUser(defaultUser);
    } catch (error) {
      console.error(error.response.data.errors);
    }
  };
```
axiosはPromiseベース(非同期)で動くHTTPクライアントです。
要はHTTP通信を行うためのライブラリを意味します。

上のコードではaxiosを使ってHTTP通信のうちPOSTメソッドを実行することにより後に続くパラメーター(emailなど)をバックエンド(Rails)側に送信しています。

## 4. devise_auth_tokenの導入
ここでは実装に必要なgemをインストールします。
`gem 'devise'`
`gem 'devise_token_auth'`
`gem 'rack-cors'`

上記のgemをGemfileに追記したら、実際にプロジェクト内へインストールしていきましょう。
```ruby:ターミナル
bundle install
```
私の場合はDockerで実装していたので、下記のコマンドになります。
```ruby:ターミナル
dokcer compose exec api(サービス名) bundle install
```

次に初期設定ファイルなどを生成していきます。
```go:ターミナル(通常 or Docker版)
rails g devise:install
docker compose exec api(サービス名) rails g devise:install

rails g devise_token_auth:install User users
docker compose exec api(サービス名) rails g devise_token_auth:insall User users
```
２つのコマンドを実行することでdeviseとdevise_token_authの導入に必要な初期設定用のファイルを生成されます。
`rails g devise_token_auth:install User users`のusersの部分はルートパス(エンドポイント)を指定しています。
`/users/sign_in`
`/users/sign_out`
`users`
などのエンドポイントになります。

## 5. Usersテーブルを作成する。
`rails g devise_token_auth:install User auth`を実行して生成されたマイグレーションファイルについて、必要な所があれば修正(null: falseの追記など)。

```ruby:config/db/日付_devise_token_auth_create_users.rb
class DeviseTokenAuthCreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table(:users) do |t|
      ## Required
      t.string :provider, null: false, default: 'email'
      t.string :uid, null: false, default: ''

      ## Database authenticatable
      t.string :encrypted_password, null: false, default: ''

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at
      t.boolean  :allow_password_change, default: false, null: false

      ## Rememberable
      t.datetime :remember_created_at

      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # 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

      ## User Info
      t.string :name, null: false, default: '', unique: true, limit: 50
      t.string :profile, limit: 160
      t.string :location
      t.string :website
      t.string :birthday, null: false
      t.string :email, null: false, unique: true

      ## Tokens
      t.json :tokens

      t.timestamps
    end

    add_index :users, :email, unique: true
    add_index :users, %i[uid provider], unique: true
    add_index :users, :reset_password_token, unique: true
    add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
```
修正が終われば`rails db:migrate`でテーブルを作成する。

## 6. APIコントローラーの作成
次に新規登録用のコントローラーを作成していきます。
```ruby: ターミナル(通常 or Docker)
rails g controller api/v1/registrations
docker compose exec api(サービス名) rails g controller api/v1/registrations
```

ストロングパラメーターで新規登録時に登録できるカラム名を指定します。
コントローラー内にはストロンゴパラメーターしか記述されていませんが、
`DeviseTokenAuth::RegistrationsController`を継承しているため、
ユーザー登録などの処理はdevise_token_authが全て裏で処理しています。
```ruby: registrations_controller.rb
module Api
  module V1
    class RegistrationsController < DeviseTokenAuth::RegistrationsController
      private

      def sign_up_params
        params.require(:registration).permit(:name, :email, :phone_number, :birthday, :password, :password_confirmation)
      end
    end
  end
end
```

## 7. ルーティング設定

```ruby:route.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      mount_devise_token_auth_for 'User', at: 'users', controllers: {
        registrations: 'api/v1/registrations'
      }
    end
  end
end
```
ここではエンドポイントとそにれ対して作動するコントローラーを指定しています。
`mount_devise_token_auth_for 'User', at: 'users'`のところで
`users`の部分でURLのエンドポイントを指定しています。
上記コードの場合、`ap1/v1/users`というパス(エンドポイント)にアクセスが来た時、
devise_token_authのregistrations系のリクエスト(新規登録など)は
app/controllers/api/v1/registrations_constroller.rbを使うように設定しています。
全てのリクエストがregistrations_controller.rbにいくわけではありません。
`api/v1/users`へのPOST(新規登録)などregistrationsに該当するリクエストのみが対象。
前述のフロントエンド側の操作でHTTPメソッドのPOSTでAPI側にアクセスするように実装しています。
フロントエンド側で指定した`http://localhost:3000/api/v1/users`にアクセスすると`routes.rb`に指定されたルーティングによりregistrations_controllerへ導かれます。

## 8. CORSに関する説明と設定
- CORSとは
Cross-Origin Resource Sharingの略で異なるオリジンからのリソース(Webページなど)へのアクセス制限を緩和する仕組みです。
オリジンとはプロトコル、ドメイン、ポート番号を併せたものを指します。

オリジン例：https://qiita.com:80
上記のオリジンで説明しますと、
プロトコル：https
ドメイン：qiita.com
ポート番号：80

言い換えますと、異なるURLからリソースへのアクセスを許可する仕組みとなります。

今回私が作成しているX(旧Twitter)のクローンサイトは下記の構成になっています。
フロントエンド(React)：http://localhost:5173
バックエンド(Rails)　：http://localhost:3000
※オリジン(ほぼURL)が異なる事に着目

ユーザーの新規登録機能はフロントエンド側で入力したデータをバックエンド側にアクセスしてデータベースに保管する作業となります。
異なるオリジン間のリソース共有が発生するので、ここでCORSの出番となります。

以下はCORSの設定です。
また、冒頭でインストールした`rack-cors'`もここで使用することとなります。
```ruby:cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:5173' # 許可するオリジンの指定

    resource '*',
             headers: :any,
             expose: %w[access-token expiry token-type uid client], #追記
             methods: %i[get post put patch delete options head]
  end
end
```
`origins`にフロントエンド側のURLを記述することでアクセスが可能になります。
初期設定では`origins *`に設定されているので、どのオリジンからもアクセスできる設定になっています。指定のオリジンに書き換える事で他のオリジンからのアクセスは不可となり、ホワイトリストの役割を果たします。
追記部分の`expose`があることによってフロントエンド側が認証情報を取得できるようになります。
この記述が無い場合は認証情報が不十分という理由により401エラーが発生する可能性がありますのでご注意ください。

## 9. confirmableを有効にする

新規登録のために必要項目を入力した後に確認メールを登録用メールアドレスに送信する設定です。

```ruby:user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable
  include DeviseTokenAuth::Concerns::User
end
```
モデルの`user.rb`でconfirmableは初期状態ではコメントアウトされているので、deviseの使用機能として追加します。

## 10. letter_opener_webの設定
上記の確認メール設定で送信されたメールが正しく送信されているか、開発環境でチェックするための設定を行います。

- letter_opener_webのインストール
```ruby:Gemfile
group :development do
  gem 'letter_opener_web', '~> 3.0'
end
```
letter_opener_webは開発環境だけで使用するので、開発環境グループに追加してインストールします。

- routesの編集
```ruby:routes.rb
mount LetterOpenerWeb::Engine, at: '/letter_opener' if Rails.env.development?
```
routes.rbに上記コードを追加することで `/letter_opener`のエンドポイントにアクセスした時に開発環境限定でletter_opener_webのアプリケーションを実行できます。

## 11. developmen.rbの設定
letter_opener_webが開発環境下で使えるように`development.rb`に追記します。
```ruby:debelopment.rb
  # 電子メールが配信完了できない場合にエラーを発生させるかの設定
  config.action_mailer.raise_delivery_errors = true
  # フラグメントキャッシュを利用するための設定
  config.action_mailer.perform_caching = false
  # 送信されるメール内のURLのドメインが`localhost`で固定される
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
  # メールの配信方法を設定
  config.action_mailer.delivery_method = :letter_opener_web
  # メール配信を行うかどうかの設定
  config.action_mailer.perform_deliveries = true
```
※フラグメントキャッシュ：Webページの断片(部分テンプレートなど)をキャッシュし、リクエストごとに毎回生成するのではなく、キャッシュされたものを再利用することでパフォーマンスを向上させる機能。

## 12. 参考にしたサイト
[letter_opener_web Github](https://github.com/fgrehm/letter_opener_web)
[letter_opener_web設定](https://qiita.com/h-taro/items/afbc5e019d82aba4f4fd)
[なんとなく CORS がわかる...はもう終わりにする。](https://qiita.com/att55/items/2154a8aad8bf1409db2b)