From 141b4ef986d8aff83b6b81ed9a63712abb0d0b39 Mon Sep 17 00:00:00 2001 From: FUGAMARU Date: Fri, 10 Nov 2023 00:12:59 +0900 Subject: [PATCH] =?UTF-8?q?[Add]=20=E3=83=91=E3=82=B9=E3=83=AF=E3=83=BC?= =?UTF-8?q?=E3=83=89=E5=A4=89=E6=9B=B4=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/parts/ConfirmationModal.tsx | 4 +- .../parts/GetLatestAuthCredentialModal.tsx | 89 ------- components/parts/InputModal.tsx | 108 +++++++++ .../templates/MainPage/SettingModal.tsx | 219 +++++++++--------- .../templates/SigninPage/ForgotPassword.tsx | 1 - components/templates/SigninPage/Signin.tsx | 11 +- hooks/useAuth.tsx | 10 +- hooks/useSettingModal.tsx | 168 ++++++++++++++ hooks/useStorage.tsx | 4 +- 9 files changed, 392 insertions(+), 222 deletions(-) delete mode 100644 components/parts/GetLatestAuthCredentialModal.tsx create mode 100644 components/parts/InputModal.tsx create mode 100644 hooks/useSettingModal.tsx diff --git a/components/parts/ConfirmationModal.tsx b/components/parts/ConfirmationModal.tsx index cc72b2b..f4d55d2 100644 --- a/components/parts/ConfirmationModal.tsx +++ b/components/parts/ConfirmationModal.tsx @@ -5,7 +5,6 @@ import { Children } from "@/types/Children" type Props = { isOpen: boolean - title: string confirmButtonText: string cancelButtonText: string onConfirm: () => void @@ -14,7 +13,6 @@ type Props = { const ConfirmationModal = ({ isOpen, - title, children, confirmButtonText, cancelButtonText, @@ -26,7 +24,7 @@ const ConfirmationModal = ({ size="md" opened={isOpen} onClose={onCancel} - title={title} + title="確認" centered styles={{ title: { color: STYLING_VALUES.TEXT_COLOR_DEFAULT, fontWeight: 700 } diff --git a/components/parts/GetLatestAuthCredentialModal.tsx b/components/parts/GetLatestAuthCredentialModal.tsx deleted file mode 100644 index 4cc7b35..0000000 --- a/components/parts/GetLatestAuthCredentialModal.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Modal, Stack, Group, Button, PasswordInput } from "@mantine/core" -import { AuthCredential, EmailAuthProvider } from "firebase/auth" -import { memo, useCallback, useState, KeyboardEvent, useMemo } from "react" -import { useAuthState } from "react-firebase-hooks/auth" -import { STYLING_VALUES } from "@/constants/StylingValues" -import { auth } from "@/utils/firebase" -import { isDefined } from "@/utils/isDefined" - -type Props = { - isOpen: boolean - onExecute: (authCredential: AuthCredential) => Promise - onCancel: () => void -} - -const GetLatestAuthCredentialModal = ({ - isOpen, - onExecute, - onCancel -}: Props) => { - const [userInfo] = useAuthState(auth) - const [isProcessing, setIsProcessing] = useState(false) - const [passwordInput, setPasswordInput] = useState("") - const isValidPassword = useMemo( - () => passwordInput.length >= 6, // 6文字以上なのはFirebaseの仕様 - [passwordInput] - ) - - const handleExecuteButtonClick = useCallback(async () => { - try { - setIsProcessing(true) - if (!isDefined(userInfo) || !isDefined(userInfo.email)) return - - const authCredential = await EmailAuthProvider.credential( - userInfo.email, - passwordInput - ) - if (!isDefined(authCredential)) return - - await onExecute(authCredential) - } finally { - setIsProcessing(false) - } - }, [passwordInput, userInfo, onExecute]) - - const handlePasswordKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.nativeEvent.isComposing || e.key !== "Enter" || !isValidPassword) - return - handleExecuteButtonClick() - }, - [handleExecuteButtonClick, isValidPassword] - ) - - return ( - - - setPasswordInput(e.currentTarget.value)} - onKeyDown={handlePasswordKeyDown} - /> - - - - - - - ) -} - -export default memo(GetLatestAuthCredentialModal) diff --git a/components/parts/InputModal.tsx b/components/parts/InputModal.tsx new file mode 100644 index 0000000..16ca077 --- /dev/null +++ b/components/parts/InputModal.tsx @@ -0,0 +1,108 @@ +import { + Modal, + Stack, + Group, + Button, + PasswordInput, + Input +} from "@mantine/core" +import { memo, useCallback, useState, KeyboardEvent, useMemo } from "react" +import { STYLING_VALUES } from "@/constants/StylingValues" + +type Props = { + isOpen: boolean + type: "email" | "password" + title: string + confirmButtonText: string + onConfirm: (inputValue: string) => Promise | void + onCancel: () => void +} + +const InputModal = ({ + isOpen, + type, + title, + confirmButtonText, + onConfirm, + onCancel +}: Props) => { + const [isProcessing, setIsProcessing] = useState(false) + const [inputValue, setInputValue] = useState("") + + const isConfirmButtonDisabled = useMemo(() => { + switch (type) { + case "email": + return /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(inputValue) + case "password": + return inputValue.length < 6 // 6文字以上なのはFirebaseの仕様 + } + }, [inputValue, type]) + + const handleConfirmButtonClick = useCallback(async () => { + try { + setIsProcessing(true) + await onConfirm(inputValue) + } finally { + setIsProcessing(false) + } + }, [inputValue, onConfirm]) + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if ( + e.nativeEvent.isComposing || + e.key !== "Enter" || + isConfirmButtonDisabled + ) + return + handleConfirmButtonClick() + }, + [handleConfirmButtonClick, isConfirmButtonDisabled] + ) + + return ( + + + {type === "email" ? ( + setInputValue(e.currentTarget.value)} + onKeyDown={handleKeyDown} + /> + ) : ( + setInputValue(e.currentTarget.value)} + onKeyDown={handleKeyDown} + /> + )} + + + + + + + + ) +} + +export default memo(InputModal) diff --git a/components/templates/MainPage/SettingModal.tsx b/components/templates/MainPage/SettingModal.tsx index 0a4b5c7..9b94b02 100644 --- a/components/templates/MainPage/SettingModal.tsx +++ b/components/templates/MainPage/SettingModal.tsx @@ -9,27 +9,21 @@ import { Switch, Text } from "@mantine/core" -import { useDisclosure, useLocalStorage } from "@mantine/hooks" +import { useLocalStorage } from "@mantine/hooks" + import { FirebaseError } from "firebase/app" -import { - AuthCredential, - deleteUser, - reauthenticateWithCredential -} from "firebase/auth" -import { memo, useCallback, useMemo, useState } from "react" +import { memo, useMemo } from "react" import { useAuthState } from "react-firebase-hooks/auth" import { IoSettingsOutline } from "react-icons/io5" import { PiUserCircleThin } from "react-icons/pi" import ConfirmationModal from "@/components/parts/ConfirmationModal" -import GetLatestAuthCredentialModal from "@/components/parts/GetLatestAuthCredentialModal" + +import InputModal from "@/components/parts/InputModal" import ModalDefault from "@/components/parts/ModalDefault" import { LOCAL_STORAGE_KEYS } from "@/constants/LocalStorageKeys" -import { PAGE_PATH } from "@/constants/PagePath" import { DEFAULT_SETTING_VALUES, SETTING_ITEMS } from "@/constants/Settings" -import useAuth from "@/hooks/useAuth" import useBreakPoints from "@/hooks/useBreakPoints" -import useStorage from "@/hooks/useStorage" -import useTransit from "@/hooks/useTransit" +import useSettingModal from "@/hooks/useSettingModal" import { SettingValues } from "@/types/DefaultSettings" import { auth } from "@/utils/firebase" import { isDefined } from "@/utils/isDefined" @@ -42,105 +36,33 @@ type Props = { const SettingModal = ({ isOpen, onClose }: Props) => { const { breakPoint, setRespVal } = useBreakPoints() const [userInfo] = useAuthState(auth) - const { signOut } = useAuth() - const { deleteUserData } = useStorage({ initialize: false }) - const { onTransit } = useTransit() const [settings, setSettings] = useLocalStorage({ key: LOCAL_STORAGE_KEYS.SETTINGS, defaultValue: DEFAULT_SETTING_VALUES }) - const [ + const { + isProcessingSignout, + handleSignoutButtonClick, + showSimpleError, + reAuth, isConfirmationDeleteUserModalOpen, - { - open: onOpenConfirmationDeleteUserModal, - close: onCloseConfirmationDeleteUserModal - } - ] = useDisclosure(false) - const [ - isGetLatestAuthCredentialModalOpen, - { - open: onOpenGetLatestAuthCredentialModal, - close: onCloseGetLatestAuthCredentialModal - } - ] = useDisclosure(false) - - const [isProcessingSignout, setIsProcessingSignout] = useState(false) - const handleSignoutButtonClick = useCallback(async () => { - setIsProcessingSignout(true) - await signOut() - onClose() - await onTransit(PAGE_PATH.MAIN_PAGE, PAGE_PATH.SIGNIN_PAGE) - }, [signOut, onClose, onTransit]) - - const executeDeleteUser = useCallback( - async (latestUserCredential: AuthCredential) => { - try { - if (!isDefined(userInfo)) return - - const newUserCredential = await reauthenticateWithCredential( - userInfo, - latestUserCredential - ) - if (!isDefined(newUserCredential.user.email)) return - - await deleteUser(newUserCredential.user) - await deleteUserData(newUserCredential.user.email) - await onTransit(PAGE_PATH.MAIN_PAGE, PAGE_PATH.SIGNIN_PAGE) - } catch (e) { - if (e instanceof FirebaseError) { - switch ( - e.code // TODO: エラーコード対応拡充?? (https://firebase.google.com/docs/reference/js/v8/firebase.FirebaseError#code) - ) { - case "auth/invalid-login-credentials": - alert( - "サインインに失敗しました。パスワードが間違っている可能性があります。" - ) // TODO: showErrorを使うとz-indexを指定しても最前面に表示されないので暫定対応 - break - default: - console.log("🟥ERROR: ", e) - alert("何らかの原因でサインインに失敗しました") // TODO: showErrorを使うとz-indexを指定しても最前面に表示されないので暫定対応 - } - } - } - }, - [userInfo, deleteUserData, onTransit] - ) - - const handleConfirmPassword = useCallback( - async (nextAction: "deleteUser" | "changePassword" | "changeEmail") => { - switch (nextAction) { - case "deleteUser": - onCloseConfirmationDeleteUserModal() - onOpenGetLatestAuthCredentialModal() - break - case "changePassword": - // TODO: パスワード変更処理 - break - case "changeEmail": - // TODO: メールアドレス変更処理 - break - } - }, - [onOpenGetLatestAuthCredentialModal, onCloseConfirmationDeleteUserModal] - ) - - const handleCancelGetLatestAuthCredentialModal = useCallback( - async (action: "deleteUser" | "changePassword" | "changeEmail") => { - switch (action) { - case "deleteUser": - onCloseGetLatestAuthCredentialModal() - onOpenConfirmationDeleteUserModal() - break - case "changePassword": - // TODO: パスワード変更処理 - break - case "changeEmail": - // TODO: メールアドレス変更処理 - break - } - }, - [onCloseGetLatestAuthCredentialModal, onOpenConfirmationDeleteUserModal] - ) + onOpenConfirmationDeleteUserModal, + onCloseConfirmationDeleteUserModal, + isConfirmationChangePasswordModalOpen, + onOpenConfirmationChangePasswordModal, + onCloseConfirmationChangePasswordModal, + isInputModalForDeleteUserOpen, + onOpenInputModalForDeleteUser, + onCloseInputModalForDeleteUser, + isInputCurrentPasswordModalForChangePasswordOpen, + onOpenInputCurrentPasswordModalForChangePassword, + onCloseInputCurrentPasswordModalForChangePassword, + isInputAfterPasswordModalForChangePasswordOpen, + onOpenInputAfterPasswordModalForChangePassword, + onCloseInputAfterPasswordModalForChangePassword, + handleConfirmForDeleteUser, + handleConfirmForChangePassword + } = useSettingModal({ onCloseModal: onClose }) const footerFunctions = useMemo(() => { if (breakPoint === "SmartPhone") { @@ -150,7 +72,12 @@ const SettingModal = ({ isOpen, onClose }: Props) => { - @@ -176,7 +103,12 @@ const SettingModal = ({ isOpen, onClose }: Props) => { - ) - }, [breakPoint, onOpenConfirmationDeleteUserModal]) + }, [ + breakPoint, + onOpenConfirmationDeleteUserModal, + onOpenConfirmationChangePasswordModal + ]) return ( <> @@ -286,19 +222,72 @@ const SettingModal = ({ isOpen, onClose }: Props) => { handleConfirmPassword("deleteUser")} + onConfirm={() => { + onCloseConfirmationDeleteUserModal() + onOpenInputModalForDeleteUser() + }} onCancel={onCloseConfirmationDeleteUserModal} > アカウントに紐づいているユーザーデーターは一度削除すると復元できません。アカウントを削除してもよろしいですか? - handleCancelGetLatestAuthCredentialModal("deleteUser")} + { + onCloseConfirmationChangePasswordModal() + onOpenInputCurrentPasswordModalForChangePassword() + }} + onCancel={onCloseConfirmationChangePasswordModal} + > + パスワードを変更するとSpotifyやWebDAVサーバーの接続設定が再度必要になります。よろしいですか? + + + { + onCloseInputModalForDeleteUser() + onOpenConfirmationDeleteUserModal() + }} + /> + + { + try { + await reAuth(password) + onCloseInputCurrentPasswordModalForChangePassword() + onOpenInputAfterPasswordModalForChangePassword() + } catch (e) { + if (e instanceof FirebaseError) showSimpleError(e) + } + }} + onCancel={() => { + onCloseInputCurrentPasswordModalForChangePassword() + onOpenConfirmationChangePasswordModal() + }} + /> + + { + onCloseInputAfterPasswordModalForChangePassword() + onOpenConfirmationChangePasswordModal() + }} /> ) diff --git a/components/templates/SigninPage/ForgotPassword.tsx b/components/templates/SigninPage/ForgotPassword.tsx index fad0c61..8bcf96a 100644 --- a/components/templates/SigninPage/ForgotPassword.tsx +++ b/components/templates/SigninPage/ForgotPassword.tsx @@ -124,7 +124,6 @@ const ForgotPassword = ({ className, isDisplay, onBack }: Props) => { { const { showError } = useErrorModal() const { checkUserExists, signIn } = useAuth() - const { - getCurrentUserData, - createHashedPassword, - setDecryptionVerifyString - } = useStorage({ initialize: false }) + const { getCurrentUserData, setHashedPassword, setDecryptionVerifyString } = + useStorage({ initialize: false }) const { getSettingState: getSpotifySettingState } = useSpotifySettingState({ initialize: false }) @@ -59,7 +56,7 @@ const Signin = ({ className, isDisplay, slideTo }: Props) => { return } - createHashedPassword(passwordInput) + setHashedPassword(passwordInput) await setDecryptionVerifyString(emailInput) const userData = await getCurrentUserData(emailInput) @@ -92,7 +89,7 @@ const Signin = ({ className, isDisplay, slideTo }: Props) => { getCurrentUserData, getSpotifySettingState, getWebDAVSettingState, - createHashedPassword, + setHashedPassword, setDecryptionVerifyString ]) diff --git a/hooks/useAuth.tsx b/hooks/useAuth.tsx index 6c8ed90..bca07fa 100644 --- a/hooks/useAuth.tsx +++ b/hooks/useAuth.tsx @@ -12,7 +12,7 @@ import { FIRESTORE_USERDATA_COLLECTION_NAME } from "@/constants/Firestore" import { db, auth } from "@/utils/firebase" const useAuth = () => { - const { createHashedPassword, createNewUserDocument } = useStorage({ + const { setHashedPassword, createNewUserDocument } = useStorage({ initialize: false }) @@ -31,7 +31,7 @@ const useAuth = () => { password ) - createHashedPassword(password) // 渡されたパスワードをハッシュ化し、LocalStorageに保存、以後共通鍵として使う + setHashedPassword(password) // 渡されたパスワードをハッシュ化し、LocalStorageに保存、以後共通鍵として使う return userCredential } catch (e) { @@ -50,7 +50,7 @@ const useAuth = () => { } } }, - [createHashedPassword] + [setHashedPassword] ) const signOut = useCallback(async () => { @@ -70,12 +70,12 @@ const useAuth = () => { await sendEmailVerification(userCredential.user) /** 渡されたパスワードをハッシュ化し、LocalStorageに保存、以後共通鍵として使う */ - createHashedPassword(password) + setHashedPassword(password) /** 復号化検証用のテキストを初期データーとしてユーザーのコレクションを新規作成する */ await createNewUserDocument(email) }, - [createNewUserDocument, createHashedPassword] + [createNewUserDocument, setHashedPassword] ) return { checkUserExists, signUp, signIn, signOut } as const diff --git a/hooks/useSettingModal.tsx b/hooks/useSettingModal.tsx new file mode 100644 index 0000000..a4d8686 --- /dev/null +++ b/hooks/useSettingModal.tsx @@ -0,0 +1,168 @@ +import { useDisclosure } from "@mantine/hooks" +import { FirebaseError } from "firebase/app" +import { + EmailAuthProvider, + reauthenticateWithCredential, + deleteUser, + updatePassword +} from "firebase/auth" +import { useCallback, useState } from "react" +import { useAuthState } from "react-firebase-hooks/auth" +import useAuth from "./useAuth" +import useStorage from "./useStorage" +import useTransit from "./useTransit" +import { PAGE_PATH } from "@/constants/PagePath" +import { auth } from "@/utils/firebase" +import { isDefined } from "@/utils/isDefined" + +type Args = { + onCloseModal: () => void +} + +const useSettingModal = ({ onCloseModal }: Args) => { + const [userInfo] = useAuthState(auth) + const { deleteUserData, deleteAllUserData } = useStorage({ + initialize: false + }) + const { signOut } = useAuth() + const { onTransit } = useTransit() + + const [isProcessingSignout, setIsProcessingSignout] = useState(false) + const handleSignoutButtonClick = useCallback(async () => { + setIsProcessingSignout(true) + await signOut() + onCloseModal() + await onTransit(PAGE_PATH.MAIN_PAGE, PAGE_PATH.SIGNIN_PAGE) + }, [signOut, onCloseModal, onTransit]) + + const showSimpleError = useCallback((e: FirebaseError) => { + switch ( + e.code // TODO: エラーコード対応拡充?? (https://firebase.google.com/docs/reference/js/v8/firebase.FirebaseError#code) + ) { + case "auth/invalid-login-credentials": + alert( + "サインインに失敗しました。パスワードが間違っている可能性があります。" + ) // TODO: useErrorModalのshowErrorを使うとz-indexを指定しても最前面に表示されないのでwindow.alertにて暫定対応 + break + default: + console.log("🟥ERROR: ", e) + alert("何らかの原因でサインインに失敗しました") // TODO: useErrorModalのshowErrorを使うとz-indexを指定しても最前面に表示されないのでwindow.alertにて暫定対応 + } + }, []) + + const reAuth = useCallback( + async (password: string) => { + if (!isDefined(userInfo) || !isDefined(userInfo.email)) return + + const authCredential = EmailAuthProvider.credential( + userInfo.email, + password + ) + + return await reauthenticateWithCredential(userInfo, authCredential) + }, + [userInfo] + ) + + /** 操作確認モーダルの表示に関する状態管理 */ + const [ + isConfirmationDeleteUserModalOpen, + { + open: onOpenConfirmationDeleteUserModal, + close: onCloseConfirmationDeleteUserModal + } + ] = useDisclosure(false) + const [ + isConfirmationChangePasswordModalOpen, + { + open: onOpenConfirmationChangePasswordModal, + close: onCloseConfirmationChangePasswordModal + } + ] = useDisclosure(false) + + /** ログイン情報の入力を受け付けるモーダルの表示に関する状態管理 */ + const [ + isInputModalForDeleteUserOpen, + { + open: onOpenInputModalForDeleteUser, + close: onCloseInputModalForDeleteUser + } + ] = useDisclosure(false) + const [ + isInputCurrentPasswordModalForChangePasswordOpen, + { + open: onOpenInputCurrentPasswordModalForChangePassword, + close: onCloseInputCurrentPasswordModalForChangePassword + } + ] = useDisclosure(false) + const [ + isInputAfterPasswordModalForChangePasswordOpen, + { + open: onOpenInputAfterPasswordModalForChangePassword, + close: onCloseInputAfterPasswordModalForChangePassword + } + ] = useDisclosure(false) + + /** ログイン情報を入力した後に実際の処理を行う関数 */ + const handleConfirmForDeleteUser = useCallback( + async (password: string) => { + try { + const newUserCredential = await reAuth(password) + if ( + !isDefined(newUserCredential) || + !isDefined(newUserCredential.user.email) + ) + return + + await deleteUser(newUserCredential.user) + await deleteUserData(newUserCredential.user.email) + await onTransit(PAGE_PATH.MAIN_PAGE, PAGE_PATH.SIGNIN_PAGE) + } catch (e) { + if (e instanceof FirebaseError) showSimpleError(e) + } + }, + [deleteUserData, onTransit, showSimpleError, reAuth] + ) + + const handleConfirmForChangePassword = useCallback( + async (newPassword: string) => { + try { + if (!isDefined(userInfo) || !isDefined(userInfo.email)) return + + await updatePassword(userInfo, newPassword) + await deleteAllUserData(userInfo.email) + await signOut() + await onTransit(PAGE_PATH.MAIN_PAGE, PAGE_PATH.SIGNIN_PAGE) + } catch (e) { + if (e instanceof FirebaseError) showSimpleError(e) + } + }, + [deleteAllUserData, showSimpleError, userInfo, onTransit, signOut] + ) + + return { + isProcessingSignout, + handleSignoutButtonClick, + showSimpleError, + reAuth, + isConfirmationDeleteUserModalOpen, + onOpenConfirmationDeleteUserModal, + onCloseConfirmationDeleteUserModal, + isConfirmationChangePasswordModalOpen, + onOpenConfirmationChangePasswordModal, + onCloseConfirmationChangePasswordModal, + isInputModalForDeleteUserOpen, + onOpenInputModalForDeleteUser, + onCloseInputModalForDeleteUser, + isInputCurrentPasswordModalForChangePasswordOpen, + onOpenInputCurrentPasswordModalForChangePassword, + onCloseInputCurrentPasswordModalForChangePassword, + isInputAfterPasswordModalForChangePasswordOpen, + onOpenInputAfterPasswordModalForChangePassword, + onCloseInputAfterPasswordModalForChangePassword, + handleConfirmForDeleteUser, + handleConfirmForChangePassword + } as const +} + +export default useSettingModal diff --git a/hooks/useStorage.tsx b/hooks/useStorage.tsx index efd00d4..750b0c9 100644 --- a/hooks/useStorage.tsx +++ b/hooks/useStorage.tsx @@ -57,7 +57,7 @@ const useStorage = ({ initialize }: Args) => { return CryptoJS.AES.decrypt(cipherText, key).toString(CryptoJS.enc.Utf8) }, []) - const createHashedPassword = useCallback((password: string) => { + const setHashedPassword = useCallback((password: string) => { const hash = CryptoJS.SHA256(password).toString() localStorage.setItem(LOCAL_STORAGE_KEYS.DATA_DECRYPTION_KEY, hash) }, []) @@ -254,7 +254,7 @@ const useStorage = ({ initialize }: Args) => { ]) return { - createHashedPassword, + setHashedPassword, createNewUserDocument, setDecryptionVerifyString, userData,