Firebaseに作成したアプリにGoogle ID登録したユーザーの退会処理ためのReactのコードを解説

ユーザーの退会 プログラミング

Firebaseに作成したReactのアプリにGoogle IDで登録したユーザーの退会処理にフォーカスを当てて解説します。

※ ソースはこちら:Github

※ アプリはこちら:「Firebaseに認証するだけのアプリ」

※ パッケージのバージョン:”firebase”: “^9.22.1″、”react”: “^18.2.0″ (詳細はGithubのpackage.jsonを参照してください。)

※ Firebaseの認証周り全般の解説はこちらを参照してください。

神谷
神谷

プログラムの多くはそうなんですが、「後始末」に類する処理は結構大変なんです。

ということでサインアップ処理よりもユーザーの退会処理の方がいろいろ大変。

メールアドレスとパスワードでユーザー登録したユーザーの退会処理よりもGoogle IDで登録したユーザの退会処理は大変でした。

ホーム画面

ホーム画面は、

  • Home.js(メインの画面)
  • HomeChild.js(ボタンをクリックしたときに出力)

によって構成されています。

// Home.js

import { signOut } from "firebase/auth";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
import { createPortal } from "react-dom";
import { HomePasswordChangeModal, HomeWithdrawalChild } from "./HomeChild";
import { auth } from "../service/firebase/firebase";
import { useAuthContext } from "../context/AuthContext";

import Title from "./Title";
import { Button } from "@mui/material";
import { Logout } from "@mui/icons-material";
import PersonRemoveIcon from "@mui/icons-material/PersonRemove";
import PublishedWithChangesIcon from '@mui/icons-material/PublishedWithChanges';

const ModalPortal = ({ children }) => {
  const target = document.querySelector(".modalContainer");
  return createPortal(children, target);
};

const Home = () => {
  const [withdrawalModalOpen, setWithdrawalModalOpen] = useState(false);
  const [passwordChangeModalOpen, setPasswordChangeModalOpen] = useState(false);
  const navigate = useNavigate();
  const { user } = useAuthContext();
  const handleLogout = () => {
    signOut(auth);
    navigate("/login");
  };

  return (
    <>
      <div>
        <Title />
        <div className="modalContainer"></div>
        <p>現在ログインしているユーザーの情報</p>
        <p>email : {user.email}</p>
        <p>
          Provider :
          {user.providerData[0].providerId === "password"
            ? "email Login"
            : user.providerData[0].providerId === "google.com"
            ? "Google"
            : "invalid provider"}
        </p>
        <p>
          {!user.emailVerified &&
            "アドレス未認証のためTodoは入力できません。(メール認証が終了したら、リロードしてください。)"}
        </p>
          <Button
            onClick={handleLogout}
            variant="outlined"
            startIcon={<Logout />}
            sx={{ mr:1 }}
          >
            ログアウト
          </Button>
          <Button
            onClick={() => setWithdrawalModalOpen(true)}
            variant="outlined"
            startIcon={<PersonRemoveIcon />}
            sx={{ mr:1 }}
          >
            退会
          </Button>
          {user.providerData[0].providerId === "password"
          && <Button onClick={() => setPasswordChangeModalOpen(true)}
            variant="outlined"
            startIcon={<PublishedWithChangesIcon />}
          >
            パスワード変更
          </Button> }
      </div>
      {withdrawalModalOpen && (
        <ModalPortal>
          <HomeWithdrawalChild
            setWithdrawalModalOpen={setWithdrawalModalOpen}
          />
        </ModalPortal>
      )}
      {passwordChangeModalOpen && (
        <ModalPortal>
          <HomePasswordChangeModal
            setPasswordChangeModalOpen={setPasswordChangeModalOpen}
          />
        </ModalPortal>
      )}
    </>
  );
};

export default Home;

// HomeChild.js
import { signOut } from "firebase/auth";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
import { createPortal } from "react-dom";
import { HomePasswordChangeModal, HomeWithdrawalChild } from "./HomeChild";
import { auth } from "../service/firebase/firebase";
import { useAuthContext } from "../context/AuthContext";

