👏

devise-token-authを用いたログイン・ログアウト機能の作成②

2023/08/14に公開

ログアウト機能の作成

前回、『devise-token-authを用いたログイン・ログアウト機能の作成①』 という記事を作成しました。
今回解説する記事はその記事の続きで、ここでは、ログアウト機能の実装方法を解説します。

ログアウト機能もこちらの記事を参考に実装しました。

実装方針としては、まず、ログイン後に Cookieへ保存した access-token, client, uid をパラメータとした HTTPヘッダ を作成します。

次に、そのHTTPヘッダ を用いて、ユーザー情報を取得し、ユーザーが持つトークン(User.last.tokens などで取得できる token, client_id, created_at , expiry などの情報)を削除します。

さらに、ログアウト処理が完了したら Cookieの中に保存している access-token, client, uid も削除します。

今回は、上記のような方針で実装してみました。実装結果は下記の通りです。

frontend/src/components/modules/Header.jsx
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { styled, useTheme } from '@mui/material/styles';
import { useNavigate } from 'react-router-dom';
import {
  IconButton,
  Drawer,
  List,
  ListItem,
  ListItemIcon,
  ListItemText,
} from '@mui/material';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import LockOpenIcon from '@mui/icons-material/LockOpen';
import ExitToAppIcon from '@mui/icons-material/ExitToApp';
import { userSignOut } from '../../apis/userSignOut';

const DrawerHeader = styled('div')(({ theme }) => ({
  display: 'flex',
  alignItems: 'center',
  padding: theme.spacing(0, 1),
  ...theme.mixins.toolbar,
  justifyContent: 'flex-start',
}));

const Header = () => {
  const [isSidebarOpen, setIsSidebarOpen] = useState(false);
  const user = useSelector(state => state.user)
  const dispatch = useDispatch()
  const navigate = useNavigate();

  // サイドバーの開閉
  const toggleSidebar = () => {
    setIsSidebarOpen(!isSidebarOpen);
  };

  const handleSignInClick = () => {
    toggleSidebar()
    navigate('/users/sign_in');
  }
  // ログアウト
  const handleSignOutClick = () => {

    // Cookieを取得
    const cookies = document.cookie.split(';');
    const cookieData = cookies.reduce((data, cookie) => {
      const [key, value] = cookie.trim().split('=');
      data[key] = value;
      return data;
    }, {});

    const access_token = cookieData['access-token'] || null;
    const client = cookieData['client'] || null;
    const uid = cookieData['uid'] || null;

    const headers = {
      'access-token': access_token,
      'client': client,
      'uid': uid
    };

    userSignOut(headers, dispatch);
    toggleSidebar();
  };

  const theme = useTheme();
  const handleDrawerClose = () => {
    toggleSidebar()
  };

  return (
    <>
      <Drawer anchor="right" open={isSidebarOpen} onClose={toggleSidebar}>
        <div className='sidebar'>
          <DrawerHeader>
            <IconButton onClick={handleDrawerClose}>
              {theme.direction === 'rtl' ? <ChevronLeftIcon /> : <ChevronRightIcon />}
            </IconButton>
          </DrawerHeader>
          <List>
            {!user?.accessToken &&
            <ListItem button onClick={handleSignInClick}>
              <ListItemIcon>
                <LockOpenIcon />
              </ListItemIcon>
              <ListItemText primary="ログイン" />
            </ListItem>
            }
            {user?.accessToken &&
            <ListItem button onClick={handleSignOutClick}>
              <ListItemIcon>
                <ExitToAppIcon />
              </ListItemIcon>
              <ListItemText primary="ログアウト" />
            </ListItem>
            }
          </List>
        </div>
      </Drawer>
    </>
  );
};

export default Header;

上記の Header component では、ログアウトボタンを作成し、クリックすると handleSignOutClick という関数が実行されます。この関数は、Cookie に保存してある access-token, client, uid を取得し、それらをパラメータにもつHTTPヘッダを作成しています。

そして、作成したHTTPヘッダを用いて userSignOut というログアウト処理をrails API として呼び出す関数を実行しています。

frontend/src/urls/index.js
const DEFAULT_API_LOCALHOST = 'http://localhost:3010/api/v1'

export const logoutIndex = `${DEFAULT_API_LOCALHOST}/auth/sign_out`
frontend/src/apis/userSignOut.js
import axios from 'axios';
import { logoutIndex } from '../urls/index'
import { deleteUserData } from '../reducks/reducers/user';

// ログアウト
export const userSignOut = async(headers, dispatch) => {
  await axios.delete(logoutIndex, { headers: headers })
  .then(response => {
    if (navigator.cookieEnabled)
    {
        document.cookie = 'access-token=;max-age=0;';
        document.cookie = 'client=;max-age=0;';
        document.cookie = 'uid=;max-age=0;';
    }
    dispatch(deleteUserData(response.data))
    window.location.reload();
    alert('ログアウト成功しました。')
  }).catch(error => {
    console.log(error);
  });
};

上記の userSignOut 関数では、react側から送信されたHTTPヘッダを用いて、HTTPリクエストのDELETEを実行し、ログアウト処理の rails APIをたたいています。

また、rails API のログアウト処理が成功したら 、Cookie に保存している access-token, client, uid も削除しています。

config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      mount_devise_token_auth_for 'User', at: 'auth', controllers: {
        sessions: 'api/v1/auth/sessions'
      }
    end
  end
end

上記の routes.rb では、ログイン処理同様、ログアウト処理もルーティングされています。

app/controllers/api/v1/auth/sessions_controller.rb
class Api::V1::Auth::SessionsController < DeviseTokenAuth::SessionsController

  -- 省略 ---

  def destroy
    # 認証情報を含むヘッダーからトークン情報を取得
    client_id = request.headers['client']
    uid = request.headers['uid']
    access_token = request.headers['access-token']

    # トークン情報を使用してユーザーを特定し、トークンを無効化する
    user = User.find_by_uid(uid)
    user.tokens.delete(client_id) if user

    if user&.save
      render json: { message: 'ログアウトしました。' }
    else
      render json: { errors: ['ログアウトに失敗しました。'] }, status: :unprocessable_entity
    end
  end
end

上記の sessions_controller.rb では、destroyアクションを定義し、ログアウト処理を定義しています。

ここでは、raect側から送られてきたHTTPヘッダの中にある client, uid を取得し、取得した uid からその uid を持つユーザー情報を取得しています。

そして、その特定したユーザーが持つトークン情報(User.last.tokens などで取得できる token, client_id, created_at , expiry などの情報)を削除しています。

以上が、devise-token-auth を用いた ログアウト処理の実装方法の説明となります。

Discussion