パスワードリセット機能の全体像の理解
実装するコードの参考にした記事
リクエスト失敗の対応
パスワードリセット画面でなぜかPUTリクエストが成功しないのでdevise_token_authのupdateアクションの中身を確認。
リクエストの送信にはaccess-token,client,uidをヘッダーに持たせる必要があることを確認。
加えて、この時点でreset_password_tokenをupdateアクションに渡せていなかったことにも気づいた。
最終的なフォームとフックのコード
リセットパスワードフォーム
import { type FC } from 'react';
import useResetPassword from '../../features/auth/hooks/useResetPassword';
import Button from '../atoms/button';
import InputField from '../atoms/inputfield';
const ResetPasswordForm: FC = () => {
const {
password,
setPassword,
passwordConfirmation,
setPasswordConfirmation,
errorMessage,
handleResetPassword,
} = useResetPassword();
const onSubmit = async (event: React.FormEvent) => {
event.preventDefault();
await handleResetPassword();
};
return (
<form onSubmit={onSubmit}>
<InputField
type="password"
placeholder="新しいパスワード"
value={password}
onChange={(e) => {
setPassword(e.target.value);
}}
/>
<InputField
type="password"
placeholder="パスワード確認"
value={passwordConfirmation}
onChange={(e) => {
setPasswordConfirmation(e.target.value);
}}
/>
<Button type="submit" label="パスワードをリセット" />
{errorMessage !== null && errorMessage !== '' && (
<p className="error-message">{errorMessage}</p>
)}
</form>
);
};
export default ResetPasswordForm;
リセットパスワードフック
アクセストークンなどを持っているとルート保護をかけているページにもアクセスできてしまうので、reset_password_tokenを保持している時はアクセスできないようにしている。
そのため、パスワードリセットが成功したらそれらのトークンなどを削除する処理を加えている。
import { useState, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { PASSWORD_RESET_REQUEST_URL } from 'urls';
// ローカルストレージからトークンを取得し、ヘッダーに追加する関数
const getAuthHeaders = () => {
return {
'Content-Type': 'application/json',
'access-token': localStorage.getItem('access-token') ?? '',
client: localStorage.getItem('client') ?? '',
uid: localStorage.getItem('uid') ?? '',
};
};
const useResetPassword = (): {
password: string;
setPassword: (password: string) => void;
passwordConfirmation: string;
setPasswordConfirmation: (passwordConfirmation: string) => void;
errorMessage: string | null;
handleResetPassword: () => Promise<void>;
} => {
const [searchParams] = useSearchParams();
const [password, setPassword] = useState('');
const [passwordConfirmation, setPasswordConfirmation] = useState('');
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const navigate = useNavigate();
useEffect(() => {
// 認証メールからリダイレクトした際にURLからトークンなどの情報を取得し、ローカルストレージに保存
const accessToken = searchParams.get('access-token');
const client = searchParams.get('client');
const uid = searchParams.get('uid');
const resetPasswordToken = searchParams.get('token');
if (
accessToken !== null &&
client !== null &&
uid !== null &&
resetPasswordToken !== null
) {
localStorage.setItem('access-token', accessToken);
localStorage.setItem('client', client);
localStorage.setItem('uid', uid);
localStorage.setItem('reset_password_token', resetPasswordToken);
}
}, [searchParams]);
// パスワードリセット処理
const handleResetPassword = async () => {
setErrorMessage(null);
try {
const resetPasswordToken =
localStorage.getItem('reset_password_token') ?? '';
const response = await fetch(PASSWORD_RESET_REQUEST_URL, {
method: 'PUT',
headers: {
...getAuthHeaders(),
},
body: JSON.stringify({
password,
password_confirmation: passwordConfirmation,
reset_password_token: resetPasswordToken,
}),
});
if (response.ok) {
localStorage.removeItem('reset_password_token');
localStorage.removeItem('access-token');
localStorage.removeItem('client');
localStorage.removeItem('uid');
navigate('/reset-password-complete'); // リセット成功時の処理
} else {
setErrorMessage(
'パスワードリセットに失敗しました。もう一度お試しください。',
);
}
} catch (error) {
setErrorMessage('エラーが発生しました。もう一度お試しください。');
}
};
return {
password,
setPassword,
passwordConfirmation,
setPasswordConfirmation,
errorMessage,
handleResetPassword,
};
};
export default useResetPassword;
ルート保護
パスワードリセットをするまでルート保護にひっかかる設定を追加。
import { useContext, type ReactNode } from 'react';
import { Navigate } from 'react-router-dom';
import { AuthContext } from '../../../App';
interface ProtectedRouteProps {
children: ReactNode;
}
const useProtectedRoute = ({
children,
}: ProtectedRouteProps): ReactNode | null => {
const { loading, isSignedIn } = useContext(AuthContext);
const resetPasswordToken = localStorage.getItem('reset_password_token');
if (resetPasswordToken !== null) {
return <Navigate to="/reset-password" replace />;
}
if (!loading) {
if (isSignedIn) {
return children;
} else {
return <Navigate to="/" replace />;
}
} else {
return null; // ローディング中は何も表示しない
}
};
export default useProtectedRoute;