View on GitHub

認証状態の判定

Home

認証状態の判定

フック

src/frontend/src/hooks/useAuth.ts

"use client";

import { useEffect, useState } from "react";

/**
 * JWT の有効期限を監視し、認証状態を返すカスタムフック。
 *
 * - ローカルストレージの access トークンを読み取り、exp クレームを検証する。
 * - 有効期限が切れたタイミングで自動的にトークンを削除し、未認証状態へ遷移する。
 * - isLoading が true の間は判定中なので、UI はローディング表示にすること。
 */
const useAuth = () => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    if (typeof window === "undefined") return;

    const access = localStorage.getItem("access");
    if (!access) {
      setIsAuthenticated(false);
      setIsLoading(false);
      return;
    }

    const clearAuth = () => {
      localStorage.removeItem("access");
      localStorage.removeItem("refresh");
      setIsAuthenticated(false);
    };

    let timer: ReturnType<typeof setTimeout> | undefined;
    try {
      const payload = JSON.parse(atob(access.split(".")[1]));
      const msUntilExpiry = payload.exp * 1000 - Date.now();
      if (msUntilExpiry <= 0) {
        clearAuth();
        setIsLoading(false);
        return;
      }
      setIsAuthenticated(true);
      setIsLoading(false);
      // 有効期限ちょうどに未認証状態へ切り替える
      timer = setTimeout(clearAuth, msUntilExpiry);
    } catch {
      clearAuth();
      setIsLoading(false);
    }

    return () => clearTimeout(timer);
  }, []);

  return { isAuthenticated, isLoading };
};

export default useAuth;

ヘッダ

src/frontend/src/app/components/Header.tsx

"use client"; // <- 追加

import Link from "next/link";

import { STYLES, SETTINGS } from "@/constants";
import useAuth from "@/hooks/useAuth"; // <- 追加

import AccountMenu from "./ui/header/AccountMenu";
import { ButtonSignUp } from "./auth/ButtonSignUp";
import { ButtonSignIn } from "./auth/ButtonSignIn";

const Header = () => {
  // ↓追加
  // JWT の有効期限を監視し、タイムアウト時に自動で未認証状態へ切り替える
  const { isAuthenticated } = useAuth();
  // ↑追加

  return (
    <header className={`${STYLES.HEADER}`}>
      <div>
        <h1 className={`${STYLES.HEADER_APP_NAME}`}>
          <Link href="/">{SETTINGS.APP_NAME}</Link>
        </h1>
      </div>
      <div>
        <nav className={`${STYLES.HEADER_MENU}`}>
          <Link href="/about/">About</Link>
          {/* ↓修正 */}
          {!isAuthenticated ? (
            <>
              <ButtonSignUp />
              <ButtonSignIn />
            </>
          ) : (
            <AccountMenu />
          )}
          {/* ↑修正 */}
        </nav>
      </div>
    </header>
  );
};

export default Header;

バックエンドサーバを起動した状態で、ブラウザで下記 URL にアクセスしてください。

ヘッダのSign Inからサインインすると、Sign Upボタン、Sign Inボタンが消え、右上にアカウントメニューが表示されます。また、アカウントメニューから、Sign Outをクリックすると、ヘッダ右上がSign Upボタン、Sign Inに戻ります。