Nextjs+ChakraUi+FirebaseAuth+react-hook-formで作るログイン画面(その3:PWリセット編)
概要
今回は、パスワードの再設定機能を説明します。
その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