From 87e8a1550dbcc5e3d00d63e86227110304605fca Mon Sep 17 00:00:00 2001 From: Hiroshi Nishito Date: Wed, 15 Jan 2025 17:59:23 +0900 Subject: [PATCH 1/6] =?UTF-8?q?#34=20=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3?= =?UTF-8?q?=E3=83=88=E5=90=8D=E3=82=92=E3=83=A1=E3=83=BC=E3=83=AB=E3=82=A2?= =?UTF-8?q?=E3=83=89=E3=83=AC=E3=82=B9=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit メールアドレスの取得ができるようになった。 --- frontend/src/App.tsx | 7 ++----- frontend/src/config/amplifyConfigure.ts | 8 +++++++- frontend/src/context/AmplifyAuthProvider.tsx | 6 +++++- frontend/src/context/AuthContext.mock.tsx | 6 +++--- frontend/src/context/authContextCore.ts | 5 +++-- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1cab667..4bb7e72 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -22,10 +22,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 ( @@ -40,7 +37,7 @@ const MainRouter: React.FC = () => { {isSignedIn ? ( <> {/* メールアドレスを右寄せ表示 */} - {isSignedIn ? accountDisplay : ""} + {isSignedIn ? displayName : ""} 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..50f7779 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,6 +69,7 @@ export const AmplifyAuthProvider: React.FC<{ children: React.ReactNode }> = ({ c value={{ user, isSignedIn, + displayName, login, logout, }} diff --git a/frontend/src/context/AuthContext.mock.tsx b/frontend/src/context/AuthContext.mock.tsx index 3fcb1a9..14e0b3c 100644 --- a/frontend/src/context/AuthContext.mock.tsx +++ b/frontend/src/context/AuthContext.mock.tsx @@ -7,7 +7,7 @@ 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; } @@ -16,7 +16,7 @@ interface AuthContextType { const AuthContext = createContext({ user: null, isSignedIn: false, - userEmail: "", + displayName: "", login: () => {}, logout: () => {}, }); @@ -44,7 +44,7 @@ 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, }} diff --git a/frontend/src/context/authContextCore.ts b/frontend/src/context/authContextCore.ts index 1561c60..8926401 100644 --- a/frontend/src/context/authContextCore.ts +++ b/frontend/src/context/authContextCore.ts @@ -2,10 +2,11 @@ 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; } @@ -14,7 +15,7 @@ export interface AuthContextType { export const AuthContext = createContext({ user: null, isSignedIn: false, - userEmail: undefined, + displayName: undefined, login: async () => {}, logout: async () => {}, }); From b06e1da305d26b8425f92fff8e99e3f5cf577dd2 Mon Sep 17 00:00:00 2001 From: Hiroshi Nishito Date: Wed, 15 Jan 2025 20:53:01 +0900 Subject: [PATCH 2/6] =?UTF-8?q?#34=20=E3=83=A1=E3=83=BC=E3=83=AB=E3=82=A2?= =?UTF-8?q?=E3=83=89=E3=83=AC=E3=82=B9=E5=A4=89=E6=9B=B4=E3=81=AB=E3=81=AF?= =?UTF-8?q?=E6=88=90=E5=8A=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ただし確認メール等の仕組みではないので、改善が必要。 --- frontend/src/App.tsx | 8 +- frontend/src/pages/AccountInfoPage.tsx | 142 +++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 frontend/src/pages/AccountInfoPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4bb7e72..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 ( @@ -36,8 +37,10 @@ const MainRouter: React.FC = () => {
{isSignedIn ? ( <> - {/* メールアドレスを右寄せ表示 */} - {isSignedIn ? displayName : ""} + {/* displayName を押すと /account へ移動するように */} + + {displayName} + @@ -58,6 +61,7 @@ const MainRouter: React.FC = () => { }/> }/> } /> + } /> diff --git a/frontend/src/pages/AccountInfoPage.tsx b/frontend/src/pages/AccountInfoPage.tsx new file mode 100644 index 0000000..7b97786 --- /dev/null +++ b/frontend/src/pages/AccountInfoPage.tsx @@ -0,0 +1,142 @@ +// frontend/src/pages/AccountInfoPage.tsx +import React, { useEffect, useState } from "react"; +import { + updateUserAttributes, + confirmUserAttribute, + sendUserAttributeVerificationCode, + UpdateUserAttributesOutput +} from "aws-amplify/auth"; + +import { useAuthContextSwitch as useAuthContext} from "../context/useAuthContextSwitch.ts"; + +const AccountInfoPage: React.FC = () => { + const { user, isSignedIn, displayName } = useAuthContext(); + + // フォーム状態管理 + const [newEmail, setNewEmail] = useState(""); + const [confirmCode, setConfirmCode] = useState(""); + const [needConfirm, setNeedConfirm] = useState(false); + const [statusMessage, setStatusMessage] = useState(""); + + // 初期メールアドレスを表示するなら(userAttributes を別途 fetchして保持など) + // ここでは AmplifyAuthProvider で既に displayName = userAttributes.email を + // 入れている例なので、表示だけする + useEffect(() => { + if (displayName) { + console.log("Current email: ", displayName); + } + }, [displayName]); + + /** + * メールアドレス更新 + */ + const handleUpdateEmail = async () => { + setStatusMessage(""); + try { + // userAttributes 更新 + const output: UpdateUserAttributesOutput = await updateUserAttributes({ + userAttributes: { email: newEmail }, + }); + + if (output.email.nextStep.updateAttributeStep === "CONFIRM_ATTRIBUTE_WITH_CODE") { + // Cognito 側が「新しいメールアドレスの確認コード」を送信している + setNeedConfirm(true); + setStatusMessage("確認コードを送信しました。届いたコードを入力し、確認ボタンを押してください。"); + } else { + // コード不要な場合も(設定によっては)ある + setStatusMessage("メールアドレスが更新されました。(確認不要)"); + } + } catch (err: any) { //eslint-disable-line + console.error("メールアドレス更新失敗: ", err); + setStatusMessage(`更新エラー: ${err?.message || "不明なエラー"}`); + } + }; + + /** + * 確認コード送信(念のため再送) + */ + const handleResendCode = async () => { + setStatusMessage(""); + try { + await sendUserAttributeVerificationCode({ + userAttributeKey: "email", + }); + setStatusMessage("確認コードを再送信しました。メールをチェックしてください。"); + } catch (err: any) { //eslint-disable-line + console.error("再送失敗: ", err); + setStatusMessage(`コード再送エラー: ${err?.message || "不明なエラー"}`); + } + }; + + /** + * 確認コードを使って属性確定 + */ + const handleConfirmCode = async () => { + setStatusMessage(""); + try { + await confirmUserAttribute({ + userAttributeKey: "email", + confirmationCode: confirmCode, + }); + setStatusMessage("メールアドレス確認が完了しました。"); + setNeedConfirm(false); + setConfirmCode(""); + } catch (err: any) { //eslint-disable-line + console.error("確認失敗: ", err); + setStatusMessage(`確認エラー: ${err?.message || "不明なエラー"}`); + } + }; + + return ( +
+ {isSignedIn ? ( + <> +

アカウント情報

+

現在のログインID: { user?.userId || "unknown"}

+

現在のメールアドレス(表示名): {displayName || "(未取得)"}

+ +
+ +

メールアドレス変更

+
+ + setNewEmail(e.target.value)} + placeholder="例: newaddress@example.com" + style={{ marginRight: "0.5rem", width: "250px" }} + /> + +
+ + {needConfirm && ( +
+ + setConfirmCode(e.target.value)} + style={{ marginRight: "0.5rem", marginLeft: "0.5rem" }} + /> + + +
+ )} + + {/* ステータス表示 */} + {statusMessage && ( +
{statusMessage}
+ )} + + ) : ( +

ログインしてください。

+ )} + +
+ ); +}; + +export default AccountInfoPage; From a687a42aa9029218edad6b3c1a8eb48ad49ab6e8 Mon Sep 17 00:00:00 2001 From: Hiroshi Nishito Date: Thu, 16 Jan 2025 00:40:03 +0900 Subject: [PATCH 3/6] =?UTF-8?q?#34=20=E6=89=8B=E5=8B=95=E7=A2=BA=E8=AA=8DO?= =?UTF-8?q?K?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ローカル、オンラインどちらもOK。 Cognitoの設定も重要なので、Issueに画像として保存した。 --- frontend/src/context/AmplifyAuthProvider.tsx | 1 + frontend/src/context/AuthContext.mock.tsx | 3 + frontend/src/context/authContextCore.ts | 2 + frontend/src/pages/AccountInfoPage.tsx | 195 +++++++++++-------- 4 files changed, 121 insertions(+), 80 deletions(-) diff --git a/frontend/src/context/AmplifyAuthProvider.tsx b/frontend/src/context/AmplifyAuthProvider.tsx index 50f7779..77b885e 100644 --- a/frontend/src/context/AmplifyAuthProvider.tsx +++ b/frontend/src/context/AmplifyAuthProvider.tsx @@ -72,6 +72,7 @@ export const AmplifyAuthProvider: React.FC<{ children: React.ReactNode }> = ({ c displayName, login, logout, + reFetchDisplayName: fetchCurrentUser, }} > {children} diff --git a/frontend/src/context/AuthContext.mock.tsx b/frontend/src/context/AuthContext.mock.tsx index 14e0b3c..70c699b 100644 --- a/frontend/src/context/AuthContext.mock.tsx +++ b/frontend/src/context/AuthContext.mock.tsx @@ -10,6 +10,7 @@ interface AuthContextType { displayName: string | undefined; login: () => void; logout: () => void; + reFetchDisplayName: () => void; } // モック用 @@ -19,6 +20,7 @@ const AuthContext = createContext({ displayName: "", login: () => {}, logout: () => {}, + reFetchDisplayName: () => {}, }); export const useAuthContext = () => useContext(AuthContext); //eslint-disable-line @@ -47,6 +49,7 @@ export const MockAuthProvider: React.FC<{ children: React.ReactNode }> = ({ 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 8926401..6d3ebce 100644 --- a/frontend/src/context/authContextCore.ts +++ b/frontend/src/context/authContextCore.ts @@ -9,6 +9,7 @@ export interface AuthContextType { displayName: string | undefined; login: () => Promise; logout: () => Promise; + reFetchDisplayName: () => Promise; } /** Context本体 */ @@ -18,6 +19,7 @@ export const AuthContext = createContext({ displayName: undefined, login: async () => {}, logout: async () => {}, + reFetchDisplayName: async () => {}, }); /** カスタムフック */ diff --git a/frontend/src/pages/AccountInfoPage.tsx b/frontend/src/pages/AccountInfoPage.tsx index 7b97786..32e1c4f 100644 --- a/frontend/src/pages/AccountInfoPage.tsx +++ b/frontend/src/pages/AccountInfoPage.tsx @@ -4,73 +4,89 @@ import { updateUserAttributes, confirmUserAttribute, sendUserAttributeVerificationCode, - UpdateUserAttributesOutput + UpdateUserAttributesOutput, + fetchUserAttributes, } from "aws-amplify/auth"; -import { useAuthContextSwitch as useAuthContext} from "../context/useAuthContextSwitch.ts"; +import { useAuthContextSwitch as useAuthContext } from "../context/useAuthContextSwitch.ts"; const AccountInfoPage: React.FC = () => { - const { user, isSignedIn, displayName } = useAuthContext(); + 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(""); - // 初期メールアドレスを表示するなら(userAttributes を別途 fetchして保持など) - // ここでは AmplifyAuthProvider で既に displayName = userAttributes.email を - // 入れている例なので、表示だけする - useEffect(() => { - if (displayName) { - console.log("Current email: ", displayName); - } - }, [displayName]); - - /** - * メールアドレス更新 - */ + // ----------------------- + // 3) メールアドレス更新 + // ----------------------- const handleUpdateEmail = async () => { setStatusMessage(""); try { - // userAttributes 更新 + // (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") { - // Cognito 側が「新しいメールアドレスの確認コード」を送信している setNeedConfirm(true); - setStatusMessage("確認コードを送信しました。届いたコードを入力し、確認ボタンを押してください。"); + setStatusMessage( + `新しいメール宛に確認コードを送りました。メールをチェックして、コードを入力してください。` + ); } else { - // コード不要な場合も(設定によっては)ある - setStatusMessage("メールアドレスが更新されました。(確認不要)"); + // 確認不要設定の時など + setStatusMessage("メールアドレスが更新されました。(確認コード不要の設定です)"); + + // "自動的に検証済み扱い" になっているかもしれないので、念のため再取得 + await reFetchDisplayName?.(); + setCurrentEmail(displayName ?? newEmail); } } catch (err: any) { //eslint-disable-line - console.error("メールアドレス更新失敗: ", err); + console.error("メールアドレス更新失敗:", err); setStatusMessage(`更新エラー: ${err?.message || "不明なエラー"}`); } }; - /** - * 確認コード送信(念のため再送) - */ + // ----------------------- + // 4) 確認コード再送 + // ----------------------- const handleResendCode = async () => { setStatusMessage(""); try { - await sendUserAttributeVerificationCode({ - userAttributeKey: "email", - }); + await sendUserAttributeVerificationCode({ userAttributeKey: "email" }); setStatusMessage("確認コードを再送信しました。メールをチェックしてください。"); } catch (err: any) { //eslint-disable-line - console.error("再送失敗: ", err); + console.error("再送失敗:", err); setStatusMessage(`コード再送エラー: ${err?.message || "不明なエラー"}`); } }; - /** - * 確認コードを使って属性確定 - */ + // ----------------------- + // 5) 確認コード入力 → confirmUserAttribute + // ----------------------- const handleConfirmCode = async () => { setStatusMessage(""); try { @@ -78,63 +94,82 @@ const AccountInfoPage: React.FC = () => { userAttributeKey: "email", confirmationCode: confirmCode, }); - setStatusMessage("メールアドレス確認が完了しました。"); - setNeedConfirm(false); + + 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 || "不明なエラー"}`); + console.error("確認失敗:", err); + setStatusMessage(`確認エラー: ${err?.message || "不明なエラー"}`); //赤文字にしたい } }; + // ----------------------- + // 6) 描画 + // ----------------------- + if (!isSignedIn) { + return ( +
+

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

