diff --git a/README.md b/README.md index 370fc46..e12c896 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,42 @@ # markdown-portal -本リポジトリは、Markdown ドキュメントの作成・編集・公開を行うための **フロントエンド + バックエンド** プロジェクトです。 -- **フロントエンド**: Vite + React をベースに、Markdownエディタや認証機能、UI等を提供 -- **バックエンド**: AWS Lambda (Node.js) + API Gateway + DynamoDB のサーバーレス構成 +本リポジトリは、Markdown ドキュメントの作成・編集・公開を行うための **フロントエンド + バックエンド** プロジェクトです。 ---- +- **フロントエンド**: Vite + React をベースに、Markdownエディタや認証機能、UI等を提供 +- **バックエンド**: AWS Lambda (Node.js) + API Gateway + DynamoDB のサーバーレス構成 -## 1. 主な機能と特徴 +## 主な特徴 -1. **Markdown ドキュメントの作成・編集・公開** - - React (フロントエンド) による WYSIWYG ベースの Markdown エディタを提供 - - 作成した Markdown は DynamoDB に保存され、必要に応じて「公開」(任意URLで誰でも閲覧可) に設定できます。 +- **Markdown ドキュメントの作成・編集・公開** + React (フロントエンド) による WYSIWYG ベースの Markdown エディタを提供。 + 作成した Markdown は DynamoDB に保存し、必要に応じて「公開」(URL で誰でも閲覧) に設定できます。 -2. **ユーザー認証と権限制御** - - ログインしていないユーザーには編集権限を与えず、ログイン済みユーザーが自分のドキュメントを管理可能。 - - Amazon Cognito (User Pool) と JWT (IDトークン) を利用し、バックエンド側で検証・制御。 - - ローカル開発時は「モック認証」(userId固定) で手軽に動作確認ができます。 +- **ユーザー認証と権限制御** + ログインしていないユーザーには編集権限を与えず、ログイン済みユーザーが自分のドキュメントを管理可能。 + Amazon Cognito (User Pool) と JWT (IDトークン) を利用し、バックエンド側で検証・制御。 + ローカル開発時は「モック認証」(userId固定) で手軽に動作確認が可能です。 -3. **サーバーレス構成** - - バックエンドは AWS Lambda + API Gateway + DynamoDB + (Cognito)。 - - Serverless Framework を用いたデプロイフローを想定し、S3/CloudFront や Amplify Hosting 上でフロントエンドを提供できます。 +- **サーバーレス構成** + AWS Lambda + API Gateway + DynamoDB + Cognito。 + Serverless Framework を用いたデプロイフローを想定し、S3/CloudFront や Amplify Hosting 上でフロントエンドを提供できます。 -4. **ローカル開発が容易** - - Docker 上の DynamoDB Local + serverless-offline + モック認証で完結するため、AWSリソースを消費せずに開発・テストが可能です。 +- **ローカル開発が容易** + Docker 上の DynamoDB Local + serverless-offline + モック認証で完結するため、AWSリソースを消費せず開発・テスト可能です。 --- -## 2. セットアップ手順 +## クイックスタート: ローカル環境 -### 2.1 ローカル環境(オフラインモード) +### 前提 -#### (1) 前提 - Node.js (推奨: v18 以降) - Docker (推奨: Docker Desktop 最新) → `amazon/dynamodb-local` イメージを使用 -#### (2) フロントエンドの起動 +### フロントエンドの起動 (オフラインモード) ```bash +# リポジトリをクローン後: cd markdown-portal/frontend npm install npm run dev:offline @@ -46,36 +45,33 @@ npm run dev:offline - `VITE_API_STAGE=local` が指定され、モック認証が有効になります。 - ブラウザで `http://localhost:5173` にアクセスするとアプリを確認できます。 -#### (3) バックエンドの起動 (Serverless Offline + DynamoDB Local) +### バックエンドの起動 (Serverless Offline + DynamoDB Local) -1. **DynamoDB Local** を起動: +1. **DynamoDB Local** を起動 ```bash docker run -p 8888:8000 amazon/dynamodb-local ``` -2. **依存パッケージのインストール & ビルド**: +2. **Serverless Offline 起動** +ローカル環境に DynamoDB テーブルを作成し、サンプルデータを投入し、APIを起動します。 ```bash cd markdown-portal/backend npm install - npm run build + npm run dev ``` -3. **テーブル作成 + サンプルデータ投入**: +3. **テーブル作成 + サンプルデータ投入** +npm run devコマンドに含まれていますが、個別に実行する場合は以下のコマンドを実行してください。 ```bash npm run create-local-tables ``` - - `dist/scripts/createLocalTables.js` により DynamoDB テーブルが作成されます。 -4. **Serverless Offline 起動**: - ```bash - npm run start:offline - ``` - - `serverless offline --stage local` により、`http://localhost:3000/local/api/...` でAPIが利用可能です。 + - `dist/scripts/createLocalTables.js` により DynamoDB テーブルが作成されます -#### (4) 動作確認 +### 動作確認 ```bash # テーブル一覧を確認 aws dynamodb list-tables --endpoint-url http://localhost:8888 -# ドキュメント一覧を取得 (GET) +# ドキュメント一覧を取得 (GET): 未認証状態のため、空の配列が返ります curl http://localhost:3000/local/api/docs ``` @@ -83,14 +79,52 @@ curl http://localhost:3000/local/api/docs --- -### 2.2 開発ステージ (dev) へのデプロイ +## AWS 設定 + +### Cognito の設定 + +このプロジェクトを AWS 上で運用する場合は、**Cognito ユーザープール** と **アプリクライアント** を作成し、以下を行ってください。 + +1. **ユーザープール作成** + - `username` として「メールアドレス」を使う設定でも、別々のユーザー名・メールアドレス設定でも構いません。 + - 「メールアドレス検証が必要」な設定の場合、新メールアドレスへの確認コード送信フローが有効になります。 + +2. **アプリクライアント (マネージドログイン画面) 設定** + - コールバックURL(SignIn URL, SignOut URL)をフロントエンドホスト先に合わせて設定 + - クライアントID・ドメインプレフィックス等を `.env` ファイルに記述し、フロントエンドの `amplifyConfigure.ts` などで読み込む + +#### ユーザープール > サインアップ 例 + +![signup-setting-example](https://github.com/user-attachments/assets/772ca7c1-3001-4e3e-b736-817de64de96e) + +#### マネージドログイン画面設定 例 + +![hosted-ui-example](https://github.com/user-attachments/assets/a34e2bfc-741f-4b88-817f-dc519f95658d) + +※ 具体的な設定画面は AWS コンソールのバージョンによって異なる場合があります。 + +--- + +### DynamoDB テーブル設計 + +#### バックアップの設定 +Issue #38 にて、DynamoDB テーブルのバックアップ設定について証跡等保存してます。 +当アプリでは、PITRを有効にし、データを保護しています。 +![](https://github.com/user-attachments/assets/15698408-b973-40dc-9fa0-1eb2a56a6078) + +## GitのSecrets管理 +Secretsの一覧画像は以下のとおりです。 +![](https://github.com/user-attachments/assets/0fdb1c27-fd3e-483a-85eb-d977ab34c251) -#### (1) 前提 -- AWS アカウントに対して DynamoDB / Lambda / API Gateway 等のIAM権限を所持 +## デプロイ: 開発ステージ (dev) + +### 前提 + +- AWS アカウントに対して DynamoDB / Lambda / API Gateway 等の IAM 権限を所持 - `serverless` CLI (グローバルインストール推奨) -- `.env.dev` 等のステージ別設定を用意 +- `.env.dev` 等のステージ別設定を用意 (Cognito, API Gateway のエンドポイントなど) -#### (2) フロントエンド (dev) のビルド & デプロイ +### フロントエンド (dev) のビルド & デプロイ ```bash cd markdown-portal/frontend @@ -101,9 +135,9 @@ npm run build # --mode develop等を使う場合も可 aws s3 sync dist s3:// --delete ``` -- CloudFront などを使う場合は、Invalidation 等が別途必要です。 +- CloudFront などを使う場合は、Invalidation 等の作業が必要です。 -#### (3) バックエンド (dev) デプロイ +### バックエンド (dev) デプロイ ```bash cd markdown-portal/backend @@ -111,113 +145,118 @@ npm install npx serverless deploy --stage dev ``` -- デプロイ成功後に出力される API Endpoint を、フロントエンド `.env.dev` 等で設定します。 +- デプロイ成功後に出力される API Endpoint を、フロントエンド `.env.dev` 等で設定し直してください。 +- フロントエンドからのリクエスト先が正しく `/dev/api` を指すようにします。 --- -### 2.3 本番ステージ (prod) へのデプロイ +## デプロイ: 本番ステージ (prod) + +### フロントエンド -#### (1) フロントエンド ```bash cd markdown-portal/frontend npm install npm run build:prod aws s3 sync dist s3:// --delete -# または Amplify Hosting, CloudFront等でホスティング +# または Amplify Hosting, CloudFront 等でホスティング ``` -#### (2) バックエンド +### バックエンド + ```bash cd markdown-portal/backend npx serverless deploy --stage prod ``` -- デプロイ後に本番用の API Endpoint が有効となり、フロントエンドから利用可能です。 +- デプロイ後に出力される API Endpoint をフロントエンドに設定してください。 --- -## 3. テスト方法 +## テスト方法 -### 3.1 フロントエンドテスト +### フロントエンドテスト -- React Testing Library + Vitest / Jest 等を使用 - ```bash - cd markdown-portal/frontend - npm run test - ``` +React Testing Library + Vitest / Jest 等を使用 +```bash +cd markdown-portal/frontend +npm run test +``` -### 3.2 バックエンドテスト +### バックエンドテスト -- Jest によるユニット/統合テスト - ```bash - cd markdown-portal/backend - npm run test - ``` -- Serverless Offline 環境で `supertest` や `curl` などを使い、実際にAPIを呼ぶテストも可能です。 +Jest によるユニット/統合テストを実装済み +```bash +cd markdown-portal/backend +npm run test +``` +- serverless-offline 環境で `supertest` や `curl` を使い、実際に API を呼ぶテストも可能です。 --- -## 4. 環境変数 (.env) について +## 環境変数 (.env) について フロントエンド側では `VITE_` プレフィックス付き変数が中心です: -- **VITE_API_STAGE** - - `local` → ローカル開発モード (例: `http://localhost:3000/local/api`) - - `dev` → 開発ステージ (API Gatewayの `/dev/api`) - - `prod` → 本番ステージ (API Gatewayの `/prod/api`) +- **VITE_API_STAGE** + `local` → ローカル開発モード (`http://localhost:3000/local/api`) + `dev` → 開発ステージ (API Gatewayの `/dev/api`) + `prod` → 本番ステージ (API Gatewayの `/prod/api`) + +- **REACT_APP_USE_MOCK_AUTH** + `"true"` の場合、オフライン用のモック認証が有効になり、ローカル環境でログイン状態を再現できます。 -- **REACT_APP_USE_MOCK_AUTH** - - `"true"` の場合、オフライン用のモック認証が有効となり、ローカル環境で手軽にログイン状態を再現できます。 +- **VITE_COGNITO_DOMAIN / VITE_COGNITO_CLIENT_ID / VITE_COGNITO_USER_POOL_ID / VITE_COGNITO_REGION** + Cognito User Pool 関連の設定 (ドメイン, クライアントID, ユーザープールID, リージョン等) -- **VITE_COGNITO_DOMAIN / VITE_COGNITO_CLIENT_ID / VITE_COGNITO_USER_POOL_ID / VITE_COGNITO_REGION** - - Cognito User Pool 関連の設定 (ドメイン, クライアントID, ユーザープールID, リージョン等) +- **VITE_SIGNIN_URL / VITE_SIGNOUT_URL** + Cognito 認証後のリダイレクト先URL (サインイン / サインアウト時) -- **VITE_SIGNIN_URL / VITE_SIGNOUT_URL** - - Cognito 認証後のリダイレクト先URL (サインイン / サインアウト時) +その他バックエンド用にも、`serverless.yml` や `.env.dev`, `.env.prod` などを併用してください。 --- -## 5. 開発フロー上の注意点 +## 開発フロー上の注意点 -1. **Gitブランチとステージ** - - feature ブランチ → ローカルDynamoDB で動作確認 - - develop ブランチ → devステージ (AWS) へ随時デプロイ - - main ブランチ → prodステージ (AWS) へ本番デプロイ +- **Gitブランチとステージ** + - feature ブランチ → ローカル DynamoDB で動作確認 + - develop ブランチ → devステージ (AWS) へ随時デプロイ + - main ブランチ → prodステージ (AWS) へ本番デプロイ -2. **デプロイ時の確認事項** - - `serverless.yml` の `stage` / `DYNAMO_TABLE_NAME` や `.env.*` の設定 - - Cognito リソース (User Pool ID / Client ID) が正しいか - - Amplify / S3 / CloudFront の設定見直し +- **デプロイ時のチェックリスト** + - `serverless.yml` の `stage` / `DYNAMO_TABLE_NAME` や `.env.*` の設定 + - Cognito リソース (User Pool ID / Client ID) が正しいか + - Amplify / S3 / CloudFront の設定見直し -3. **セキュリティ** - - JWT トークン検証を必ずバックエンドで実行し、所有者以外はドキュメントを操作できないよう制御 - - 公開しないドキュメント (`isPublic=false`) は認証必須として扱う +- **セキュリティ** + - JWT トークン検証をバックエンドで行い、所有者以外はドキュメント操作を不可に + - `isPublic=false` のドキュメントは認証必須にする --- -## 6. 個人情報の取扱いポリシー +## 個人情報の取扱いポリシー 当プロジェクトでは、以下の個人情報を収集・保持する場合があります。 -- **ユーザーID**: 内部的なユーザー識別・権限管理 +- **ユーザーID**: 内部的なユーザー識別・権限管理用 - **メールアドレス**: ログイン通知やパスワードリセットなどで必要な連絡手段 -### 6.1 個人情報の削除 +### 個人情報の削除 - **6カ月以上ログインがない場合** - 管理者は、データ保持ポリシーに基づき、6カ月以上ログイン実績のないアカウントを **事前通知なし** で削除できるものとします。 - - 削除には、ユーザーID・メールアドレス・作成したドキュメントを含む関連データがすべて含まれます。 + 管理者は、データ保持ポリシーに基づき、6カ月以上ログインのないアカウントを **事前通知なし** で削除する場合があります。 + ユーザーID・メールアドレス・作成ドキュメントを含む関連データが削除対象です。 -### 6.2 問い合わせ窓口 +### 問い合わせ窓口 -個人情報の取扱いや削除ポリシーに関して、疑問・要望などありましたら、リポジトリの Issue もしくは管理者宛にご連絡ください。 +個人情報の取扱いや削除ポリシーに関する疑問・要望がある場合は、リポジトリの Issue もしくは管理者宛にご連絡ください。 --- -## 7. まとめ +## まとめ -- **ローカル開発**: Docker上の DynamoDB Local + serverless-offline + モック認証を利用し、素早い開発・テストが可能 -- **本番運用**: Cognito + DynamoDB + Lambda + API Gateway + (S3/CloudFront / Amplify) を組み合わせたサーバーレス構成 -- **デプロイフロー**: Git ブランチを (local → dev → prod) の各ステージと対応させ、CI/CD を構築して運用 +- **ローカル開発**: Docker 上の DynamoDB Local + serverless-offline + モック認証で高速に開発 +- **本番運用**: Cognito + DynamoDB + Lambda + API Gateway + (S3/CloudFront / Amplify) などでサーバーレス運用 +- **デプロイフロー**: Git ブランチ (local → dev → prod) に応じて CI/CD などを構築可能 -以上が当プロジェクトの概要と環境別の立ち上げ手順です。ご質問や不明点などあれば、Issue や Pull Request を通じてお寄せください。 \ No newline at end of file +ご質問や不明点があれば、Issue や Pull Request を通じてお気軽にお寄せください。 \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1cab667..c2bc835 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,6 +12,7 @@ import "./App.css"; // 必要に応じて、追加CSSをApp.cssなどに追記 import footerStyles from "./styles/Footer.module.scss"; import TermsOfUsePage from "./pages/TermsOfUsePage"; +import AccountInfoPage from "./pages/AccountInfoPage"; const App: React.FC = () => { return ( @@ -22,10 +23,7 @@ const App: React.FC = () => { }; const MainRouter: React.FC = () => { - const { isSignedIn, user, login, logout } = useAuthContext(); - - //user.usernameの最初の5文字を取得する - const accountDisplay = user ? user.userId.slice(0, 7) : null; + const { isSignedIn, login, logout, displayName } = useAuthContext(); return ( @@ -39,8 +37,10 @@ const MainRouter: React.FC = () => {
{isSignedIn ? ( <> - {/* メールアドレスを右寄せ表示 */} - {isSignedIn ? accountDisplay : ""} + {/* displayName を押すと /account へ移動するように */} + + {displayName} + @@ -61,6 +61,7 @@ const MainRouter: React.FC = () => { }/> }/> } /> + } /> diff --git a/frontend/src/__tests__/context/AuthContext.test.tsx b/frontend/src/__tests__/context/AuthContext.test.tsx index 1d3d42c..ac32ade 100644 --- a/frontend/src/__tests__/context/AuthContext.test.tsx +++ b/frontend/src/__tests__/context/AuthContext.test.tsx @@ -6,13 +6,13 @@ import { MockAuthProvider, LOCAL_USER_ID, LOCAL_USER_EMAIL } from '../../context import { useAuthContext } from '../../context/AuthContext.mock' function TestComponent(): JSX.Element { - const { user, isSignedIn, userEmail, login, logout } = useAuthContext(); + const { user, isSignedIn, displayName, login, logout } = useAuthContext(); return (