import Title from "./Title";
import { Button } from "@mui/material";
import { Logout } from "@mui/icons-material";
import PersonRemoveIcon from "@mui/icons-material/PersonRemove";
import PublishedWithChangesIcon from '@mui/icons-material/PublishedWithChanges';

const ModalPortal = ({ children }) => {
  const target = document.querySelector(".modalContainer");
  return createPortal(children, target);
};

const Home = () => {
  const [withdrawalModalOpen, setWithdrawalModalOpen] = useState(false);
  const [passwordChangeModalOpen, setPasswordChangeModalOpen] = useState(false);
  const navigate = useNavigate();
  const { user } = useAuthContext();
  const handleLogout = () => {
    signOut(auth);
    navigate("/login");
  };

  return (
    <>
      <div>
        <Title />
        <div className="modalContainer"></div>
        <p>現在ログインしているユーザーの情報</p>
        <p>email : {user.email}</p>
        <p>
          Provider :
          {user.providerData[0].providerId === "password"
            ? "email Login"
            : user.providerData[0].providerId === "google.com"
            ? "Google"
            : "invalid provider"}
        </p>
        <p>
          {!user.emailVerified &&
            "アドレス未認証のためTodoは入力できません。(メール認証が終了したら、リロードしてください。)"}
        </p>
          <Button
            onClick={handleLogout}
            variant="outlined"
            startIcon={<Logout />}
            sx={{ mr:1 }}
          >
            ログアウト
          </Button>
          <Button
            onClick={() => setWithdrawalModalOpen(true)}
            variant="outlined"
            startIcon={<PersonRemoveIcon />}
            sx={{ mr:1 }}
          >
            退会
          </Button>
          {user.providerData[0].providerId === "password"
          && <Button onClick={() => setPasswordChangeModalOpen(true)}
            variant="outlined"
            startIcon={<PublishedWithChangesIcon />}
          >
            パスワード変更
          </Button> }
      </div>
      {withdrawalModalOpen && (
        <ModalPortal>
          <HomeWithdrawalChild
            setWithdrawalModalOpen={setWithdrawalModalOpen}
          />
        </ModalPortal>
      )}
      {passwordChangeModalOpen && (
        <ModalPortal>
          <HomePasswordChangeModal
            setPasswordChangeModalOpen={setPasswordChangeModalOpen}
          />
        </ModalPortal>
      )}
    </>
  );
};

export default Home;

ホーム画面の退会ボタン

Google IDでユーザー登録したユーザーのホーム画面
Google IDで登録したユーザーの退会処理

ホーム(メイン)画面に「退会」ボタンを作成しました。

// Home.jsから抜粋
~
          <Button
            onClick={() => setWithdrawalModalOpen(true)}
            variant="outlined"
            startIcon={<PersonRemoveIcon />}
            sx={{ mr:1 }}
          >
            退会
          </Button>
~

「退会」ボタンをクリックすると、withdrawalModalOpenがsetWithdrawalModalOpenによってtrueになり、退会のためのモーダル画面が表示されます。

「退会」のためのモーダル画面出力

「退会」のモーダル画面には、退会を確認するためのボタンがあります。

「本当に退会する」ボタンをクリックすると、退会処理が起動します。

Google IDで登録したユーザーを削除する画面
Google IDで登録したユーザーを削除する画面
// HomeChild.js抜粋
export const HomeWithdrawalChild = ({ setWithdrawalModalOpen }) => {
  const navigate = useNavigate();
  const user = auth.currentUser;

  const handleDelUserClick = () => {
    delUser().then((res) => {
      setWithdrawalModalOpen(false);
      res && user.providerData[0].providerId === "password"
        ? navigate("/SignUp")
        : navigate("/Login");
    });
  };
  return (
    <div className="modal">
      <div className="modal__content">
        <p>本当に退会しますか?</p>
        <div>
          <button type="button" onClick={handleDelUserClick}>
            本当に退会する
          </button>
        </div>
        <div>
          <button type="button" onClick={() => setWithdrawalModalOpen(false)}>
            やっぱり退会しない
          </button>
        </div>
      </div>
    </div>
  );
};

