devise_token_authを使ったログイン・ログアウト機能の実装

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

## 0. 前提条件
この記事ではdevise_token_authで新規登録機能を実装した後にログイン・ログアウト機能を実装しています。実装に必要なgemは既にインストールされた前提で話を進めていきますので、まだインストールなどされていない方は先にインストールをお願いします。
参考記事：[devise_auth_tokenを使った新規登録機能の実装](https://qiita.com/Tohru-f/items/4b17cb6ef823f74b611e)

開発環境

|言語など|バージョン|
|:------|:------:|
|PC     |   Mac  |
|Rails  |   7.0.6|
| Ruby  |  3.2.1 |
|  React|  19.1  |


## 1. 実装の流れ
1, ログイン処理にはEmailとPasswordをAPI側に送信する必要がある
2, ログインが完了するとAPI側でトークンが発行され、headersとして返ってくる
3, 返ってきたheadersの値はログアウトする際に必要となるので、ローカルストレージに保存
4, ログアウト処理を実行する時にローカルストレージで保存したトークン情報を一緒に送信

## 2. ログイン処理の実装
現状でのルーティングは下記の通りとなっております。
```Ruby:config/routes.rb
  namespace :api do
    namespace :v1 do
      mount_devise_token_auth_for 'User', at: 'users', controllers: {
        registrations: 'api/v1/registrations'
      }
      resources :sessions, only: %i[index]
    end
  end
```
ログイン処理を実行するにはフロントエンド側から下記のroutesにHTTP通信でアクセスする必要があります。
![sign_in機能のroutes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/12a870cb-2264-4676-b1fc-86d62ea158a3.png)
HTTP通信がPOSTメソッドで指定されています。
これはデータベースに新しい情報を作成するからではありません。
EmailとPasswordを使ってログインを実行した時に新しいトークンが発行されるからです。

以下、API(バックエンド)側での設定です。

```Ruby:config/initializers/devise_token_auth.rb
  # コメントアウトを外してfalseに設定する
　config.change_headers_on_each_request = false

  # コメントアウトを外す
　config.token_lifespan = 2.weeks

  # コメントアウトを外す
  config.headers_names = {
    'access-token': 'access-token',
    client: 'client',
    expiry: 'expiry',
    uid: 'uid',
    'token-type': 'token-type'
  }
```
`config.change_headers_on_each_request`はリクエスト毎にtokenを新しくするかどうかの設定となります。今回は毎回更新する必要は無いので、falseに設定しました。
`config.token_lifespan`はトークンの有効期間です。今回はデフォルトの期間で進めます。
`config.headers_names`は認証用ヘッダーの各要素の名前の定義です。特に変更の必要はないと思います。

```Ruby:config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:5173'

    resource '*',
             headers: :any,
             # exposeの行を追記
             expose: %w[access-token expiry token-type uid client],
             methods: %i[get post put patch delete options head]
  end
end
```
デフォルト時から`expose`の部分を追記しています。
フロントエンド側で受け取りたい内容を追記することでheadersの中に認証情報として返ってくるようになります。
この記述が無い場合は認証情報が不十分という理由でエラーが発生する可能性があります。

以下はフロントエンド側での実装です。
```JavaScript:LoginModal.jsx
  let defaultUser = {
    email: "",
    password: "",
  };

  const [user, setUser] = useState(defaultUser);

  const handleUserChange = (e) => {
    const { name, value } = e.target;

    setUser({
      ...user,
      [name]: value,
    });
  };

  const navigate = useNavigate();

  const handleSignIn = async () => {
    try {
      // ログイン処理として指定のURLにPOSTメソッドでemailとpasswordを送信
      const response = await axios.post("/users/sign_in", {
        email: user.email,
        password: user.password,
      });
      console.log(response.data);
      console.log(response.headers);
      setUser(defaultUser);
      if (response.status === 200) {
        console.log("Signed in successfully!");
        localStorage.setItem("access-token", response.headers["access-token"]);
        localStorage.setItem("client", response.headers.client);
        localStorage.setItem("uid", response.headers.uid);
        navigate("/main");
      }
    } catch (error) {
      if (error.response) {
        console.error(error.response.data.errors);
      } else {
        console.error(error.message);
      }
    }
  };
```
```JavaScript:AxiosBaseService.jsx
import axios from "axios";

// baseURLを設定してコンポーネント内でのURLの記述を簡略化
const AxiosBaseService = (axios.defaults.baseURL =
  "http://localhost:3000/api/v1");

export default AxiosBaseService;
```
axiosを使ってHTTPメソッドのPOSTを使用し、指定のURLにemailとpasswordを送信します。
ログインが成功すればheadersとして、`access-token`, `client`, `uid`といった認証情報が返却されてきます。

## 3. ローカルストレージへheadersに含まれるトークン情報を保存する

```JavaScript:LoginModal.jsx
  const handleSignIn = async () => {
    try {
      const response = await axios.post("/users/sign_in", {
        email: user.email,
        password: user.password,
      });
      console.log(response.data);
      console.log(response.headers);
      setUser(defaultUser);
      // ログインが成功してStatus200が返って来ればlocalStorageに認証情報を保存
      if (response.status === 200) {
        console.log("Signed in successfully!");
        localStorage.setItem("access-token", response.headers["access-token"]);
        localStorage.setItem("client", response.headers.client);
        localStorage.setItem("uid", response.headers.uid);
        navigate("/main");
      }
    } catch (error) {
      if (error.response) {
        console.error(error.response.data.errors);
      } else {
        console.error(error.message);
      }
    }
  };
```
ログインが成功してheadersから認証情報を色できた場合、後に行うリクエスト(ログアウト等)を実行する際に必要となるので、ローカルストレージに保管して必要な時に参照できるようしておきます。
`access-token`だけブラケット記法で`response.headers["access-token"]`と記述したのは`response.headers.access-token`と記述した場合、使用しているPrettier - Code formatterのExtensionsにフォーマットされて`response.headers.access - token`とハイフンの前後に半角スペースを補完されてしまうためです。

## 4. ログアウト処理の実装
ログアウト処理を実行するにはフロントエンド側から下記のroutesにHTTP通信でアクセスします。今回のHTTPメソッドはDELETEです。

![sign outのroutes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/dec0f61a-7d77-4a7c-8ccc-1416b76be208.png)

```JavaScript:SginOutIcon.jsx
export const SignOutIcon = () => {
  const access_token = localStorage.getItem("access-token");
  const client = localStorage.getItem("client");
  const uid = localStorage.getItem("uid");

  const navigate = useNavigate();

  const handleSignOut = async () => {
    try {
      // ローカルストレージに保管している認証情報を使ってAPI側にアクセスする
      const response = await axios.delete("/users/sign_out", {
        headers: {
          "access-token": access_token,
          client: client,
          uid: uid,
        },
      });
      console.log(response.data);
      // ログアウト処理が成功してStatus200が判明したらローカルストレージの認証情報を削除する
      if (response.status === 200) {
        console.log("Signed out successfully!");
        localStorage.clear();
        navigate("/");
      }
    } catch (error) {
      if (error.response) {
        console.error(error.response.data.errors);
      } else {
        console.error(error.message);
      }
    }
  };

  return (
    <Button onClick={handleSignOut}>
      <Span>サインアウト</Span>
    </Button>
  );
};
```
ログアウト処理ではログインの時に取得した認証情報が必要となるので、必要となる`access-token`, `client`, `uid` をローカルストレージから呼び出して変数に保管しておきます。
axiosを使いHTTPメソッドのDELETEで指定のURLに認証情報を送ります。
ログアウトが成功した場合、認証情報は必要なくなるので次のログインに備えてローカルストレージの認証情報は削除します。

以上でログインとログアウトの機能実装が終わりました。
最後まで読んで頂きありがとうございます。

## 5. 参考にさせて頂いたサイトなど
[RailsでCORSのAccess-Control-Expose-Headersを設定する](https://qiita.com/cyperaceae/items/a6e5ec7f0cdf89365e8c)
[devise_token_authで認証機能を作成する](https://kumaskun.hatenablog.com/entry/2023/08/10/002230)
[ポートフォリオ作成で超有益!　Ruby on Railsのログイン機能作成時に使うdevise_token_authの使い方](https://qiita.com/Hashimoto-Noriaki/items/4dbad98f2dcc009fa609)
[devise_token_authの設定について](https://zenn.dev/rrr068/articles/a55348969b8148)
[devise-token-authを用いたログイン・ログアウト機能の作成②](https://zenn.dev/kingdom0927/articles/0da51319c52738)