{user?.userId || 'no-user'}

{isSignedIn ? 'true' : 'false'}

-

{userEmail || 'no-email'}

+

{displayName || 'no-email'}

@@ -29,7 +29,7 @@ describe('AuthContext.mock', () => { // 初期値 expect(screen.getByTestId('userId').textContent).toBe('no-user') expect(screen.getByTestId('isSignedIn').textContent).toBe('false') - expect(screen.getByTestId('userEmail').textContent).toBe('no-email') + expect(screen.getByTestId('displayName').textContent).toBe('no-email') // ログイン操作 userEvent.click(screen.getByText('login')) @@ -38,7 +38,7 @@ describe('AuthContext.mock', () => { await waitFor(() => { expect(screen.getByTestId('userId').textContent).toBe(LOCAL_USER_ID) expect(screen.getByTestId('isSignedIn').textContent).toBe('true') - expect(screen.getByTestId('userEmail').textContent).toBe(LOCAL_USER_EMAIL) + expect(screen.getByTestId('displayName').textContent).toBe(LOCAL_USER_EMAIL) }) }) }) diff --git a/frontend/src/config/amplifyConfigure.ts b/frontend/src/config/amplifyConfigure.ts index 3970a50..6e973ae 100644 --- a/frontend/src/config/amplifyConfigure.ts +++ b/frontend/src/config/amplifyConfigure.ts @@ -15,12 +15,18 @@ export const config: ResourcesConfig = { loginWith: { oauth: { domain: `${COGNITO_DOMAIN_PREFIX}.auth.${COGNITO_REGION}.amazoncognito.com`, - scopes: ["openid"], + scopes: [ + "openid", + "aws.cognito.signin.user.admin", + ], redirectSignIn: [SIGNIN_URL], redirectSignOut: [SIGNOUT_URL], responseType: "code", }, }, + userAttributes: { + email: {required: true}, + } }, }, }; diff --git a/frontend/src/context/AmplifyAuthProvider.tsx b/frontend/src/context/AmplifyAuthProvider.tsx index 3f050e0..77b885e 100644 --- a/frontend/src/context/AmplifyAuthProvider.tsx +++ b/frontend/src/context/AmplifyAuthProvider.tsx @@ -1,6 +1,6 @@ // AmplifyAuthProvider.tsx import React, { useEffect, useState } from 'react'; -import { AuthUser, getCurrentUser, signInWithRedirect, signOut } from 'aws-amplify/auth'; +import { FetchUserAttributesOutput, fetchUserAttributes , AuthUser, getCurrentUser, signInWithRedirect, signOut } from 'aws-amplify/auth'; import { Hub } from 'aws-amplify/utils'; import { AuthEvents } from '../config/projectVars'; import { AuthContext } from './authContextCore'; @@ -8,13 +8,16 @@ import { AuthContext } from './authContextCore'; export const AmplifyAuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [isSignedIn, setIsSignedIn] = useState(false); const [user, setUser] = useState(null); + const [displayName, setDisplayName] = useState(undefined); // ❶ 現在のユーザーを取得する共通関数 const fetchCurrentUser = async () => { try { const currentUser = await getCurrentUser(); + const userAttributes: FetchUserAttributesOutput = await fetchUserAttributes(); setIsSignedIn(true); setUser(currentUser); + setDisplayName(userAttributes?.email || "匿名ユーザー"); } catch { setIsSignedIn(false); setUser(null); @@ -66,8 +69,10 @@ export const AmplifyAuthProvider: React.FC<{ children: React.ReactNode }> = ({ c value={{ user, isSignedIn, + displayName, login, logout, + reFetchDisplayName: fetchCurrentUser, }} > {children} diff --git a/frontend/src/context/AuthContext.mock.tsx b/frontend/src/context/AuthContext.mock.tsx index 3fcb1a9..70c699b 100644 --- a/frontend/src/context/AuthContext.mock.tsx +++ b/frontend/src/context/AuthContext.mock.tsx @@ -7,18 +7,20 @@ export const LOCAL_USER_EMAIL = "mockuser@example.com"; interface AuthContextType { user: { userId: string } | null; isSignedIn: boolean; - userEmail?: string; + displayName: string | undefined; login: () => void; logout: () => void; + reFetchDisplayName: () => void; } // モック用 const AuthContext = createContext({ user: null, isSignedIn: false, - userEmail: "", + displayName: "", login: () => {}, logout: () => {}, + reFetchDisplayName: () => {}, }); export const useAuthContext = () => useContext(AuthContext); //eslint-disable-line @@ -44,9 +46,10 @@ export const MockAuthProvider: React.FC<{ children: React.ReactNode }> = ({ value={{ user, isSignedIn, - userEmail: isSignedIn ? LOCAL_USER_EMAIL : undefined, + displayName: isSignedIn ? LOCAL_USER_EMAIL : undefined, login, logout, + reFetchDisplayName: login, }} > {children} diff --git a/frontend/src/context/authContextCore.ts b/frontend/src/context/authContextCore.ts index 1561c60..6d3ebce 100644 --- a/frontend/src/context/authContextCore.ts +++ b/frontend/src/context/authContextCore.ts @@ -2,21 +2,24 @@ import { createContext, useContext } from "react"; import { AuthUser } from "aws-amplify/auth"; + export interface AuthContextType { user: AuthUser | null; isSignedIn: boolean; - userEmail?: string; + displayName: string | undefined; login: () => Promise; logout: () => Promise; + reFetchDisplayName: () => Promise; } /** Context本体 */ export const AuthContext = createContext({ user: null, isSignedIn: false, - userEmail: undefined, + displayName: undefined, login: async () => {}, logout: async () => {}, + reFetchDisplayName: async () => {}, }); /** カスタムフック */ diff --git a/frontend/src/pages/AccountInfoPage.tsx b/frontend/src/pages/AccountInfoPage.tsx new file mode 100644 index 0000000..32e1c4f --- /dev/null +++ b/frontend/src/pages/AccountInfoPage.tsx @@ -0,0 +1,177 @@ +// frontend/src/pages/AccountInfoPage.tsx +import React, { useEffect, useState } from "react"; +import { + updateUserAttributes, + confirmUserAttribute, + sendUserAttributeVerificationCode, + UpdateUserAttributesOutput, + fetchUserAttributes, +} from "aws-amplify/auth"; + +import { useAuthContextSwitch as useAuthContext } from "../context/useAuthContextSwitch.ts"; + +const AccountInfoPage: React.FC = () => { + const { + user, + isSignedIn, + displayName, // これはAmplifyAuthProviderで「最新の検証済みメール」をセットしたもの + reFetchDisplayName, // さきほどの「再取得関数」(無い場合は別途自前で書く) + } = useAuthContext(); + + /** ----------------------- + // 1) 表示用の「現在のメールアドレス」 + // => ここでは "検証済み" のものをセットしている + ----------------------- + */ + const [currentEmail, setCurrentEmail] = useState(""); + + useEffect(() => { + // 初期表示時に displayName を currentEmail にコピー + // (displayName は AmplifyAuthProvider 側が fetchUserAttributes した結果) + setCurrentEmail(displayName ?? ""); + }, [displayName]); + + // ----------------------- + // 2) 更新フロー用ステート + // ----------------------- + const [newEmail, setNewEmail] = useState(""); + const [confirmCode, setConfirmCode] = useState(""); + const [needConfirm, setNeedConfirm] = useState(false); + const [statusMessage, setStatusMessage] = useState(""); + + // ----------------------- + // 3) メールアドレス更新 + // ----------------------- + const handleUpdateEmail = async () => { + setStatusMessage(""); + try { + // (A) updateUserAttributes で新アドレスを「未検証」で登録 → 認証コード送信される + const output: UpdateUserAttributesOutput = await updateUserAttributes({ + userAttributes: { email: newEmail }, + }); + + // (B) nextStep が CONFIRM_ATTRIBUTE_WITH_CODE なら、コード入力待ちへ + if (output.email.nextStep.updateAttributeStep === "CONFIRM_ATTRIBUTE_WITH_CODE") { + setNeedConfirm(true); + setStatusMessage( + `新しいメール宛に確認コードを送りました。メールをチェックして、コードを入力してください。` + ); + } else { + // 確認不要設定の時など + setStatusMessage("メールアドレスが更新されました。(確認コード不要の設定です)"); + + // "自動的に検証済み扱い" になっているかもしれないので、念のため再取得 + await reFetchDisplayName?.(); + setCurrentEmail(displayName ?? newEmail); + } + } catch (err: any) { //eslint-disable-line + console.error("メールアドレス更新失敗:", err); + setStatusMessage(`更新エラー: ${err?.message || "不明なエラー"}`); + } + }; + + // ----------------------- + // 4) 確認コード再送 + // ----------------------- + const handleResendCode = async () => { + setStatusMessage(""); + try { + await sendUserAttributeVerificationCode({ userAttributeKey: "email" }); + setStatusMessage("確認コードを再送信しました。メールをチェックしてください。"); + } catch (err: any) { //eslint-disable-line + console.error("再送失敗:", err); + setStatusMessage(`コード再送エラー: ${err?.message || "不明なエラー"}`); + } + }; + + // ----------------------- + // 5) 確認コード入力 → confirmUserAttribute + // ----------------------- + const handleConfirmCode = async () => { + setStatusMessage(""); + try { + await confirmUserAttribute({ + userAttributeKey: "email", + confirmationCode: confirmCode, + }); + + setStatusMessage("メールアドレスの検証が完了しました。"); + + // (A) コード検証できたので、UIで「現在のメールアドレス」を新しいものに更新したい + // ただし Cognito は非同期のため、念のため fetchUserAttributes し直して最新を取得 + if (reFetchDisplayName) { + await reFetchDisplayName(); + } else { + // refetch 関数がない場合、自前で fetchUserAttributes() → 取得後 setCurrentEmail() + const attrs = await fetchUserAttributes(); + // attrs.email が現在の(=検証済み)メール + setCurrentEmail(attrs.email || ""); + } + + setConfirmCode(""); + setNeedConfirm(false); + } catch (err: any) { //eslint-disable-line + console.error("確認失敗:", err); + setStatusMessage(`確認エラー: ${err?.message || "不明なエラー"}`); //赤文字にしたい + } + }; + + // ----------------------- + // 6) 描画 + // ----------------------- + if (!isSignedIn) { + return ( +
+

ログインしていません。メールアドレスを変更するにはログインしてください。

+
+ ); + } + + return ( +
+

アカウント情報

+

ユーザー名(Cognito): {user?.userId || "unknown"}

+ + {/* 検証済みのメールアドレスを表示 */} +

現在のメールアドレス(検証済み): {currentEmail || "(未取得)"}

+ +
+ +

メールアドレス変更

+
+ + setNewEmail(e.target.value)} + placeholder="新しいメールアドレス" + style={{ marginRight: "0.5rem", width: "250px" }} + /> + +
+ + {needConfirm && ( +
+ + setConfirmCode(e.target.value)} + style={{ marginRight: "0.5rem", marginLeft: "0.5rem" }} + /> + + +
+ )} + + {/* ステータス表示 */} + {statusMessage && ( +
{statusMessage}
+ )} +
+ ); +}; + +export default AccountInfoPage;