Viteでauth0を使ってみる ~ その③ react-router-dom の導入とルートの保護

2021-08-24

react-router-domのインストール

  • 後で使うために、2つインストールしておきます。
$ npm i react-router-dom history

フォルダ階層の変更等

ちょっと、これまでのコードを整理しました。色々、変えたので最終的にTwitterログイン後の状態で、次のように表示されるようにしました。

  • 以下変更の詳細です。
  • src\components\LoginButton.jsx 及び src\components\LogoutButton.jsx をフォルダ src\components\auth を新規作成し、その下に移動しました。
  • src\App.jsx を次のように修正します。
ログイン状態の画面遷移
import LoginButton from './components/LoginButton';
import LogoutButton from './components/LogoutButton';

↓

import LoginButton from './components/auth/LoginButton';
import LogoutButton from './components/auth/LogoutButton';
  • src\App.jsx の内容を2つのページに分けることとし、src\components\pages を新規作成し、その下に Home.jsx Profile.jsx を新規作成し、それぞれ次に設定します。
// src\components\pages\Home.jsx
import React from 'react';
import { useAuth0 } from '@auth0/auth0-react';
import LoginButton from '../auth/LoginButton';
import LogoutButton from '../auth/LogoutButton';
import logo from '../../logo.svg';
import '../../App.css';

const Home = () => {
  const { user, isAuthenticated, isLoading } = useAuth0();

  return (
    <div className='App'>
      <header className='App-header'>
        <img src={logo} className='App-logo' alt='logo' />
        <p>Hello Vite + React!</p>
        {isLoading && <div>読み込み中...</div>}
        {!isAuthenticated && <LoginButton />}
        {isAuthenticated && (
          <div>
            <LogoutButton />
            <img src={user.picture} alt={user.name} />
            <h2>{user.name}</h2>
          </div>
        )}
      </header>
    </div>
  );
};

export default Home;
// src\components\pages\Profile.jsx
import React, { useState, useEffect } from 'react';
import { useAuth0 } from '@auth0/auth0-react';
import LoginButton from '../auth/LoginButton';
import LogoutButton from '../auth/LogoutButton';

const Profile = () => {
  const [userMetadata, setUserMetadata] = useState(null);
  const { user, isAuthenticated, isLoading, getAccessTokenSilently } =
    useAuth0();

  useEffect(() => {
    const getUserMetadata = async () => {
      try {
        const accessToken = await getAccessTokenSilently({
          audience: import.meta.env.VITE_AUTH0_AUDIENCE,
          scope: import.meta.env.VITE_AUTH0_SCOPE,
        });

        const userDetailsByIdUrl = `${
          import.meta.env.VITE_AUTH0_AUDIENCE
        }users/${user.sub}`;

        const metadataResponse = await fetch(userDetailsByIdUrl, {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        });

        const { description } = await metadataResponse.json();
        setUserMetadata(description.replaceAll('\n', '  '));
      } catch (e) {
        console.log(e.message);
      }
    };

    getUserMetadata();
  }, [getAccessTokenSilently, user?.sub]);
  return (
    <div className='App'>
      <header className='App-header'>
        <h1>プロフィール</h1>
        {isLoading && <div>読み込み中...</div>}
        {!isAuthenticated && <LoginButton />}
        {isAuthenticated && (
          <div>
            <LogoutButton />
            <img src={user.picture} alt={user.name} />
            <h2>{user.name}</h2>
            <p>{user.email}</p>
            {userMetadata ? <div>{JSON.stringify(userMetadata)}</div> : '※なし'}
          </div>
        )}
      </header>
    </div>
  );
};

export default Profile;
  • ここで元のsrc\App.jsxにあったフッターのメニューは src\components\layout\FooterMenu.jsx に分離して、src\App.jsx に含めるようにしました。
// src\components\layout\FooterMenu.jsx
import React from 'react';
import { Link } from 'react-router-dom';

const FooterMenu = () => {
  return (
    <footer className='App-footer'>
      <p>
        <Link to='/' className='App-link'>
          ホーム
        </Link>
        {' | '}
        <Link to='/profile' className='App-link'>
          プロフィール
        </Link>
        {' | '}
        <a
          className='App-link'
          href='https://ja.reactjs.org'
          target='_blank'
          rel='noopener noreferrer'
        >
          Reactを学ぶ
        </a>
        {' | '}
        <a
          className='App-link'
          href='https://ja.vitejs.dev/guide/'
          target='_blank'
          rel='noopener noreferrer'
        >
          Viteガイド
        </a>
      </p>
    </footer>
  );
};

export default FooterMenu;
  • src\App.jsxsrc\main.jsx は次に変更します。
// src\App.jsx
function App() {
  return (
    <Auth0Provider
      domain={import.meta.env.VITE_AUTH0_DOMAIN}
      clientId={import.meta.env.VITE_AUTH0_CLIENT_ID}
      audience={import.meta.env.VITE_AUTH0_AUDIENCE}
      scope={import.meta.env.VITE_AUTH0_SCOPE}
      redirectUri={window.location.origin}
      onRedirectCallback={onRedirectCallback}
    >
      <Router history={history}>
        <Switch>
          <Route exact path='/' component={Home} />
          <Route exact path='/profile' component={Profile} />
        </Switch>
        <FooterMenu />
      </Router>
    </Auth0Provider>
  );
}

export default App;
// src\main.jsx 
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);
  • src\App.css.App-header をコピーした .App-footer を作り、両者の min-height を変更しました。
.App-header {
  background-color: #282c34;
  min-height: 90vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-footer {
  background-color: #282c34;
  min-height: 10vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

ルートの保護

先程の状態はTwitterログイン状態で、フッターメニューの[プロフィール]をクリックするとURL/profileとなり、/Twitterのプロフィール文が表示され

ログインしていない状況でのprofileページの表示

ます。もしもログインしていない状態で [プロフィール]をクリックすると 次の画面となります。

仮にログインしていない状況ではプロフィールページには遷移できないようにしたいとします。このためには、ログインしていな場合は、 フッターメニューの[プロフィール] を表示しないようにする方法が考えられますが、localhost:3000/profile と入力すれば表示されてしまうので不完全です。

ここでは、それを防ぎ、ログインしていない場合は次の画面となる方法を説明します。

ログインせずにログインが必要なページに遷移しようとしたときの画面
  • フォルダ src\components\route を新規作成し、src\components\route\ProtectedRoute.jsx を次のようにします。
import React from 'react';
import { Route } from 'react-router-dom';
import { withAuthenticationRequired } from '@auth0/auth0-react';

const ProtectedRoute = ({ component, ...args }) => (
  <Route component={withAuthenticationRequired(component)} {...args} />
);

export default ProtectedRoute;

src\App.jsx を次のように変更します。

:
import ProtectedRoute from './components/route/ProtectedRoute';
:
      <Router history={history}>
        <Switch>
          <Route exact path='/' component={Home} />
          <ProtectedRoute exact path='/profile' component={Profile} />  <--- ★RouteからProtectedRouteに変更
        </Switch>
        <FooterMenu />
      </Router>