diff --git a/src/Components/DashBoard/DashBoard.tsx b/src/Components/DashBoard/DashBoard.tsx index 9bb477d3..45293525 100644 --- a/src/Components/DashBoard/DashBoard.tsx +++ b/src/Components/DashBoard/DashBoard.tsx @@ -77,6 +77,18 @@ export default function DashBoard({ limit, }) .then((data) => { + if ( + !data || + !data.successResults || + !data.failedResults || + !data.pendingResults + ) { + showSnackBar({ + message: "データの取得に失敗しました。ログインし直してください。", + type: "warning", + }); + return; + } // 既に追加されている場合は追加しない setSuccessResults((prev) => { const newResults = data.successResults.filter( @@ -213,6 +225,9 @@ export default function DashBoard({ limit: 1, }) .then((data) => { + if (!data.successResults) { + return; + } if (data.successResults.length > 0) { setLastPostDate(data.successResults[0].post?.submittedAt); } diff --git a/src/Components/GoalModal/DeleteGoalModal.tsx b/src/Components/GoalModal/DeleteGoalModal.tsx index c97635c1..4d592d4b 100644 --- a/src/Components/GoalModal/DeleteGoalModal.tsx +++ b/src/Components/GoalModal/DeleteGoalModal.tsx @@ -1,5 +1,6 @@ "use client"; -import { appCheckToken, functionsEndpoint } from "@/app/firebase"; +import { functionsEndpoint } from "@/app/firebase"; +import getAppCheckToken from "@/utils/getAppCheckToken"; import { useDeleteGoal } from "@/utils/ResultContext"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import { DialogContent, DialogTitle, Modal, ModalDialog } from "@mui/joy"; @@ -14,6 +15,23 @@ export default function DeleteGoalModal({ goalId }: { goalId: string }) { const [open, setOpen] = useState(false); const handleDelete = async () => { + const appCheckToken = await getAppCheckToken().catch((error) => { + showSnackBar({ + message: error.message, + type: "warning", + }); + return ""; + }); + + if (!appCheckToken) { + showSnackBar({ + message: + "App Checkの初期化に失敗しました。debug tokenがサーバーに登録されていることを確認してください。", + type: "warning", + }); + return; + } + try { const response = await fetch(`${functionsEndpoint}/goal/${goalId}`, { method: "DELETE", diff --git a/src/Components/NameUpdate/NameUpdate.tsx b/src/Components/NameUpdate/NameUpdate.tsx index 5365bffd..24d0b247 100644 --- a/src/Components/NameUpdate/NameUpdate.tsx +++ b/src/Components/NameUpdate/NameUpdate.tsx @@ -1,5 +1,6 @@ "use client"; -import { appCheckToken, auth, functionsEndpoint } from "@/app/firebase"; +import { auth, functionsEndpoint } from "@/app/firebase"; +import getAppCheckToken from "@/utils/getAppCheckToken"; import { useUser } from "@/utils/UserContext"; import { DialogContent, @@ -33,6 +34,23 @@ export default function NameUpdate() { return; } + const appCheckToken = await getAppCheckToken().catch((error) => { + showSnackBar({ + message: error.message, + type: "warning", + }); + return ""; + }); + + if (!appCheckToken) { + showSnackBar({ + message: + "App Checkの初期化に失敗しました。debug tokenがサーバーに登録されていることを確認してください。", + type: "warning", + }); + return; + } + try { const response = await fetch( `${functionsEndpoint}/user/${user?.userId}`, diff --git a/src/Components/PostModal/DeletePostModal.tsx b/src/Components/PostModal/DeletePostModal.tsx index 65c99c34..e43f51c0 100644 --- a/src/Components/PostModal/DeletePostModal.tsx +++ b/src/Components/PostModal/DeletePostModal.tsx @@ -1,5 +1,6 @@ "use client"; -import { appCheckToken, functionsEndpoint } from "@/app/firebase"; +import { functionsEndpoint } from "@/app/firebase"; +import getAppCheckToken from "@/utils/getAppCheckToken"; import { useDeletePost } from "@/utils/ResultContext"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import { DialogContent, DialogTitle, Modal, ModalDialog } from "@mui/joy"; @@ -20,6 +21,23 @@ export default function DeletePostModal({ const [open, setOpen] = useState(false); const handleDelete = async () => { + const appCheckToken = await getAppCheckToken().catch((error) => { + showSnackBar({ + message: error.message, + type: "warning", + }); + return ""; + }); + + if (!appCheckToken) { + showSnackBar({ + message: + "App Checkの初期化に失敗しました。debug tokenがサーバーに登録されていることを確認してください。", + type: "warning", + }); + return; + } + try { const response = await fetch(`${functionsEndpoint}/post/${goalId}`, { method: "DELETE", diff --git a/src/Components/Progress/Progress.tsx b/src/Components/Progress/Progress.tsx index daad667f..e12736ae 100644 --- a/src/Components/Progress/Progress.tsx +++ b/src/Components/Progress/Progress.tsx @@ -49,7 +49,7 @@ export default function Progress({ return ( ); @@ -72,7 +72,7 @@ export default function Progress({ return ( ); diff --git a/src/app/firebase.ts b/src/app/firebase.ts index b5b0853c..4f6d803d 100644 --- a/src/app/firebase.ts +++ b/src/app/firebase.ts @@ -1,11 +1,5 @@ -import { showSnackBar } from "@/Components/SnackBar/SnackBar"; import { Analytics, getAnalytics } from "firebase/analytics"; import { initializeApp } from "firebase/app"; -import { - getToken, - initializeAppCheck, - ReCaptchaV3Provider, -} from "firebase/app-check"; import { browserLocalPersistence, connectAuthEmulator, @@ -31,44 +25,11 @@ export const storage = getStorage(app); export const auth = getAuth(app); let messaging: Messaging | null = null; let analytics: Analytics | null = null; -let appCheckToken = ""; // クライアントサイドでのみ実行する初期化 if (typeof window !== "undefined") { messaging = getMessaging(app); analytics = getAnalytics(app); - - // 開発環境ならApp Checkを使用しない - // ステージング環境でApp Checkのデバッグトークンを有効にする - if (process.env.NODE_ENV !== "development") { - if (process.env.NEXT_PUBLIC_IS_STAGING === "true") { - ( - window as unknown as { FIREBASE_APPCHECK_DEBUG_TOKEN: boolean } - ).FIREBASE_APPCHECK_DEBUG_TOKEN = true; - console.log("App Check Debug Token Enabled"); - } - // App Checkの設定 - const appCheck = initializeAppCheck(app, { - provider: new ReCaptchaV3Provider( - process.env.NEXT_PUBLIC_FIREBASE_RECAPTCHA_SITEKEY as string - ), - isTokenAutoRefreshEnabled: true, - }); - - getToken(appCheck) - .then((token) => { - console.log("App Check: Success"); - appCheckToken = token.token; - }) - .catch((error) => { - console.log(error.message); - showSnackBar({ - message: - "App Checkの初期化に失敗しました。debug tokenがサーバーに登録されていることを確認してください。", - type: "warning", - }); - }); - } } export const googleProvider = new GoogleAuthProvider(); @@ -99,5 +60,5 @@ if (process.env.NODE_ENV === "production") { console.log("Functions: Emulator"); } -export { analytics, appCheckToken, functionsEndpoint, messaging }; +export { analytics, functionsEndpoint, messaging }; console.log("firebaseConfig initialized"); diff --git a/src/utils/API/Goal/createGoal.ts b/src/utils/API/Goal/createGoal.ts index d4440190..a28696c5 100644 --- a/src/utils/API/Goal/createGoal.ts +++ b/src/utils/API/Goal/createGoal.ts @@ -1,5 +1,6 @@ -import { appCheckToken, functionsEndpoint } from "@/app/firebase"; +import { functionsEndpoint } from "@/app/firebase"; import { Goal } from "@/types/types"; +import getAppCheckToken from "@/utils/getAppCheckToken"; /** * Cloud FunctionsのAPIを呼び出して、目標をFirestoreに登録する @@ -18,6 +19,8 @@ export const createGoal = async (postData: Goal) => { throw new Error("too long comment"); } + const appCheckToken = await getAppCheckToken(); + const response = await fetch(`${functionsEndpoint}/goal/`, { method: "POST", headers: { @@ -61,6 +64,10 @@ export const handleCreateGoalError = (error: unknown) => { if (error.message.includes("500")) { snackBarMessage = "サーバーエラーが発生しました"; } + if (error.message.includes("App Checkの初期化に失敗しました。")) { + snackBarMessage = + "App Checkの初期化に失敗しました。debug tokenがサーバーに登録されていることを確認してください。"; + } } else { console.error("An unknown error occurred"); snackBarMessage = "不明なエラーが発生しました"; diff --git a/src/utils/API/Goal/updateGoal.ts b/src/utils/API/Goal/updateGoal.ts index d212dcec..33dc381a 100644 --- a/src/utils/API/Goal/updateGoal.ts +++ b/src/utils/API/Goal/updateGoal.ts @@ -1,4 +1,5 @@ -import { appCheckToken, functionsEndpoint } from "@/app/firebase"; +import { functionsEndpoint } from "@/app/firebase"; +import getAppCheckToken from "@/utils/getAppCheckToken"; /** * Cloud FunctionsのAPIを呼び出して、目標を編集する @@ -27,6 +28,8 @@ export const updateGoal = async ( throw new Error("too long comment"); } + const appCheckToken = await getAppCheckToken(); + const response = await fetch(`${functionsEndpoint}/goal/${goalId}`, { method: "PUT", headers: { @@ -70,6 +73,10 @@ export const handleUpdateGoalError = (error: unknown) => { if (error.message.includes("500")) { snackBarMessage = "サーバーエラーが発生しました"; } + if (error.message.includes("App Checkの初期化に失敗しました。")) { + snackBarMessage = + "App Checkの初期化に失敗しました。debug tokenがサーバーに登録されていることを確認してください。"; + } } else { console.error("An unknown error occurred"); snackBarMessage = "不明なエラーが発生しました"; diff --git a/src/utils/API/Post/createPost.ts b/src/utils/API/Post/createPost.ts index 77c57356..cd21d968 100644 --- a/src/utils/API/Post/createPost.ts +++ b/src/utils/API/Post/createPost.ts @@ -1,5 +1,6 @@ -import { appCheckToken, functionsEndpoint } from "@/app/firebase"; +import { functionsEndpoint } from "@/app/firebase"; import { PostWithGoalId } from "@/types/types"; +import getAppCheckToken from "@/utils/getAppCheckToken"; /** * Cloud FunctionsのAPIを呼び出して、投稿をFirestoreに登録する @@ -13,6 +14,8 @@ export const createPost = async (postData: PostWithGoalId) => { throw new Error("too long comment"); } + const appCheckToken = await getAppCheckToken(); + const response = await fetch(`${functionsEndpoint}/post/`, { method: "POST", headers: { @@ -53,6 +56,10 @@ export const handleCreatePostError = (error: unknown) => { if (error.message.includes("500")) { snackBarMessage = "サーバーエラーが発生しました"; } + if (error.message.includes("App Checkの初期化に失敗しました。")) { + snackBarMessage = + "App Checkの初期化に失敗しました。debug tokenがサーバーに登録されていることを確認してください。"; + } } else { console.error("An unknown error occurred"); snackBarMessage = "不明なエラーが発生しました"; diff --git a/src/utils/API/Reaction/updateReaction.ts b/src/utils/API/Reaction/updateReaction.ts index 92e1b2f8..4b4a7f9b 100644 --- a/src/utils/API/Reaction/updateReaction.ts +++ b/src/utils/API/Reaction/updateReaction.ts @@ -1,5 +1,6 @@ -import { appCheckToken, functionsEndpoint } from "@/app/firebase"; +import { functionsEndpoint } from "@/app/firebase"; import { ReactionTypeMap } from "@/types/types"; +import getAppCheckToken from "@/utils/getAppCheckToken"; /** * Cloud FunctionsのAPIを呼び出して、リアクションを更新する @@ -15,6 +16,8 @@ export const updateReaction = async ( goalId: string, reactionType: ReactionTypeMap | "" ) => { + const appCheckToken = await getAppCheckToken(); + const response = await fetch(`${functionsEndpoint}/reaction/${goalId}`, { method: "PUT", headers: { diff --git a/src/utils/API/Result/fetchResult.ts b/src/utils/API/Result/fetchResult.ts index b04858b4..d5cf61da 100644 --- a/src/utils/API/Result/fetchResult.ts +++ b/src/utils/API/Result/fetchResult.ts @@ -1,4 +1,5 @@ -import { appCheckToken, functionsEndpoint } from "@/app/firebase"; +import { functionsEndpoint } from "@/app/firebase"; +import getAppCheckToken from "@/utils/getAppCheckToken"; /** * Cloud FunctionsのAPIを呼び出して、結果の一覧を取得する @@ -34,6 +35,8 @@ export const fetchResult = async ({ queryParams.append("limit", limit.toString()); } + const appCheckToken = await getAppCheckToken(); + const response = await fetch( `${functionsEndpoint}/result/${userId}?${queryParams.toString()}`, { @@ -75,6 +78,10 @@ export const handleFetchResultError = (error: unknown) => { if (error.message.includes("500")) { snackBarMessage = "サーバーエラーが発生しました"; } + if (error.message.includes("App Checkの初期化に失敗しました。")) { + snackBarMessage = + "App Checkの初期化に失敗しました。debug tokenがサーバーに登録されていることを確認してください。"; + } } else { console.error("An unknown error occurred"); snackBarMessage = "不明なエラーが発生しました"; diff --git a/src/utils/API/User/fetchUser.ts b/src/utils/API/User/fetchUser.ts index 534396dd..1d7cbdc5 100644 --- a/src/utils/API/User/fetchUser.ts +++ b/src/utils/API/User/fetchUser.ts @@ -1,5 +1,6 @@ -import { appCheckToken, functionsEndpoint } from "@/app/firebase"; +import { functionsEndpoint } from "@/app/firebase"; import { User } from "@/types/types"; +import getAppCheckToken from "@/utils/getAppCheckToken"; /** * Cloud FunctionsのAPIを呼び出して、ユーザー情報をFirestoreから取得する @@ -8,6 +9,8 @@ import { User } from "@/types/types"; * @return {*} {Promise} */ export const fetchUserById = async (userId: string): Promise => { + const appCheckToken = await getAppCheckToken(); + const response = await fetch(`${functionsEndpoint}/user/id/${userId}`, { method: "GET", headers: { @@ -45,6 +48,10 @@ export const handleFetchUserError = (error: unknown) => { if (error.message.includes("429")) { snackBarMessage = "リクエストが多すぎます。数分後に再度お試しください。"; } + if (error.message.includes("App Checkの初期化に失敗しました。")) { + snackBarMessage = + "App Checkの初期化に失敗しました。debug tokenがサーバーに登録されていることを確認してください。"; + } } else { console.error("An unknown error occurred"); snackBarMessage = "不明なエラーが発生しました"; diff --git a/src/utils/Auth/signInWithGoogleAccount.ts b/src/utils/Auth/signInWithGoogleAccount.ts index 222c5e2f..f2fb85d3 100644 --- a/src/utils/Auth/signInWithGoogleAccount.ts +++ b/src/utils/Auth/signInWithGoogleAccount.ts @@ -23,6 +23,11 @@ export const signInWithGoogleAccount = async () => { "Googleアカウントでのログインに失敗しました。ページを更新して再度お試しください。"; if ((error as Error)?.message.includes("auth/popup-closed-by-user")) { message = "ログインがキャンセルされました"; + } else if ( + (error as Error)?.message.includes("appCheck/fetch-status-error") + ) { + message = + "App Checkの初期化に失敗しました。debug tokenがサーバーに登録されていることを確認してください。"; } showSnackBar({ message, diff --git a/src/utils/Auth/signInWithMail.ts b/src/utils/Auth/signInWithMail.ts index 90e56093..80662583 100644 --- a/src/utils/Auth/signInWithMail.ts +++ b/src/utils/Auth/signInWithMail.ts @@ -33,6 +33,9 @@ export const signInWithMail = async (email: string, password: string) => { } else if (errorMessage.includes("auth/too-many-requests")) { snackBarMessage = "リクエストが多すぎます。しばらくしてからもう一度お試しください。"; + } else if (errorMessage.includes("appCheck/fetch-status-error")) { + snackBarMessage = + "App Checkの初期化に失敗しました。debug tokenがサーバーに登録されていることを確認してください"; } showSnackBar({ diff --git a/src/utils/Auth/signUpWithMail.tsx b/src/utils/Auth/signUpWithMail.tsx index 87d97f9f..48a67c55 100644 --- a/src/utils/Auth/signUpWithMail.tsx +++ b/src/utils/Auth/signUpWithMail.tsx @@ -1,10 +1,11 @@ -import { appCheckToken, auth, functionsEndpoint } from "@/app/firebase"; +import { auth, functionsEndpoint } from "@/app/firebase"; import { showSnackBar } from "@/Components/SnackBar/SnackBar"; import { createUserWithEmailAndPassword, sendEmailVerification, updateProfile, } from "firebase/auth"; +import getAppCheckToken from "../getAppCheckToken"; import { updateUser } from "../UserContext"; /** @@ -31,6 +32,23 @@ export const signUpWithMail = async ( console.error("Error updating user name:", profileUpdateError); } + const appCheckToken = await getAppCheckToken().catch((error) => { + showSnackBar({ + message: error.message, + type: "warning", + }); + return ""; + }); + + if (!appCheckToken) { + showSnackBar({ + message: + "App Checkの初期化に失敗しました。debug tokenがサーバーに登録されていることを確認してください。", + type: "warning", + }); + return; + } + // displayNameをFirestoreに登録 const response = await fetch(`${functionsEndpoint}/user/${user.uid}`, { method: "PUT", diff --git a/src/utils/CloudMessaging/notificationController.ts b/src/utils/CloudMessaging/notificationController.ts index 56beb923..7664d4f4 100644 --- a/src/utils/CloudMessaging/notificationController.ts +++ b/src/utils/CloudMessaging/notificationController.ts @@ -1,6 +1,8 @@ "use client"; -import { appCheckToken, functionsEndpoint, messaging } from "@/app/firebase"; +import { functionsEndpoint, messaging } from "@/app/firebase"; +import { showSnackBar } from "@/Components/SnackBar/SnackBar"; import { getToken } from "firebase/messaging"; +import getAppCheckToken from "../getAppCheckToken"; // 通知を受信する export async function requestPermission(userId: string): Promise { @@ -9,6 +11,7 @@ export async function requestPermission(userId: string): Promise { return; } + // 通知の許可を取得 console.log("Requesting permission..."); const permission = await Notification.requestPermission(); if (permission !== "granted") { @@ -16,6 +19,7 @@ export async function requestPermission(userId: string): Promise { } console.log("Notification permission granted."); + // Service Workerの登録 const registration = await navigator.serviceWorker .register("/messaging-sw.js") .then(() => navigator.serviceWorker.ready); @@ -25,24 +29,43 @@ export async function requestPermission(userId: string): Promise { throw new Error("Firebase messaging is not initialized"); } - const currentToken = await getToken(messaging, { + // トークンの取得 + const notificationToken = await getToken(messaging, { vapidKey: process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY, serviceWorkerRegistration: registration, }); - if (!currentToken) { + if (!notificationToken) { throw new Error("No registration token available"); } - console.log("currentToken:", currentToken); + console.log("currentToken:", notificationToken); + const appCheckToken = await getAppCheckToken().catch((error) => { + showSnackBar({ + message: error.message, + type: "warning", + }); + return ""; + }); + + if (!appCheckToken) { + showSnackBar({ + message: + "App Checkの初期化に失敗しました。debug tokenがサーバーに登録されていることを確認してください。", + type: "warning", + }); + return; + } + + // トークンをFirestoreに登録 const response = await fetch(`${functionsEndpoint}/user/${userId}`, { method: "PUT", headers: { "X-Firebase-AppCheck": appCheckToken, "Content-Type": "application/json", }, - body: JSON.stringify({ fcmToken: currentToken }), + body: JSON.stringify({ fcmToken: notificationToken }), }); if (!response.ok) { @@ -63,6 +86,7 @@ export async function revokePermission(userId: string): Promise { const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.getSubscription(); + // 通知を解除 if (subscription) { await subscription.unsubscribe(); const reg = await navigator.serviceWorker.getRegistration(); @@ -74,6 +98,24 @@ export async function revokePermission(userId: string): Promise { console.log("No subscription found"); } + const appCheckToken = await getAppCheckToken().catch((error) => { + showSnackBar({ + message: error.message, + type: "warning", + }); + return ""; + }); + + if (!appCheckToken) { + showSnackBar({ + message: + "App Checkの初期化に失敗しました。debug tokenがサーバーに登録されていることを確認してください。", + type: "warning", + }); + return; + } + + // トークンをFirestoreから削除 const response = await fetch(`${functionsEndpoint}/user/${userId}`, { method: "PUT", headers: { diff --git a/src/utils/getAppCheckToken.ts b/src/utils/getAppCheckToken.ts new file mode 100644 index 00000000..981b48e8 --- /dev/null +++ b/src/utils/getAppCheckToken.ts @@ -0,0 +1,43 @@ +"use client"; +import { app } from "@/app/firebase"; +import { + getToken, + initializeAppCheck, + ReCaptchaV3Provider, +} from "firebase/app-check"; + +/** + * App Checkのトークンを生成する + * + * @export + * @return {*} {Promise} + */ +export default async function getAppCheckToken(): Promise { + try { + // 開発環境ならApp Checkを使用しない + if (process.env.NODE_ENV === "development") { + return "development"; + } + + // ステージング環境ならApp Checkのデバッグトークンを有効化 + if (process.env.NEXT_PUBLIC_IS_STAGING === "true") { + ( + window as unknown as { FIREBASE_APPCHECK_DEBUG_TOKEN: boolean } + ).FIREBASE_APPCHECK_DEBUG_TOKEN = true; + } + + const appCheck = initializeAppCheck(app, { + provider: new ReCaptchaV3Provider( + process.env.NEXT_PUBLIC_FIREBASE_RECAPTCHA_SITEKEY as string + ), + isTokenAutoRefreshEnabled: true, + }); + + const token = await getToken(appCheck); + return token.token; + } catch { + throw new Error( + "App Checkの初期化に失敗しました。debug tokenがサーバーに登録されていることを確認してください。" + ); + } +}