Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 135 additions & 96 deletions README.md

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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 (
<Router>
Expand All @@ -39,8 +37,10 @@ const MainRouter: React.FC = () => {
<div className="navbar-right">
{isSignedIn ? (
<>
{/* メールアドレスを右寄せ表示 */}
<span className="user-email">{isSignedIn ? accountDisplay : ""}</span>
{/* displayName を押すと /account へ移動するように */}
<Link to="/account" className="user-email">
{displayName}
</Link>
<button className="logout-btn" onClick={logout}>
Logout
</button>
Expand All @@ -61,6 +61,7 @@ const MainRouter: React.FC = () => {
<Route path="/documents/:slug" element={<PublicDocumentPage/>}/>
<Route path="/privacy-policy" element={<PrivacyPolicyPage/>}/>
<Route path="/terms-of-use" element={<TermsOfUsePage />} />
<Route path="/account" element={<AccountInfoPage />} />
</Routes>
</main>

Expand Down
8 changes: 4 additions & 4 deletions frontend/src/__tests__/context/AuthContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
<p data-testid="userId">{user?.userId || 'no-user'}</p>
<p data-testid="isSignedIn">{isSignedIn ? 'true' : 'false'}</p>
<p data-testid="userEmail">{userEmail || 'no-email'}</p>
<p data-testid="displayName">{displayName || 'no-email'}</p>
<button onClick={login}>login</button>
<button onClick={logout}>logout</button>
</div>
Expand All @@ -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'))
Expand All @@ -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)
})
})
})
8 changes: 7 additions & 1 deletion frontend/src/config/amplifyConfigure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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},
}
},
},
};
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/context/AmplifyAuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
// 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';

export const AmplifyAuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isSignedIn, setIsSignedIn] = useState(false);
const [user, setUser] = useState<AuthUser | null>(null);
const [displayName, setDisplayName] = useState<string | undefined>(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);
Expand Down Expand Up @@ -66,8 +69,10 @@ export const AmplifyAuthProvider: React.FC<{ children: React.ReactNode }> = ({ c
value={{
user,
isSignedIn,
displayName,
login,
logout,
reFetchDisplayName: fetchCurrentUser,
}}
>
{children}
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/context/AuthContext.mock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<AuthContextType>({
user: null,
isSignedIn: false,
userEmail: "",
displayName: "",
login: () => {},
logout: () => {},
reFetchDisplayName: () => {},
});

export const useAuthContext = () => useContext(AuthContext); //eslint-disable-line
Expand All @@ -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}
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/context/authContextCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
logout: () => Promise<void>;
reFetchDisplayName: () => Promise<void>;
}

/** Context本体 */
export const AuthContext = createContext<AuthContextType>({
user: null,
isSignedIn: false,
userEmail: undefined,
displayName: undefined,
login: async () => {},
logout: async () => {},
reFetchDisplayName: async () => {},
});

/** カスタムフック */
Expand Down
177 changes: 177 additions & 0 deletions frontend/src/pages/AccountInfoPage.tsx
Original file line number Diff line number Diff line change
@@ -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<string>("");

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 (
<div style={{ padding: "1rem" }}>
<p>ログインしていません。メールアドレスを変更するにはログインしてください。</p>
</div>
);
}

return (
<div style={{ padding: "1rem" }}>
<h2>アカウント情報</h2>
<p>ユーザー名(Cognito): {user?.userId || "unknown"}</p>

{/* 検証済みのメールアドレスを表示 */}
<p>現在のメールアドレス(検証済み): {currentEmail || "(未取得)"} </p>

<hr style={{ margin: "1rem 0" }} />

<h3>メールアドレス変更</h3>
<div style={{ margin: "0.5rem 0" }}>
<label>新しいメールアドレス: </label>
<input
type="email"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
placeholder="新しいメールアドレス"
style={{ marginRight: "0.5rem", width: "250px" }}
/>
<button onClick={handleUpdateEmail}>更新</button>
</div>

{needConfirm && (
<div style={{ margin: "0.5rem 0", border: "1px solid #ccc", padding: "0.5rem" }}>
<label>メールに届いた確認コード:</label>
<input
type="text"
value={confirmCode}
onChange={(e) => setConfirmCode(e.target.value)}
style={{ marginRight: "0.5rem", marginLeft: "0.5rem" }}
/>
<button onClick={handleConfirmCode}>確認する</button>
<button style={{ marginLeft: "1rem" }} onClick={handleResendCode}>
コード再送
</button>
</div>
)}

{/* ステータス表示 */}
{statusMessage && (
<div style={{ marginTop: "1rem", color: "#007bff" }}>{statusMessage}</div>
)}
</div>
);
};

export default AccountInfoPage;
Loading