View on GitHub

セッションの取得

Home

セッションの取得

定数の定義

src/frontend/src/constants/styles.ts

/**
 * スタイル関連の定数
 */
export const STYLES = {
  ......
  // ↓追加
  // マイアカウントページ関連
  MY_ACCOUNT_PAGE: `flex min-h-screen items-center justify-center bg-gray-100`,
  MY_ACCOUNT_PAGE_CARD: `w-full max-w-md rounded-lg border border-gray-300 bg-white p-8 shadow-lg`,
  MY_ACCOUNT_PAGE_TITLE: `mb-6 text-center text-2xl font-bold`,
  MY_ACCOUNT_LABEL: `text-sm font-medium`,
  // ↑追加
} as const;

src/frontend/src/constants/messages.ts

......
/**
 * エラーメッセージの定数
 */
export const ERROR_MESSAGES = {
  ......
  // 認証関連
  EMAIL_AND_PASSWORD_REQUIRED: 'メールアドレスとパスワードを入力してください。',
  SIGN_UP_FAILED: 'サインアップに失敗しました。時間をおいて再度お試しください。',
  SIGN_IN_FAILED: 'サインインに失敗しました。メールアドレスとパスワードを確認してください。',
  USER_NOT_AUTHENTICATED: 'ユーザが認証されていません', // <- 追加
} as const;

マイアカウント情報取得 API

src/frontend/src/api/auth/getMyAccount.ts

import { ApiContext, User } from "@/types/data";
import { ERROR_MESSAGES } from "@/constants";
import { fetcher } from "@/utils";

const context: ApiContext = {
  apiRootUrl: process.env.NEXT_PUBLIC_API_BASE_URL,
};

/**
 * アカウント情報取得API
 * @returns ユーザオブジェクト
 */
const getMyAccount = async (): Promise<User> => {
  const access =
    typeof window !== "undefined" ? localStorage.getItem("access") : null;
  if (!access) throw new Error(ERROR_MESSAGES.USER_NOT_AUTHENTICATED);

  const url = `${context.apiRootUrl?.replace(/\/$/g, "")}/accounts/me/`;
  const headers: Record<string, string> = {
    Accept: "application/json",
    Authorization: `Bearer ${access}`,
  };

  return await fetcher(url, {
    method: "GET",
    headers: headers,
    cache: "no-store",
  });
};

export default getMyAccount;

インデックスページ

src/frontend/src/app/components/Index.tsx

"use client";

import { useEffect, useState } from "react";

import { User } from "@/types/data";
import SignInPage from "@/app/signin/page";
import getMyAccount from "@/api/auth/getMyAccount";
import useAuth from "@/hooks/useAuth";

/**
 * インデックスコンポーネント
 *
 * JWT の有効期限を監視し、タイムアウト時にメイン画面をログアウト状態へ切り替える。
 */
const Index = () => {
  const { isAuthenticated, isLoading } = useAuth();
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    const load = async () => {
      try {
        const user = await getMyAccount();
        setUser(user);
        // setStatus('authenticated');
      } catch {
        setUser(null);
        // setStatus('unauthenticated');
      }
    };
    load();
  }, []);

  return (
    <>
      {isLoading ? (
        <div>Loading...</div>
      ) : !isAuthenticated ? (
        <SignInPage />
      ) : (
        <>
          <div>ようこそ {user?.user_email} さん!</div>
        </>
      )}
    </>
  );
};

export default Index;

src/frontend/src/app/page.tsx

import Index from "./components/Index"; // <- 追加

const Page = () => {
  return <Index />; // <- 修正
};

export default Page;

マイアカウントページ

src/frontend/src/app/myaccount/page.tsx

"use client";

import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";

import { User } from "@/types/data";
import { STYLES } from "@/constants";
import getMyAccount from "@/api/auth/getMyAccount";
import useAuth from "@/hooks/useAuth";

const MyAccountPage = () => {
  const router = useRouter();
  const { isAuthenticated, isLoading } = useAuth();
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    if (isLoading || isAuthenticated) return;
    router.replace("/signin");
  }, [isLoading, isAuthenticated, router]);

  useEffect(() => {
    if (!isAuthenticated) return;
    const load = async () => {
      try {
        const user = await getMyAccount();
        setUser(user);
      } catch {
        setUser(null);
      }
    };
    load();
  }, [isAuthenticated]);

  if (isLoading || !isAuthenticated) {
    return <div>Loading...</div>;
  }

  return (
    <>
      <div className={`${STYLES.MY_ACCOUNT_PAGE}`}>
        <div className={`${STYLES.MY_ACCOUNT_PAGE_CARD}`}>
          <h1 className={`${STYLES.MY_ACCOUNT_PAGE_TITLE}`}>マイアカウント</h1>
          <div className={`${STYLES.MY_ACCOUNT_LABEL}`}>
            ユーザID: {user?.user_id}
          </div>
          <div className={`${STYLES.MY_ACCOUNT_LABEL}`}>
            メールアドレス: {user?.user_email}
          </div>
        </div>
      </div>
    </>
  );
};

export default MyAccountPage;

アカウントメニュー

src/frontend/src/app/components/ui/header/AccountMenu.tsx

'use client';

import * as React from 'react';
import Link from 'next/link'; // <- 追加
......
import { AccountCircle } from '@mui/icons-material'; // <- 追加
import { ButtonSignOut } from '../../auth/ButtonSignOut';

export default function AccountMenu() {
  ......
  return (
    <React.Fragment>
      ...(略)...
      <Menu
        ...(略)...
      >
        <MenuItem onClick={handleClose}>
          {/* ↓修正 */}
          <AccountCircle fontSize="small" />
          <Link href="/myaccount/">My Account</Link>
          {/* ↑修正 */}
        </MenuItem>
        <Divider />
        ...(略)...
      </Menu>
    </React.Fragment>
  );
}

実行確認

サインインする前に、ブラウザで下記 URL にアクセスしてください。

自動的にサインインページが表示されることを確認してください。サインイン後は、「Loading…」と表示された後、「ようこそ さん!」と表示されます。

アカウントメニューからMy accountをクリックすると、マイアカウント情報が表示されます。アカウント情報を読み込むまでに「Loading…」と表示されます。

サインアウトした後、下記 URL に直接アクセスすると、サインインページにリダイレクトされます。

サインインした状態で、アクセストークンの有効期限が切れると、自動的にサインアウトされ、サインインページが表示されることを確認してください。簡単にテストするには、Djangoの設定ファイルの下記のACCESS_TOKEN_LIFETIMEを一時的に変更すると良いです。

src/backend/config/settings/base.py

......
SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),  # <- minutes=0.5などに設定する。
    ......
}

参考

  1. Auth.js | Get Session