「本当に退会する」ボタンをクリックすると、handleDelUserClick()の中で、delUser()がコールされます。

delUser()で退会のためにアプリからのユーザー削除処理が行われ、成功すると、サインアップ画面に移行します。

退会処理:アプリからGoogle IDで登録したユーザーを削除する

アプリからGoogle IDで登録したユーザーを削除する処理を以下に解説します。

delUser.js:ユーザー削除処理のモジュール

// delUser.js
import {
  deleteUser,
} from "firebase/auth";
import { auth, googleProvider } from "../firebase/firebase";
import getCredential from "../firebase/getCredential";
import { issueMsg } from "../common/issueMsg";
import loginFirebase from "../firebase/loginFirebase";

export const delUser = async () => {
  const user = auth.currentUser;
  let loginProvider;

  try {
    // ここで、ログインしているプロバイダーを判断する
    if (user.providerData[0].providerId === "password") {
      loginProvider = "email";
    } else if (user.providerData[0].providerId === "google.com") {
      loginProvider = "google";
      /******* Google認証の退会処理開始 *******/
      // 退会用のログイン
      const currentLogonUserEmail = user.email;
      issueMsg(
        "ログインしているGoogle IDで再ログインしてください",
        currentLogonUserEmail
      );
      try {
        // ポップアップウインドウが表示
        // await signInWithPopup(auth, googleProvider);
        await loginFirebase("google",googleProvider);
        const newUser = auth.currentUser;
        const newUserEmail = newUser.email;
        await deleteUser(newUser);
        if (newUserEmail !== currentLogonUserEmail) {
          issueMsg(
            "再ログインしたGoogleIdが異なります。再度ログインして退会処理をやり直してください。誤ログインEmail:",
            newUserEmail
          );
          return false;
        }
        issueMsg("退会処理が完了しました。:", newUser.email);
        return true;
      } catch (error) {
        if (error.code === "auth/popup-closed-by-user") {
          issueMsg(
            "退会処理のためのGoogleIdでのログインがキャンセルされました。"
          );
          return false;
        } else {
          console.log(error.code);
          issueMsg(error.message);
          return false;
        }
      }
      /******  Google認証の退会処理終了 *******/
    } else {
      issueMsg(
        "退会処理で不当なプロバイダーが指定された。",
        user.providerData[0].providerId
      );
      return false;
    }
    const result = await getCredential(loginProvider, user);
    if (!result) {
      return false;
    }
    await deleteUser(user);
    issueMsg("退会処理が完了しました。:", user.email);
    return true;
  } catch (error) {
    if (error.code === "auth/wrong-password") {
      issueMsg("入力したパスワードが不正です。");
    } else {
      issueMsg(`退会処理で不正が発生しました。`, error.code);
    }
    console.log(`退会失敗`);
    console.log(user);
    console.log(error);
    console.log(error.message);
    console.log(error.code);
    return false;
  }
};

delUser.jsにはメールとパスワードで登録したユーザーとGoogle IDでユーザー登録したユーザーのユーザー削除処理両方が含まれています。

アプリからユーザーを削除するのに、単にdeleteUser()を発行すると、「auth/requires-recent-login」というエラーでFirebaseに怒られます。

意味は、

Thrown if the user’s last sign-in time does not meet the security threshold. Use firebase.User.reauthenticateWithCredential to resolve. This does not apply if the user is anonymous.

「ユーザの最終サインイン時刻がセキュリティ閾値を満たしていない場合にスローされます。firebase.User.reauthenticateWithCredential を使用して解決します。ユーザが匿名の場合は適用されません。」

英文は、Firebaseのドキュメント、和文はDeepL翻訳で和訳しました。

