From 126bcae0760887d9b8b17d6ca609766af9eebb30 Mon Sep 17 00:00:00 2001 From: FUGAMARU Date: Sat, 21 Oct 2023 00:31:56 +0900 Subject: [PATCH] =?UTF-8?q?[Update]=20Firestore=E3=81=A8=E3=81=AE=E3=83=87?= =?UTF-8?q?=E3=83=BC=E3=82=BF=E3=83=BC=E3=81=AE=E5=90=8C=E6=9C=9F=E3=83=95?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=82=92=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spotify/SpotifyApiCallbackPage.tsx | 24 ++-- atoms/userDataAtom.ts | 7 + hooks/useAuth.tsx | 4 +- hooks/useInitializer.tsx | 12 +- hooks/useSetSpotifySettingState.tsx | 13 +- hooks/useSpotifyToken.tsx | 8 +- hooks/useStorage.tsx | 134 ++++++++++++------ hooks/useWebDAVSettingState.tsx | 8 +- types/UserData.ts | 2 +- ...13\347\231\272\343\203\241\343\203\242.md" | 2 +- 10 files changed, 129 insertions(+), 85 deletions(-) create mode 100644 atoms/userDataAtom.ts diff --git a/app/callback/spotify/SpotifyApiCallbackPage.tsx b/app/callback/spotify/SpotifyApiCallbackPage.tsx index 42ac7c6..258c89f 100644 --- a/app/callback/spotify/SpotifyApiCallbackPage.tsx +++ b/app/callback/spotify/SpotifyApiCallbackPage.tsx @@ -1,9 +1,12 @@ "use client" import { useSearchParams, useRouter } from "next/navigation" -import { memo, useCallback, useEffect, useRef } from "react" +import { memo, useEffect, useRef } from "react" +import { useRecoilValue } from "recoil" +import { userDataAtom } from "@/atoms/userDataAtom" import useErrorModal from "@/hooks/useErrorModal" import useSpotifyToken from "@/hooks/useSpotifyToken" +import { isDefined } from "@/utils/isDefined" const SpotifyApiCallbackPage = () => { const router = useRouter() @@ -11,21 +14,12 @@ const SpotifyApiCallbackPage = () => { const { getAccessToken } = useSpotifyToken({ initialize: false }) const hasApiCalledRef = useRef(false) const { showError } = useErrorModal() - - const handleGetAccessToken = useCallback( - async (code: string) => { - try { - await getAccessToken(code) - } catch (e) { - throw e - } - }, - [getAccessToken] - ) + const userData = useRecoilValue(userDataAtom) useEffect(() => { ;(async () => { - if (hasApiCalledRef.current) return + if (hasApiCalledRef.current || !isDefined(userData)) return + hasApiCalledRef.current = true try { @@ -34,14 +28,14 @@ const SpotifyApiCallbackPage = () => { const code = searchParams.get("code") if (code === null) throw new Error("パラメーターが指定されていません") - await handleGetAccessToken(code) + await getAccessToken(code) } catch (e) { showError(e) } finally { router.push("/connect?provider=spotify") } })() - }, [searchParams, router, handleGetAccessToken, showError]) + }, [searchParams, router, getAccessToken, showError, userData]) return null } diff --git a/atoms/userDataAtom.ts b/atoms/userDataAtom.ts new file mode 100644 index 0000000..8d17aef --- /dev/null +++ b/atoms/userDataAtom.ts @@ -0,0 +1,7 @@ +import { atom } from "recoil" +import { UserData } from "@/types/UserData" + +export const userDataAtom = atom({ + key: "userDataAtom", + default: undefined +}) diff --git a/hooks/useAuth.tsx b/hooks/useAuth.tsx index 81b2b26..84f5e68 100644 --- a/hooks/useAuth.tsx +++ b/hooks/useAuth.tsx @@ -12,7 +12,9 @@ import { FIRESTORE_USERDATA_COLLECTION_NAME } from "@/constants/Firestore" import { db, auth } from "@/utils/firebase" const useAuth = () => { - const { createNewHashedPassword, createNewUserDocument } = useStorage() + const { createNewHashedPassword, createNewUserDocument } = useStorage({ + initialize: false + }) const checkUserExists = useCallback(async (email: string) => { // fetchSignInMethodsForEmailを使っても空配列しか返ってこないのでFirestoreから該当UUIDのコレクションがあるかどうかで判定する diff --git a/hooks/useInitializer.tsx b/hooks/useInitializer.tsx index 6a00714..8f9d174 100644 --- a/hooks/useInitializer.tsx +++ b/hooks/useInitializer.tsx @@ -1,25 +1,23 @@ import { useEffect } from "react" -import { useAuthState } from "react-firebase-hooks/auth" import { useSetRecoilState } from "recoil" import useSetSpotifySettingState from "./useSetSpotifySettingState" import useSpotifyApi from "./useSpotifyApi" import useSpotifyToken from "./useSpotifyToken" +import useStorage from "./useStorage" import useSetWebDAVSettingState from "./useWebDAVSettingState" import { faviconIndexAtom } from "@/atoms/faviconIndexAtom" import { LOCAL_STORAGE_KEYS } from "@/constants/LocalStorageKeys" -import { auth } from "@/utils/firebase" import { generateRandomNumber } from "@/utils/randomNumberGenerator" const useInitializer = () => { /** MixJuiceを開いた時に実行したい、かつ表示に直接関係ない処理はこのフック内で行う TODO: ログインチェックなど */ - const [user, loading] = useAuthState(auth) - useSpotifyApi({ initialize: true }) useSpotifyToken({ initialize: true }) + useStorage({ initialize: true }) - useSetSpotifySettingState({ isLoadingUser: loading }) - useSetWebDAVSettingState({ isLoadingUser: loading }) + useSetSpotifySettingState() + useSetWebDAVSettingState() const setFaviconIndex = useSetRecoilState(faviconIndexAtom) useEffect(() => { @@ -39,8 +37,6 @@ const useInitializer = () => { ) } }, []) - - return { user, isLoadingUser: loading } as const } export default useInitializer diff --git a/hooks/useSetSpotifySettingState.tsx b/hooks/useSetSpotifySettingState.tsx index d10a009..638042c 100644 --- a/hooks/useSetSpotifySettingState.tsx +++ b/hooks/useSetSpotifySettingState.tsx @@ -1,4 +1,5 @@ import { useEffect } from "react" +import { useAuthState } from "react-firebase-hooks/auth" import { useRecoilValue, useSetRecoilState } from "recoil" import useStorage from "./useStorage" import { selectedSpotifyPlaylistsAtom } from "@/atoms/selectedSpotifyPlaylistsAtom" @@ -6,15 +7,13 @@ import { spotifySettingStateAtom } from "@/atoms/spotifySettingStateAtom" import { FIRESTORE_DOCUMENT_KEYS } from "@/constants/Firestore" import { LOCAL_STORAGE_KEYS } from "@/constants/LocalStorageKeys" import { LocalStorageSpotifySelectedPlaylists } from "@/types/LocalStorageSpotifySelectedPlaylists" +import { auth } from "@/utils/firebase" import { isDefined } from "@/utils/isDefined" -type Args = { - isLoadingUser: boolean -} - -const useSetSpotifySettingState = ({ isLoadingUser }: Args) => { +const useSetSpotifySettingState = () => { const selectedPlaylists = useRecoilValue(selectedSpotifyPlaylistsAtom) - const { getUserData } = useStorage() + const { getUserData } = useStorage({ initialize: false }) + const [, isLoadingUser] = useAuthState(auth) /** * done: Spotifyのログイン・プレイリストの選択が完了している @@ -26,7 +25,7 @@ const useSetSpotifySettingState = ({ isLoadingUser }: Args) => { ;(async () => { if (isLoadingUser) return - const refreshToken = await getUserData( + const refreshToken = getUserData( FIRESTORE_DOCUMENT_KEYS.SPOTIFY_REFRESH_TOKEN ) diff --git a/hooks/useSpotifyToken.tsx b/hooks/useSpotifyToken.tsx index 135565e..ea149b2 100644 --- a/hooks/useSpotifyToken.tsx +++ b/hooks/useSpotifyToken.tsx @@ -18,7 +18,9 @@ const useSpotifyToken = ({ initialize }: Props) => { /** 参考: https://developer.spotify.com/documentation/web-api/tutorials/code-pkce-flow */ const [accessToken, setAccessToken] = useRecoilState(spotifyAccessTokenAtom) // useStateを使うとSpotifyの設定画面を離れた場合にアクセストークンが消えるのでRecoilを使う - const { getUserData, updateUserData, deleteUserData } = useStorage() + const { getUserData, updateUserData, deleteUserData } = useStorage({ + initialize: false + }) /** 現在のアドレスからコールバック用のリダイレクトURIを求める */ const [redirectUri, setRedirectUri] = useState("") @@ -145,7 +147,7 @@ const useSpotifyToken = ({ initialize }: Props) => { console.log("🟦DEBUG: Spotify APIのアクセストークンを更新します") const clientId = localStorage.getItem(LOCAL_STORAGE_KEYS.SPOTIFY_CLIENT_ID) - const refreshToken = await getUserData( + const refreshToken = getUserData( FIRESTORE_DOCUMENT_KEYS.SPOTIFY_REFRESH_TOKEN ) @@ -198,7 +200,7 @@ const useSpotifyToken = ({ initialize }: Props) => { "Spotify APIのアクセストークンの更新に失敗しました。Spotifyに再ログインしてください。" ) } - }, [setAccessToken, deleteAuthConfig, getUserData, updateUserData]) + }, [setAccessToken, deleteAuthConfig, updateUserData, getUserData]) /* useMemoにすると、Date.nowがaccessTokenの取得が完了した時点で固定されるのでuseCallbackにする必要がある */ const hasValidAccessTokenState = useCallback(() => { diff --git a/hooks/useStorage.tsx b/hooks/useStorage.tsx index 5581e43..1957184 100644 --- a/hooks/useStorage.tsx +++ b/hooks/useStorage.tsx @@ -1,9 +1,11 @@ import CryptoJS from "crypto-js" import { setDoc, doc, getDoc, deleteField, updateDoc } from "firebase/firestore" -import { useCallback, useMemo } from "react" +import { useCallback, useEffect, useMemo } from "react" import { useAuthState } from "react-firebase-hooks/auth" +import { useRecoilState } from "recoil" import useErrorModal from "./useErrorModal" +import { userDataAtom } from "@/atoms/userDataAtom" import { FIRESTORE_USERDATA_COLLECTION_NAME, FIRESTORE_DOCUMENT_KEYS @@ -13,9 +15,12 @@ import { UserData, UserDataKey } from "@/types/UserData" import { auth, db } from "@/utils/firebase" import { isDefined } from "@/utils/isDefined" -const useStorage = () => { +type Args = { initialize: boolean } + +const useStorage = ({ initialize }: Args) => { const { showError } = useErrorModal() - const [user] = useAuthState(auth) + const [userData, setUserData] = useRecoilState(userDataAtom) + const [user, isLoadingUser] = useAuthState(auth) const decryptionVerifyString = useMemo( () => process.env.NEXT_PUBLIC_DECRYPTION_VERIFY_STRING, @@ -68,51 +73,15 @@ const useStorage = () => { [encryptText, decryptionVerifyString] ) - /** getUserDataやupdateUserなどの関数は、その関数の使用箇所で関数自体をtry/catchでラップするのが正しいのだろうが、コードが汚くなる気がするのでここで処理してしまうことにする */ - + /** ユーザーデーターを扱う時はupdateUserDataと同じような書き方で統一したいのであえてクラスのGetterっぽくしている */ const getUserData = useCallback( - async (key: UserDataKey) => { - try { - const email = user?.email - if (!isDefined(email)) - throw new Error( - "ログイン中ユーザーのメールアドレスを取得できませんでした" - ) - - if (!isDefined(decryptionVerifyString)) - throw new Error( - "データーの復号化検証に必要な環境変数 NEXT_PUBLIC_DECRYPTION_VERIFY_STRING が設定されていません。サーバー管理者にお問い合わせください。" - ) - - const userDataDocument = await getDoc( - doc(db, FIRESTORE_USERDATA_COLLECTION_NAME, email) - ) - - if (!userDataDocument.exists()) - throw new Error("ユーザーデーターが存在しません") - - if (!userDataDocument.data()?.[key]) return undefined // keyで指定されたフィールドが存在しない場合はundefinedを返す - - const encryptedUserData = userDataDocument.data() as UserData // TODO: withConverter使って型に強くしたい - const decryptedVerifyString = decryptText( - encryptedUserData[FIRESTORE_DOCUMENT_KEYS.DECRYPTION_VERIFY_STRING] - ) - - if (decryptionVerifyString !== decryptedVerifyString) - throw new Error( - "データーの復号化検証に失敗しました。再ログインしてください。" - ) // TODO: モーダルのボタン押したらログインフォームに飛ばされる独自例外に置き換える - - return decryptText(encryptedUserData[key] as string) - } catch (e) { - showError(e) - } - }, - [showError, decryptText, user, decryptionVerifyString] + (key: UserDataKey) => userData?.[key], + [userData] ) const updateUserData = useCallback( async (key: UserDataKey, value: string) => { + /** updateUserDataの使用箇所でupdateUserData自体をtry/catchしてしまうのが正しい実装なのだろうが、如何せん使用箇所が多くいちいちtry/cathcを書いているとコードが汚くなる気がするので例外処理はここで捌いてしまう */ try { const email = user?.email if (!isDefined(email)) @@ -133,11 +102,23 @@ const useStorage = () => { doc(db, FIRESTORE_USERDATA_COLLECTION_NAME, email), data ) + + if (!isDefined(userData)) + throw new Error("ユーザーデーターがundefinedです") + const updatedUserData = { ...userData, [key]: value } + setUserData(updatedUserData) } catch (e) { showError(e) } }, - [encryptText, showError, user, decryptionVerifyString] + [ + encryptText, + showError, + user, + decryptionVerifyString, + userData, + setUserData + ] ) const deleteUserData = useCallback( @@ -152,13 +133,76 @@ const useStorage = () => { await updateDoc(doc(db, FIRESTORE_USERDATA_COLLECTION_NAME, email), { [key]: deleteField() }) + + if (!isDefined(userData)) + throw new Error("ユーザーデーターがundefinedです") + const updatedUserData = { ...userData } + delete updatedUserData[key] + setUserData(updatedUserData) } catch (e) { showError(e) } }, - [showError, user] + [showError, user, userData, setUserData] ) + /** MixJuiceを起動した時にFirestoreのデーターをローカルのRecoilStateに取り込む */ + useEffect(() => { + if (!initialize || !isDefined(user) || isLoadingUser) return + ;(async () => { + try { + const email = user?.email + if (!isDefined(email)) + throw new Error( + "ログイン中ユーザーのメールアドレスを取得できませんでした" + ) + + if (!isDefined(decryptionVerifyString)) + throw new Error( + "データーの復号化検証に必要な環境変数 NEXT_PUBLIC_DECRYPTION_VERIFY_STRING が設定されていません。サーバー管理者にお問い合わせください。" + ) + + const userDataDocument = await getDoc( + doc(db, FIRESTORE_USERDATA_COLLECTION_NAME, email) + ) + + if (!userDataDocument.exists()) + throw new Error("ユーザーデーターが存在しません") + + const encryptedUserData = userDataDocument.data() as UserData // TODO: withConverter使って型に強くしたい + const decryptedUserData = Object.fromEntries( + Object.entries(encryptedUserData).map(([key, value]) => [ + key, + decryptText(value as string) + ]) + ) as unknown as UserData + + const decryptedVerifyString = + decryptedUserData[FIRESTORE_DOCUMENT_KEYS.DECRYPTION_VERIFY_STRING] + + if (decryptionVerifyString !== decryptedVerifyString) + throw new Error( + "データーの復号化検証に失敗しました。再ログインしてください。" + ) // TODO: モーダルのボタン押したらログインフォームに飛ばされる独自例外に置き換える + + setUserData(decryptedUserData) + console.log( + "🟩DEBUG: Firestore上のユーザーデータをRecoilStateに取り込みました" + ) + } catch (e) { + showError(e) + } + })() + }, [ + initialize, + user, + isLoadingUser, + setUserData, + showError, + decryptText, + decryptionVerifyString + ]) + return { createNewHashedPassword, createNewUserDocument, diff --git a/hooks/useWebDAVSettingState.tsx b/hooks/useWebDAVSettingState.tsx index 1afe930..29a473e 100644 --- a/hooks/useWebDAVSettingState.tsx +++ b/hooks/useWebDAVSettingState.tsx @@ -1,15 +1,15 @@ import { useEffect } from "react" +import { useAuthState } from "react-firebase-hooks/auth" import { useRecoilState, useSetRecoilState } from "recoil" import { selectedWebDAVFoldersAtom } from "@/atoms/selectedWebDAVFoldersAtom" import { webDAVAuthenticatedAtom } from "@/atoms/webDAVAuthenticatedAtom" import { webDAVSettingStateAtom } from "@/atoms/webDAVSettingStateAtom" import { LOCAL_STORAGE_KEYS } from "@/constants/LocalStorageKeys" +import { auth } from "@/utils/firebase" -type Args = { - isLoadingUser: boolean -} +const useSetWebDAVSettingState = () => { + const [, isLoadingUser] = useAuthState(auth) -const useSetWebDAVSettingState = ({ isLoadingUser }: Args) => { /** * done: WebDAVサーバーへのログイン・フォルダーの指定が完了している * setting: WebDAVサーバーへのログインは完了しているが、フォルダーの指定が完了していない diff --git a/types/UserData.ts b/types/UserData.ts index 3cbc912..929b97f 100644 --- a/types/UserData.ts +++ b/types/UserData.ts @@ -2,7 +2,7 @@ import { FIRESTORE_DOCUMENT_KEYS } from "@/constants/Firestore" export interface UserData { [FIRESTORE_DOCUMENT_KEYS.DECRYPTION_VERIFY_STRING]: string - [FIRESTORE_DOCUMENT_KEYS.SPOTIFY_REFRESH_TOKEN]?: string + [FIRESTORE_DOCUMENT_KEYS.SPOTIFY_REFRESH_TOKEN]?: string | undefined // TODO: フィールド追加する } diff --git "a/\351\226\213\347\231\272\343\203\241\343\203\242.md" "b/\351\226\213\347\231\272\343\203\241\343\203\242.md" index e5f1202..9c00474 100644 --- "a/\351\226\213\347\231\272\343\203\241\343\203\242.md" +++ "b/\351\226\213\347\231\272\343\203\241\343\203\242.md" @@ -11,4 +11,4 @@ ## usePlayerで扱っているステートを全てグローバルステート化してpropsによるバケツリレーをやめることができない理由 -・メインページ以外でusePlayerを利用する際、イベントリスナーが重複することによる二重処理などが行われないようにするため、引数には`{initialize: false}`を渡すことになるが、`initialize`が`false`の場合、usePlayer配下のフック(例: useSpotifyPlayer)の引数も`{initialize: false}`になるので、usePlayer配下のフックのuseEffectも作動しなくなり、再生位置の更新などがされなくなる。 +メインページ以外でusePlayerを利用する際、イベントリスナーが重複することによる二重処理などが行われないようにするため、引数には`{initialize: false}`を渡すことになるが、`initialize`が`false`の場合、usePlayer配下のフック(例: useSpotifyPlayer)の引数も`{initialize: false}`になるので、usePlayer配下のフックのuseEffectも作動しなくなり、再生位置の更新などがされなくなる。