【RailsAPI】devise_token_auth にてゲストログイン機能を実装したい！

個人開発にて実装した、devise_token_authを使用したゲストログイン機能のmemoになります。

# 課題

- ゲストログイン機能を実装したい

# 前提

- devise_token_auth 導入済

# 1.Rails

## 1 ) ルーティングの実装

今回、ゲストログイン処理は `sessions_controller.rb`に追加しました。

```ruby: config/routes.rb
Rails.application.routes.draw do

  namespace :v1 do
    mount_devise_token_auth_for "User", at: "auth", controllers: {
      registrations: "v1/auth/registrations",
      sessions: "v1/auth/sessions",
      confirmations: "v1/auth/confirmations",
      passwords: "v1/auth/passwords"
    }

  ######################## こちらを追加    ########################

  devise_scope :v1_user do
    post "auth/guest_sign_in", to: "auth/sessions#guest_sign_in"
  end

  ######################## こちらを追加    ########################

end
```

[前回書いた記事](https://qiita.com/fumi238000/items/08c9f50277329fc0f788)で苦戦した部分になります 。
（こちらの方が今回の実装より時間を使っていますw）

## 2 ) ゲストユーザー情報を取得するクラスメソッドを実装

`self.guest` メソッドにて、ゲストユーザーを取得します。

```ruby: app/controllers/v1/auth/sessions_controller.rb
class User < ApplicationRecord
  # ゲストユーザーが存在しない場合、ゲストユーザーを作成
  def self.guest
    find_or_create_by!(email: "guest@example.com") do |user|
      user.password = SecureRandom.urlsafe_base64
      user.name = "ゲストユーザー"
    end
  end

end
```

`find_or_create_by!` を使用して、ゲストユーザー(メールアドレス `guest@example.com` を持つユーザー)が

- 存在する場合は、そのゲストユーザー情報を取得
- 存在しない場合は作成

という挙動になっています。

(これに加えて `guest@example.com` に対して制限(新規作成・削除や編集等)をかける必要があると思いますが、今回は割愛します。)

## 3) ゲストログイン処理を実装する

今回のメインとなるゲストログインになります。

流れとしては以下のイメージです。

*①ゲストユーザー情報を取得 → ②認証情報を作成 → ③認証情報を保存 → ④認証情報を持つゲストログインユーザー情報を返す*

```ruby: app/controllers/v1/auth/sessions_controller.rb
class V1::Auth::SessionsController < DeviseTokenAuth::SessionsController
  # ゲストユーザーでログイン  
  def guest_sign_in
    @resource = User.guest # ①
    @token = @resource.create_token　 # ②
    @resource.save! # ③
    render_create_success # ④
  end
end
```

### ①ゲストユーザー情報を取得

先ほど作成した `self.guest` を使用して、ユーザー情報をインスタンス変数 `@resource` に格納します。

なぜ `@resource` に格納しているのか？ というと、

**devise_token_auth で `@resource` というインスタンス変数にユーザー情報を格納して使用するメソッドがあるためです。(後半にそのコードを記載しています。)**

そのため、今回は `@resource` に格納しております。

### ②ゲストユーザーの認証情報を作成
`@resource` に付与する認証情報を作成します。

実行前と後で挙動を確認すると分かりやすいです。

```bash
# メソッド実行前
> @resource.tokens
=> {}

# メソッド実行
> @resource.create_token
=> #<struct DeviseTokenAuth::TokenFactory::Token
 client="f_zh3AczlwV8MsknLTtBtw",
 token="tU13YCRWvOyW7M-mXcGvUQ",
 token_hash=
  "$2a$04$HRvcYJVeIjUm.IXdUTWoDOwMZ9/4Ktx9.LiLazhNaUCMoZDkpQP26",
 expiry=1645149284>

# メソッド実行後 #=> 認証情報が入っていることが確認できる！
> @resource.tokens
=> {"f_zh3AczlwV8MsknLTtBtw"=>
  {:token=>
    "$2a$04$HRvcYJVeIjUm.IXdUTWoDOwMZ9/4Ktx9.LiLazhNaUCMoZDkpQP26",
   :expiry=>1645149284}}
```

### ③作成した認証情報を保存

上記で作成した認証情報をDBに保存します。

```bash
> @resource.save!
=> true

# 保存前
User.find_by(email: "guest@example.com").tokens
=> {}

# 保存後
User.find_by(email: "guest@example.com").tokens
=> {"Zxy1nUgAIu0VAXw5vpI3KA"=>{"token"=>"$2a$04$opeH9EQuW6j6w2YwbuNAhuJoLSKwtwNRh1vlQNLVyfiylMBo6QL8y", "expiry"=>1645151486}}
```

これで、ゲストユーザーに認証情報を付与することができました。

後は、保存している認証情報を返せばOKです。

### ④render_create_successメソッドでレスポンスを返す

あとは、認証情報を付与されたユーザー情報をレスポンスで返します。

```bash
> render_create_success
=> "{\"data\":{\"id\":3035,\"provider\":\"email\",\"uid\":\"guest@example.com\",\"allow_password_change\":false,\"name\":\"ゲストユーザー\",\"image\":null,\"email\":\"guest@example.com\"}}"
```

### render_create_successってなに？

devise_token_authで定義されており、詳細が以下になります。
(こちらが先ほどの`@resource` を使用するメソッドとなります。)

```ruby
def render_create_success
    render json: {
      data: resource_data(resource_json: @resource.token_validation_response)
    }
end

def resource_data(opts = {})
  response_data = opts[:resource_json] || @resource.as_json
  response_data['type'] = @resource.class.name.parameterize if json_api?
  response_data
end

def token_validation_response
  as_json(except: %i[tokens created_at updated_at])
end
```

## 4 ) テストの実装

後はリクエストを送信した時、期待している情報が返ってくることをテストしておきます。(この辺は設定によって違ってくると思います。

認証情報がheaderに格納されていればOKです。

```ruby: spec/requests/v1/auth/sessions_spec.rb
spec/requests/v1/auth/sessions_spec.rb

    # ゲストログイン
    describe "POST v1/users/auth/guest_sign_in" do
      context "リクエストが送信された時" do
        subject(:request) { post(v1_auth_guest_sign_in_path) }

        it "ゲストログインできる" do
          request
          expect(response).to have_http_status(:ok)
          header = response.header
          expect(header["access-token"]).to be_present
          expect(header["client"]).to be_present
          expect(header["expiry"]).to be_present
          expect(header["uid"]).to be_present
          expect(header["token-type"]).to be_present
        end
      end
    end
```

これでゲストログイン機能は実装できました。


# 2.React

あとは、React側はリクエスト処理を送信し、認証情報を保存することで、ゲストログインが実現できます。

( こちらはサンプルです。参考例として記載しています。)

```jsx
const GuestlogIn = useCallback(() => {
    axios
      .post(http://localhost:5000/v1/auth/guest_sign_in)
      .then((res) => {
      // 成功処理
      // Cookieに認証情報を保存
      // 画面遷移
      toast.success('ゲストログインしました');  // メッセージ
      })
      .catch((error) => {
       // 例外処理
      toast.error('ゲストログインできませんでした'); // メッセージ
      });
  }, []);
  return { GuestlogIn };
};
```

# まとめ

今回はゲストログインを実装することができました。

GitHubを見て、なぜそうなるのか動作確認しながら確認することが重要ですね。

次回はメール認証をやりたいと思います。

# 参考資料
https://github.com/lynndylanhurley/devise_token_auth/blob/master/app/controllers/devise_token_auth/sessions_controller.rb

https://qiita.com/take18k_tech/items/35f9b5883f5be4c6e104