🍺

Nextjs+ChakraUi+FirebaseAuth+react-hook-formで作るログイン画面(その3:PWリセット編)

2024/05/13に公開

概要

今回は、パスワードの再設定機能を説明します。
その1:ロジック編
その2:コンポーネント編
その3:パスワードリセット編 🔗今ここ
その4:ソーシャルログイン編(作成中)

sign-in.tsxの全体コード

ここをクリックしてコードを表示
// react, next
import { useRouter } from "next/router";
import NextLink from 'next/link';
// 3rd party
import {
    Box,
    HStack,
    Link,
    Stack,
} from '@chakra-ui/react';
import {
    Auth,
    getAuth,
    signInWithEmailAndPassword,
} from "firebase/auth";
import { FirebaseError } from 'firebase/app';
import { useForm, SubmitHandler } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
// ganymede
import { Layout } from "../../components/Layout";
import { AccountSubmitButton } from '../../components/atoms/Button';
import { FormCard } from '../../components/organisms/Card';
import { SocialAuth } from "../../components/organisms/SocialAuth";
import { ModalAlert } from "../../components/organisms/Modal";
import { FormPasswordInputControl, FormTextInputControl } from '../../components/organisms/Form';
import { firebaseApp } from '../../firebase/firebase';
import { useErrorHandler } from "../../hooks/useModalHandler";
import { signinSchema } from '../../validation/validationSchemas';


type InputFormData = {
    email: string;
    password: string;
};


const SignIn = () => {
    const router = useRouter();
    const { isOpen, onClose, errorMessage, errorHandler } = useErrorHandler();

    const formDefaultValues: InputFormData = {
        email: '',
        password: '',
    };
    const {
        register,
        handleSubmit,
        reset,
        formState: { errors },
    } = useForm({
        mode: 'all',
        defaultValues: formDefaultValues,
        resolver: yupResolver(signinSchema),
    });

    const onSubmit: SubmitHandler<InputFormData> = async (data) => {
        try {
            const auth: Auth = getAuth(firebaseApp);
            await signInWithEmailAndPassword(auth, data.email, data.password);
            router.push('/');
        } catch (e: unknown) {
            const error = e instanceof FirebaseError ? e : e as Error;
            errorHandler(error);
            reset({ email: '', password: '' });
        }
    };

    return (
        <Layout title="Sign In">
            <FormCard title="サインイン">
                <form onSubmit={handleSubmit(onSubmit)} data-testid="sign-in-form">
                    <Stack spacing={4}>
                        <FormTextInputControl
                            id="email"
                            isInvalid={errors.email ? true : false}
                            placeholder="email"
                            registers={register('email')}           // registerの引数はInputDataのkey
                            errorMessage={errors.email && errors.email.message}
                        >
                            Email
                        </FormTextInputControl>
                        <Box>
                            <FormPasswordInputControl
                                id="password"
                                isInvalid={errors.password ? true : false}
                                placeholder="password"
                                registers={register('password')}    // registerの引数はInputDataのkey
                                errorMessage={errors.password && errors.password.message}
                            >
                                パスワード
                            </FormPasswordInputControl>
                            <Box mt={2}>
                                <Link as ={NextLink} href='/account/pw-reset-mail' color={'blue.400'}>
                                    パスワードをお忘れですか?
                                </Link>
                            </Box>
                        </Box>
                        <HStack alignItems="center" justifyContent="end">
                            <AccountSubmitButton>Sign In</AccountSubmitButton>
                        </HStack>
                        <ModalAlert
                            isOpen={isOpen}
                            modalTitle="Error"
                            onClose={onClose}
                        >
                            {errorMessage}
                        </ModalAlert>
                    </Stack>
                    <Box mt={12}>
                        <SocialAuth>
                            Sign in
                        </SocialAuth>
                    </Box>
                </form>
            </FormCard>
        </Layout>
    );
};


export default SignIn;

パスワード再設定のフロー

パスワードの再設定は、sign-in.tsxの中では"パスワードをお忘れですか?"の部分のみであるが、別の画面に飛ぶのでフローが重要になります。パスワード再設定のフローは以下のようになります。

シーケンス図のPlantUMLコード
@startuml
autonumber

skinparam sequence {
    BoxBorderColor #lightgray
}

actor user as user
box Next.jsのClientサイド #FAFFFF
    participant "sign-in.tsx" as signin #DDEEFF
    participant "<size:11>pw-reset-mail.tsx" as reset #DDEEFF
    participant "<size:11>mail-sended-info\n<size:11>/[mode].tsx" as sended #DDEEFF
end box
box
end box

box Cloud #FFFAFA
    participant "Firebase" as fb #FFD700
end box

user -> signin
user <-- signin
user -> reset:"パスワードをお忘れですか?"\nクリック
user <-- reset:Email入力画面表示
user -> reset:Email入力 &\n送信ボタンクリック
reset -> fb:[firebase-api]sendPasswordResetEmail
reset <-- fb
reset -> sended:redirect
user <-- sended:メール送信済み通知
||60||
user <-- fb: Email(リンク付き)
user -> fb:リンククリック
user <-- fb:パスワード入力画面(Popup)
user -> fb:パスワード入力
user <-- fb:入力完了画面(Popup)
@enduml

これだけだとなんのこっちゃって感じだと思いますので、説明します。
まず、登場人物は、

  • sign-in.tsx (サインイン画面)
  • pw-reset-email.tsx (リセット用Email入力画面)
  • mail-sended-info/[mode].tsx (送信済み通知画面)
  • firebase

