セッションの取得
定数の定義
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などに設定する。
...(略)...
}