+
+ ); + } + return (
- {isSignedIn ? ( - <> -

アカウント情報

-

現在のログインID: { user?.userId || "unknown"}

-

現在のメールアドレス(表示名): {displayName || "(未取得)"}

- -
- -

メールアドレス変更

-
- - setNewEmail(e.target.value)} - placeholder="例: newaddress@example.com" - style={{ marginRight: "0.5rem", width: "250px" }} - /> - -
- - {needConfirm && ( -
- - setConfirmCode(e.target.value)} - style={{ marginRight: "0.5rem", marginLeft: "0.5rem" }} - /> - - -
- )} - - {/* ステータス表示 */} - {statusMessage && ( -
{statusMessage}
- )} - - ) : ( -

ログインしてください。

+

アカウント情報

+

ユーザー名(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}
+ )}
); }; From 9e75eba63a86849409afcfefc59bbbbdfc7fef22 Mon Sep 17 00:00:00 2001 From: Hiroshi Nishito Date: Thu, 16 Jan 2025 00:59:32 +0900 Subject: [PATCH 4/6] =?UTF-8?q?#34=20=E3=83=86=E3=82=B9=E3=83=88=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit front/backどちらも100%貫通。 --- frontend/src/__tests__/context/AuthContext.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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) }) }) }) From f4de18ee0306f7ebc44800b39a174d7369338685 Mon Sep 17 00:00:00 2001 From: Hiroshi Nishito Date: Thu, 16 Jan 2025 01:00:06 +0900 Subject: [PATCH 5/6] =?UTF-8?q?#18=20README=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI/CDとGitのブランチ管理の方法を記載する必要がある。 --- README.md | 224 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 128 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index 370fc46..dc598f5 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,45 @@ 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 コンソールのバージョンによって異なる場合があります。 + +--- + +## GitのSecrets管理 +Secretsの一覧画像は以下のとおりです。 +![](https://github.com/user-attachments/assets/0fdb1c27-fd3e-483a-85eb-d977ab34c251) + +## デプロイ: 開発ステージ (dev) -#### (1) 前提 -- AWS アカウントに対して DynamoDB / Lambda / API Gateway 等のIAM権限を所持 +### 前提 + +- 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 +128,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 +138,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 From f64bd74bda231021f43d607649932fd9311dbb9f Mon Sep 17 00:00:00 2001 From: Hiroshi Nishito Date: Thu, 16 Jan 2025 09:18:24 +0900 Subject: [PATCH 6/6] =?UTF-8?q?#38=20=E3=83=90=E3=83=83=E3=82=AF=E3=82=A2?= =?UTF-8?q?=E3=83=83=E3=83=97=E8=A8=AD=E5=AE=9A=E6=96=B9=E6=B3=95=E3=82=92?= =?UTF-8?q?README=E3=81=AB=E8=A8=98=E8=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index dc598f5..e12c896 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,13 @@ curl http://localhost:3000/local/api/docs --- +### 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)