拡張子tsxが付いているのはNext.jsのモジュール名(ファイル名)で、それぞれクライアントサイドで動作します。当然ながらfirebaseはクラウドにあります。
処理の流れは、このようになります。

  • 1-2: サインイン画面を表示します。
  • 3-4:"パスワードをお忘れですか?"のリンクをクリックすると、Emailの入力画面を表示します。
  • 5-9:Emailを入力して送信すると、APIでfirebaseにパスワードのリセットの指示を行い、画面がredirectされます。

sign-in.tsxの関係部分の説明

パスワードの再設定が関係するコードは、sign-in.tsxではここだけです。これは単純にリンクを貼っているだけです。

    <Link as={NextLink} href='/account/pw-reset-mail' color={'blue.400'}>
        パスワードをお忘れですか?
    </Link>

pw-reset-email.tsxの説明

"パスワードをお忘れですか?"のリンクを踏むと表示されるEmailの入力画面です。

🔖画像

ページの画像はこのようになります。

🔖全体コード

ここをクリックしてコードを表示
// react, next
import { useRouter } from "next/router";
// 3rd party
import {
    Stack,
    HStack,
} from '@chakra-ui/react';
import { getAuth, sendPasswordResetEmail } from 'firebase/auth';
import { useForm, SubmitHandler } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { FirebaseError } from 'firebase/app';
// ganymede
import { Layout } from '../../components/Layout';
import { firebaseApp } from '../../firebase/firebase';
import { FormTextInputControl } from '../../components/organisms/Form';
import { AccountSubmitButton } from '../../components/atoms/Button';
import { FormCard } from '../../components/organisms/Card';
import { ModalAlert } from '../../components/organisms/Modal'
import { useErrorHandler } from '../../hooks/useModalHandler';
import { emailSchema } from '../../validation/validationSchemas';

type InputFormData = {
    email: string;
};

const PasswordResetMail = () => {
    const router = useRouter();
    const { isOpen, onClose, errorMessage, errorHandler } = useErrorHandler();

    const formDefaultValues: InputFormData = {
        email: '',
    };
    const {
        register,
        handleSubmit,
        formState: { errors },
    } = useForm({
        mode: 'all',
        defaultValues: formDefaultValues,
        resolver: yupResolver(emailSchema),
    });

    const currentPageMessage = 'ご登録されたメールアドレスを入力してください。\nパスワードリセット用のメールを送付いたします。';

    const onSubmit: SubmitHandler<{email: string}> = async ({email}) => {
        const auth = getAuth(firebaseApp);
        try {
            await sendPasswordResetEmail(auth, email)
            const mode = 'reset-pw';
            router.push(`/account/mail-sended-info/${mode}`);
        } catch (e: unknown) {
            const error = e instanceof FirebaseError ? e : e as Error;
            errorHandler(error)
        }
    };

    return (
        <Layout title="Reset Password">
            <FormCard title="パスワードの再設定">
                <form onSubmit={handleSubmit(onSubmit)}>
                    <Stack spacing={2}>
                        <FormTextInputControl
                            id="email"
                            isInvalid={errors.email ? true : false}
                            placeholder="email"
                            registers={register('email')}           // registerの引数はInputDataのkey
                            errorMessage={errors.email && errors.email.message}
                        >
                            {currentPageMessage}
                        </FormTextInputControl>
                        <HStack alignItems="center" justifyContent="end">
                            <AccountSubmitButton>Email 送信</AccountSubmitButton>
                        </HStack>
                        <ModalAlert
                            isOpen={isOpen}
                            modalTitle='Email送信エラー'
                            onClose={onClose}
                        >
                            {errorMessage}
                        </ModalAlert>
                    </Stack>
                </form>
            </FormCard>
        </Layout>
    )
}


export default PasswordResetMail

🔖コードの解説

コードは殆どsign-in.tsxと同じです。ポイントはfirebaseのAPIのsendPasswordResetEmailの部分です。それ以外についての説明は割愛します。
sendPasswordRestEmailが使用されているのはonSubmit関数です。

    const onSubmit: SubmitHandler<{email: string}> = async ({email}) => {
        try {
            await sendPasswordResetEmail(auth, email)
            const mode = 'reset-pw';
            router.push(`/account/mail-sended-info/${mode}`);
        } catch (e: unknown) {
            const error = e instanceof FirebaseError ? e : e as Error;
            errorHandler(error)
        }
    };
  • sendPasswordResetEmailは、firebaseのパスワードを再設定するAPIです。これを実行するとfirebaseから引数のemail宛にパスワードをリセット用のメールを送信します。
  • メールを送信したら/account/mail-sended-info/rest-pwにページ遷移します。
  • 送信されたメールのリンクをクリックすると、クライアント側のブラウザに以下のPopup画面が表示されます。この画面はfirebaseで作成されるため作成する必要がありません。
  • 新しいパスワードの部分にパスワードを入力すればパスワードリセットの処理は完了します。

🔖送信メールの書式設定

パスワード再設定の送信メールの書式はある程度変更することが可能です。

  • Firebaseのコンソール画面にログインします。
  • 左側のサイドバーからAuthenticationを選択
  • 画面上部のテンプレートタブを選択
  • パスワードの再設定を選択

🔖APP_NAMEの変更

テンプレートの%APP_NAME%を変更するには、

  • Firebaseのコンソール画面を開く
  • 画面左上にある歯車アイコンをクリック
  • "プロジェクトの設定"をクリック
  • "公開設定"の"公開名"の鉛筆アイコンをクリックして名前を変更

mail-sended-info/[mode].tsxの説明

Emailを入力して送信した後に表示される連絡画面です。
このPageコンポーネントは、再利用できるようにパスパラメータ(mode)を利用できるようにしています。
特に説明は必要ないかと思いますので、画像と全体コードを掲載しておきます。

全体のコードは以下のようになります。

ここをクリックしてmail-sended-info/[mode].tsxのコードを表示

パスワードリセットの機能の説明はこれで完了です。

Discussion