0
0

devise_token_authを用いたパスワードリセット機能の実装

Posted at

パスワードリセット機能の全体像の理解

実装するコードの参考にした記事

リクエスト失敗の対応

パスワードリセット画面でなぜか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;

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0