【Rails】JWTを用いたUser周りのAPI実装

Rails APIでUser周りの実装をしていきます。User周りの実装は基本的なアプリでは必須になってくると思うのでぜひ参考にしてください。
Docker環境上で開発しています。
のちに同じプロジェクト内で管理画面を開発することを考えたため、Rails APIでは開発していません。
駆け出しエンジニアですので間違っているところはコメントください。
## Userテーブル
Userテーブルは以下の設計にしました

| カラム名     | データ型      | 制約                      | 説明                        |
|--------------|---------------|---------------------------|-----------------------------|
| id           | integer       | 主キー、オートインクリメント | ユーザーの一意な識別子      |
| name         | string        | null: false               | ユーザーの名前              |
| email        | string        | null: false, 一意制約       | ユーザーのメールアドレス    |
| password_digest | string     | null: false               | ハッシュ化されたパスワード  |
| created_at   | datetime      | null: false               | レコード作成日時            |
| updated_at   | datetime      | null: false               | レコード更新日時            |

## ルーティング
今回の実装はユーザーの新規登録、ログイン、退会、ログアウトを実装していきます。
| HTTPメソッド | パス                 | コントローラ#アクション | 説明                                 |
|--------------|----------------------|-------------------------|--------------------------------------|
| `POST`       | `/v1/signup`         | `users#create`          | ユーザーの新規登録                   |
| `POST`       | `/v1/login`          | `sessions#create`       | ユーザーのログイン（JWT発行）        |
| `DELETE`     | `/v1/logout`         | `sessions#destroy`      | ユーザーのログアウト                  |
| `DELETE`     | `/v1/profile`        | `users#destroy`         | ユーザーの退会                       |

1. ユーザーの新規登録では、リクエストボディにname、email、passwordを含め、成功すると、登録されたユーザーの情報が返されます
1. ユーザーのログインでは、リクエストボディにemailとpasswordを含め、認証に成功すると、JWTトークンが発行され、今後のリクエストでの認証に使用されます
1. ユーザーのロウアウトでは、現在のセッションを終了し、ユーザーをログアウトさせます。このエンドポイントにアクセスすることで、JWTトークンを無効化し、以後のリクエストに対して認証を行えなくします
1. ユーザーの退会では、ユーザーアカウントを削除します。このエンドポイントを利用することで、ユーザーは自分のアカウントを退会することができます。成功すると、関連するユーザーデータも削除されます