これはどういうことかというと、下記の理由からです。

アカウントの削除、メインのメールアドレスの設定、パスワードの変更といったセキュリティ上重要な操作を行うには、ユーザーが最近ログインしている必要があります。

『Firebase でユーザーを管理する』から引用

簡単に言うと、ログインしてからある程度時間がたつと再度ログインするか、ユーザーの再認証をしないと「アカウントの削除、メインのメールアドレスの設定、パスワードの変更」等はエラーになるよ、ということです。

ということで、削除(deleteUser())する前に再認証処理というものが必要になります。

Google IDで登録したユーザーの再認証処理

Google IDで登録したユーザーの場合は、ユーザーに退会処理の前に再度同じGoogle IDでログインしてもらい、その後ユーザ情報を削除しました。

// delUser.js抜粋
~ 
     const currentLogonUserEmail = user.email;
      issueMsg(
        "ログインしているGoogle IDで再ログインしてください",
        currentLogonUserEmail
      );
~

まず、現在ログオンしているGoogle IDに対応するemailアドレスをユーザ情報から抽出。

そして、ポップアップメッセージにそのemailアドレスを表示。

Google IDでログインしているユーザーのメールアドレス表示
再ログインするためのメールアドレスの表示

再認証のための再ログインのポップアップ表示をします。

// delUser.js抜粋
~  
       await loginFirebase("google",googleProvider);
~

※ loginFirebaseの処理については、こちの記事の「Google IDでログインするときの処理」を参照してください。

Google IDでログインするためのポップアップ
Google IDで再ログインするためのポップアップ

そして、現在ログインしているGoogle IDで再ログインすると、Google IDでユーザー登録したユーザーの削除が行われます。

// delUser.js抜粋
~ 
        const newUserEmail = newUser.email;
        await deleteUser(newUser);
~
Google IDでユーザー登録したユーザーの削除処理完了
Google IDでユーザー登録したユーザーの削除完了
神谷
神谷

メールアドレスとパスワードでユーザー登録したユーザーと同じように、reauthenticateWithCredential()によって再認証しようとしたのですが、いろいろ調べてもGoogle IDで登録したユーザーでの再認証処理を作りこむことができませんでした。

ということで、ちょっとみっともないのですが、ログインのポップアップを再表示し、再ログオンすることによって、再認証させました。

最後に

Google IDでユーザー登録したユーザーの削除には苦労しました。

reauthenticateWithCredential()によって再認証ってできると思って、いろいろ調べて、いろいろテストしてみたんですが、出来ませんでした。

Google IDでユーザー登録したユーザーの削除の部分のコードが汚いのもそれが理由です。

もしも、このコードを参考するのなら、このあまりきれいでない部分は修正してください。

当該Reactのプロジェクトのsrc配下のファイル構造

以下が、当該Reactのプロジェクトのsrc配下のファイル構造です。

SRC
│  App.css
│  App.js
│  App.test.js
│  index.css
│  index.js
│  reportWebVitals.js
│  service-worker.js
│  serviceWorkerRegistration.js
│  setupTests.js
│
├─components
│  │  Home.js
│  │  HomeChild.js
│  │  Login.js
│  │  PrivateRoute.js
│  │  PublicRoute.js
│  │  SignUp.js
│  │  Title.js
│  │
│  └─styles
│          portal.css
│
├─context
│      AuthContext.js
│
└─service
    ├─authenticationProcess
    │      changePassword.js
    │      delUser.js
    │
    ├─common
    │      issueMsg.js
    │
    └─firebase
            firebase.js
            getCredential.js
            loginFirebase.js
            registration.js
            sendEmailLink.js
            submitPasswordResetEmail.js

このブログに掲載されているコードは、著作権者(私)に帰属します。コードを自由に使用、複製、改変、配布、販売、サブライセンスできます。また、コードを使用した結果、何らかの損害が発生した場合でも、著作権者は一切責任を負いません。

タイトルとURLをコピーしました