以下がroutsファイルです
```ruby:config/routes.rb
Rails.application.routes.draw do
  namespace :v1 do
    post '/signup', to: 'users#create'
    post '/login', to: 'sessions#create'
    delete '/logout', to: 'sessions#destroy'
    delete '/profile', to: 'users#destroy'
  end
end
```
## 期待するレスポンス
* POST /v1/signup
```json:json
{
    "message": "ユーザー登録が完了しました",
    "user": {
        "id": 1,
        "name": "bar",
        "email": "bar@example.com"
    }
}
```
* POST /v1/login
```json:json
{
    "message": "ログインに成功しました。",
    "token": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0fQ.KzYrVPsdU0i8n1g5qoQRtadIj8CCYTJSbgof5nf-5HM"
}
```
* DELETE /v1/logout
```json:json
{
  "message": "ログアウトしました。。"
}
```
* DELETE /v1/profile
```json:json
{
  "message": "アカウントが削除されました。"
}
```
## Userテーブルの作成
#### マイグレーションの作成
```
docker-compose run web rails generate model User name:string email:string:uniq password_digest:string
```
このコマンドにより、Userテーブルのマイグレーションファイルが生成されます。name、email、password_digestのカラムが追加されます。
#### マイグレーションファイルの修正
生成されたマイグレーションファイルを開き、emailカラムにユニーク制約を明示的に追加して、以下のように修正します。
```ruby:xxxx_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.string :email, null: false, unique: true
      t.string :password_digest, null: false

      t.timestamps
    end
    add_index :users, :email, unique: true
  end
end
```
#### マイグレーションの実行
```
docker-compose run web rails db:migrate
```
これでUserテーブルがデータベースに作成されます。
また、userモデルも作成されます
## bcryptの有効化
* Gemfileにbcryptを追加
まず、プロジェクトのGemfileにbcryptを追加します。bcryptはパスワードのハッシュ化に必要です。
```ruby:Gemfile
gem 'bcrypt', '~> 3.1.7'
```
* bundle installを実行
```
docker-compose run web bundle install 
```
* build
```
docker-compose build 
```
* Userモデルにhas_secure_passwordを追加
```ruby:user.rb
class User < ApplicationRecord
  has_secure_password

  validates :name, presence: true
  validates :email, presence: true, uniqueness: true
end
```
## コントローラの作成
#### Apiコントローラ
APIモードでないRailsアプリケーションで、JSONリクエストを扱うためにapiコントローラでCSRFを防止します。apiで扱われるコントローラはapiコントローラを継承します。
また、ログイン中のユーザーをリクエストヘッダーから取得するためのcurrent_userメソッドを作成します。
```
docker-compose exec web rails generate controller api/v1/Api
```
```ruby:users_controller.rb
module Api
  module V1
    class ApiController < ApplicationController
      protect_from_forgery with: :null_session
      def current_user
        token = request.headers['Authorization']&.split(' ')&.last
        return nil unless token
    
        begin
          decoded_token = JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
          user_id = decoded_token['user_id']
          @current_user ||= User.find(user_id)
        rescue JWT::DecodeError
          nil
        end
      end
    end
  end
end
```
#### Usersコントローラー
ユーザーの新規登録、退会を管理するために、UsersControllerを作成します
```
docker-compose exec web rails generate controller api/v1/Users
```
コントローラーを編集します
```ruby:users_controller.rb
module Api
  module V1
    class UsersController < ApiController
      # ユーザー登録
      def create
        user = User.new(user_params)
        if user.save
          render json: { message: "ユーザー登録が完了しました", user: {id: user.id, name: user.name, email: user.email} }, status: :created
        else
          render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
        end
      end

      # プロフィール削除（退会処理）
      def destroy
        user = current_user
        if user && user.destroy
          render json: { message: "アカウントが削除されました。" }, status: :ok
        else
          render json: { error: "アカウントの削除に失敗しました" }, status: :unprocessable_entity
        end
      end

      private

      # ユーザー登録時のパラメータ
      def user_params
        params.require(:user).permit(:name, :email, :password, :password_confirmation)
      end
      wrap_parameters :user, include: [:name, :email, :password, :password_confirmation]
    end
  end
end
```
#### Sessionsコントローラー
ユーザーのリグイン、ログアウトを管理するために、SessionsControllerを作成します
```
docker-compose exec web rails generate controller api/v1/Sessions
```
コントローラーを編集します
```ruby:sessions_controller.rb
module Api
  module V1
    class SessionsController < ApiController
      # ログイン
      def create
        user = User.find_by(email: params[:email])

        if user&.authenticate(params[:password])
          token = encode_token(user_id: user.id)
          render json: { message: "ログインに成功しました。", token: token }, status: :ok
        else
          render json: { error: "メールアドレスまたはパスワードが無効です。" }, status: :unauthorized
        end
      end

      # ログアウト
      def destroy
        render json: { message: "ログアウトしました。" }, status: :ok
      end

      private

      # JWTトークンをエンコード
      def encode_token(payload)
        JWT.encode(payload, Rails.application.secrets.secret_key_base)
      end
    end
  end
end
```
## JWTの有効化
* Gemfileにjwtを追加
まず、プロジェクトのGemfileにjwtを追加します。
```ruby:Gemfile
gem 'jwt'
```
* bundle installを実行
```
docker-compose run web bundle install 
```
* build
```
docker-compose build 
```
## Postmanを使ってリクエスト
Postmanを使ってそれぞれリクエストを送ってみます。
#### 新規登録
リクエストメソッドをPOSTに設定しURLを以下に設定する
```
http://localhost:3000/v1/signout
```
リクエストボディを以下のように設定して
```json:json
{
  "name": "bar",
  "email": "bar@example.com",
  "password": "password123"
}
```
リクエストを送ると以下のレスポンスが返ってきます。
```json:json
{
    "message": "ユーザー登録が完了しました",
    "user": {
        "id": 1,
        "name": "bar",
        "email": "bar@example.com"
    }
}
```
#### ログイン
リクエストメソッドをPOSTに設定しURLを以下に設定する
```
http://localhost:3000/v1/login
```
リクエストボディを以下のように設定して
```json:json
{
  "email": "bar@example.com",
  "password": "password123"
}
```
リクエストを送ると以下のレスポンスが返ってきます。
tokenはJWTでidをエンコードしたものです。
```json:json
{
    "message": "ログインに成功しました。",
    "token": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo1fQ.7wAvPaPWV1iQxks7yQhrFM5wJO6fDoiRvVbTHU7nxP0"
}
```
#### ログアウト
リクエストメソッドをDELETに設定しURLを以下に設定する
```
http://localhost:3000/v1/logout
```
リクエストボディは設定せずにリクエストを送ると以下のレスポンスが返ってきます。
```json:json
{
    "message": "ログアウトしました。"
}
```
ログアウトではフロント側でsessionnを削除する操作を行います。
#### 退会
リクエストメソッドをDELETに設定しURLを以下に設定する
```
http://localhost:3000/v1/profile
```
リクエストヘッダにkayをAuthorization、valueをログインした時にトークンとして返ってきた値をセットします。
リクエストボディは設定せずに
リクエストを送ると以下のレスポンスが返ってきます。
tokenはJWTでidをエンコードしたものです。
```json:json
{
    "message": "アカウントが削除されました。"
}
```
## まとめ
今回はRails APIでUser周りの実装をしました。ぜひ参考にしていただけたらと思います。
プログラムで気になったことを以下の記事にまとめたのでぜひご覧ください。
[【Rails】Rails APIで新規登録、退会処理](https://qiita.com/kay903347/items/e27b441d2abf4f98721c)
[【Rails】Rails APIでログイン、ログアウト](https://qiita.com/kay903347/items/dda8119e5aaff0370f8e)