为什么在使用 Vue 和 Pinia 的 Ionic 中,Firebase 身份验证不会在刷新时持续存在?

Why is firebase authentication not persisting on refresh in Ionic with Vue and Pinia?

我一直在关注这里的代码:https://github.com/aaronksaunders/ionic-v6-firebase-tabs-auth

我遇到的问题是,当我使用 ionic serve 刷新页面并在网络浏览器中加载应用程序时,我的身份验证状态没有持续存在。

pinia 商店代码:

import { defineStore } from "pinia";
import { User } from "firebase/auth";
import {
  Profile,
  getProfile,
  setProfile,
} from "@/firebase/helpers/firestore/profileManager";
import { onSnapshot, Unsubscribe, doc } from "@firebase/firestore";
import { db } from "@/firebase/connectEmulators";
import { getAuth } from "@firebase/auth";
import {
  onAuthStateChanged,
  signInWithEmailAndPassword,
  signOut,
  createUserWithEmailAndPassword,
  updateProfile as updateAuthProfile,
} from "firebase/auth";
import errorHandler from "@/helpers/errorHandler";

/**@see {@link Profile} */
export enum UserType {
  DNE,
  uploader,
  checker,
  host,
}

interface State {
  user: User | null;
  profile: Profile | null;
  error: null | any;
  unsub: Unsubscribe | null;
}

export const useUserStore = defineStore("user", {
  state: (): State => {
    return {
      user: null,
      profile: null,
      error: null,
      unsub: null,
    };
  },
  getters: {
    isLoggedIn: (state) => state.user !== null,
    //DEV: do we need this to be a getter?
    userError: (state) => {
      if(state.error){
        switch (state.error.code) {
          case "auth/user-not-found":
            return "Email or Password incorrect!";
          case "auth/wrong-password":
            return "Email or Password incorrect!";
          default:
            return state.error;
        }
      }
      return null;
    },
    /**
     * @see Profile
     */
    getType: (state): UserType => {
      if (state.user === null) return UserType.DNE;
      if (!state.profile) return UserType.DNE;
      if (state.profile.locations.length > 0) return UserType.host;
      if (state.profile.queues.length > 0) return UserType.checker;
      return UserType.uploader;
    },
  },
  actions: {
    initializeAuthListener() {
      return new Promise((resolve) => {
        const auth = getAuth();
        onAuthStateChanged(auth, (user) => {
          console.log("AuthListener Initialized");
          if (user) {
            console.log("AuthListener: User exists!");
            this.user = user;

            getProfile(user.uid).then((profile) => {
              if (profile) {
                this.profile = profile;
                this.initializeProfileListener();
              } else {
                this.profile = null;
                if (this.unsub) this.unsub();
              }
            });
          } else {
            console.log("AuthListener: User does not exist!");
            this.user = null;
          }
          resolve(true);
        });
      });
    },
    /**
     *
     * @param email email for login
     * @param password password for login
     */
    async signInEmailPassword(email: string, password: string) {
      try {
        const auth = getAuth();
        const userCredential = await signInWithEmailAndPassword(
          auth,
          email,
          password
        );
        this.user = userCredential.user ? userCredential.user : null;
        this.error = null;
        return true;
      } catch (error: any) {
        console.log(typeof error.code);
        console.log(error.code);
        this.user = null;
        this.error = error;
        return false;
      }
    },
    async logoutUser() {
      try {
        const auth = getAuth();
        await signOut(auth);
        this.user = null;
        this.profile = null;
        this.error = null;
        if (this.unsub) this.unsub();
        return true;
      } catch (error: any) {
        this.error = error;
        return false;
      }
    },
    async createEmailPasswordAccount(
      email: string,
      password: string,
      userName: string,
      refSource: string
    ) {
      try {
        const auth = getAuth();
        const userCredential = await createUserWithEmailAndPassword(
          auth,
          email,
          password
        );
        //Add username to fireauth profile
        //DEV: test for xss vulnerabilities
        await updateAuthProfile(userCredential.user, { displayName: userName });

        //create user profile data in firestore
        let profile: Profile | undefined = new Profile(
          userCredential.user.uid,
          refSource
        );
        await setProfile(profile);
        profile = await getProfile(userCredential.user.uid);
        //set local store
        this.user = userCredential.user ? userCredential.user : null;
        this.profile = profile ? profile : null;
        this.error = null;
        //TODO: send email verification
        return true;
      } catch (error: any) {
        this.user = null;
        this.error = error;
        return false;
      }
    },
    initializeProfileListener() {
      try {
        if (!this.profile) errorHandler(Error("Profile not set in state!"));
        else {
          const uid = this.profile.uid;
          const unsub: Unsubscribe = onSnapshot(
            doc(db, "profiles", uid),
            (snapshot) => {
              const fbData = snapshot.data();
              if (!fbData)
                errorHandler(Error("Profile Listener snapshot.data() Null!"));
              else {
                const profile = new Profile(
                  snapshot.id,
                  fbData.data.referralSource
                );
                profile.data = fbData.data;
                profile.settings = fbData.settings;
                profile.locations = fbData.locations;
                profile.queues = fbData.queues;
                profile.checkers = fbData.checkers;
                profile.uploadHistory = fbData.uploadHistory;
                profile.hostLevel = fbData.hostLevel;
                this.profile = profile;
              }
            },
            (error) => {
              errorHandler(error);
            }
          );
          this.unsub = unsub;
        }
      } catch (error) {
        errorHandler(error as Error);
      }
    },
  },
});

main.ts 我在其中初始化身份验证侦听器:

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

import { IonicVue } from "@ionic/vue";

/* Core CSS required for Ionic components to work properly */
import "@ionic/vue/css/core.css";

/* Basic CSS for apps built with Ionic */
import "@ionic/vue/css/normalize.css";
import "@ionic/vue/css/structure.css";
import "@ionic/vue/css/typography.css";

/* Optional CSS utils that can be commented out */
import "@ionic/vue/css/padding.css";
import "@ionic/vue/css/float-elements.css";
import "@ionic/vue/css/text-alignment.css";
import "@ionic/vue/css/text-transformation.css";
import "@ionic/vue/css/flex-utils.css";
import "@ionic/vue/css/display.css";

/* Theme variables */
import "./theme/variables.css";

/* PWA elements for using Capacitor plugins */
import { defineCustomElements } from "@ionic/pwa-elements/loader";

/* Pinia used for state management */
import { createPinia } from "pinia";
import { useUserStore } from "./store/userStore";
const pinia = createPinia();

const app = createApp(App)
  .use(IonicVue, {
    // TODO: remove for production
    mode: process.env.VUE_APP_IONIC_MODE,
  })
  .use(pinia);

defineCustomElements(window);

//get the store
const store = useUserStore();

store.initializeAuthListener().then(() => {
  app.use(router);
});

router.isReady().then(() => {
  app.mount("#app");
});

我已经尝试重构 main.ts 以将应用程序安装在初始化 auth 侦听器的回调中,并且我已经尝试使我的代码与上述 main.ts 中的代码完全相同 link。都没有解决问题。

我也看了这里的问题: 答案中的大部分要点都不应该相关,因为我目前正在使用 firebase 模拟器来测试应用程序。

即便如此,我还是验证了我的 api 密钥是正确的。 当我启动应用程序时,我可以看到在浏览器中创建了 cookie,所以我认为它们被擦除不是问题。

理想情况下,我想避免在此处实现 @capacitor/storage,因为它没有必要。

我确实计划实施此库来处理 ios 和 android 的身份验证:https://github.com/baumblatt/capacitor-firebase-auth 但这不应该与应用程序的网络版本相关。

编辑: 意识到我遗漏了一段与该问题相关的代码。不知道我怎么没有复制过来。添加的代码是初始化配置文件侦听器函数。

我最终重构了 pinia 商店并解决了问题。我相信问题可能是由身份验证侦听器如何调用 initializeProfileListener 引起的。我在 auth 侦听器中没有代码来检查配置文件侦听器是否已经初始化,所以每次 authstate 更改或者它会初始化一个新的配置文件侦听器而不取消旧的。不过,我不确定这是导致问题的原因。

下面是可以正常运行的新代码。 pinia 商店:

import { defineStore } from "pinia";
import { User } from "firebase/auth";
import {
  Profile,
  getProfile,
  profileListener,
} from "@/firebase/helpers/firestore/profileManager";
import {
  fbCreateAccount,
  fbSignIn,
  fbAuthStateListener,
  fbSignOut,
} from "@/firebase/helpers/firestore/authHelper";
import {Unsubscribe} from "@firebase/firestore";
import errorHandler from "@/helpers/errorHandler";

/**@see {@link Profile} */
export enum UserType {
  DNE,
  uploader,
  checker,
  host,
}

interface State {
  user: User | null;
  profile: Profile | null;
  error: null | any;
  unsub: Unsubscribe | null;
}

export const useUserStore = defineStore("user", {
  state: (): State => {
    return {
      user: null,
      profile: null,
      error: null,
      unsub: null,
    };
  },
  getters: {
    isLoggedIn: (state) => state.user !== null,
    //DEV: do we need this to be a getter?
    userError: (state) => {
      if (state.error) {
        switch (state.error.code) {
          case "auth/user-not-found":
            return "Email or Password incorrect!";
          case "auth/wrong-password":
            return "Email or Password incorrect!";
          default:
            return state.error;
        }
      }
      return null;
    },
    /**
     * @see Profile
     */
    getType: (state): UserType => {
      if (state.user === null) return UserType.DNE;
      if (!state.profile) return UserType.DNE;
      if (state.profile.locations.length > 0) return UserType.host;
      if (state.profile.queues.length > 0) return UserType.checker;
      return UserType.uploader;
    },
  },
  actions: {
    initializeAuthListener() {
      return new Promise((resolve) => {
        fbAuthStateListener(async (user: any) => {
          if (user) {
            this.user = user;
            const profile = await getProfile(user.uid);
            if (profile) {
              this.profile = profile;
              //TODO: initialize profile listener
              if(this.unsub === null) {
                this.initializeProfileListener();
              }
            }
          }
          resolve(true);
        });
      });
    },
    /**
     *
     * @param email email for login
     * @param password password for login
     */
    async signInEmailPassword(email: string, password: string) {
      try {
        const userCredential = await fbSignIn(email, password);
        this.user = userCredential.user ? userCredential.user : null;
        this.error = null;
        return true;
      } catch (error: any) {
        console.log(typeof error.code);
        console.log(error.code);
        this.user = null;
        this.error = error;
        return false;
      }
    },
    async logoutUser() {
      try {
        await fbSignOut();
        this.user = null;
        this.profile = null;
        this.error = null;
        if (this.unsub) this.unsub();
        return true;
      } catch (error: any) {
        this.error = error;
        return false;
      }
    },
    async createEmailPasswordAccount(
      email: string,
      password: string,
      userName: string,
      refSource: string
    ) {
      try {
        const { user, profile } = await fbCreateAccount(
          email,
          password,
          userName,
          refSource
        );
        //set local store
        this.user = user ? user : null;
        this.profile = profile ? profile : null;
        this.error = null;
        //TODO: send email verification
        return true;
      } catch (error: any) {
        this.user = null;
        this.error = error;
        return false;
      }
    },
    initializeProfileListener() {
      try {
        if (this.user) {
          const unsub = profileListener(
            this.user?.uid,
            async (profile: any) => {
              if (profile) {
                this.profile = profile;
              }
            }
          );
          this.unsub = unsub;
        }
      } catch (error) {
        errorHandler(error as Error);
      }
    },
  },
});

authHelper.ts

import { auth } from "@/firebase/firebase";
import {
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signOut,
  onAuthStateChanged,
  updateProfile as updateAuthProfile,
} from "@firebase/auth";
import { Profile, setProfile, getProfile } from "./profileManager";

/**
 * @param email
 * @param password
 * @param userName
 * @param refSource @see profileManager
 * @returns
 */
export const fbCreateAccount = async (
  email: string,
  password: string,
  userName: string,
  refSource: string
) => {
  //DEBUG: creating a user works but throws an error.
  const userCredential = await createUserWithEmailAndPassword(
    auth,
    email,
    password
  );
  if (userCredential) {
    //add username to fireauth profile
    await updateAuthProfile(userCredential.user, { displayName: userName });
    //create user profile data in firestore
    let profile: Profile | undefined = new Profile(
      userCredential.user.uid,
      refSource
    );
    await setProfile(profile);
    profile = await getProfile(userCredential.user.uid);
    //TODO: errorHandling for setProfile and getProfile
    return {
      user: userCredential.user,
      profile: profile,
    };
  } else {
    return {
      user: null,
      profile: null,
    };
  }
};

/**
 *
 * @param email
 * @param password
 * @returns UserCredential {@link https://firebase.google.com/docs/reference/js/auth.usercredential.md?authuser=0#usercredential_interface}
 */
export const fbSignIn = async (email: string, password: string) => {
  const userCredential = signInWithEmailAndPassword(auth, email, password);
  //TODO: add call to add to profile signins array
  return userCredential;
};

export const fbSignOut = async () => {
  await signOut(auth);
  return true;
};

/**
 * @see {@link https://firebase.google.com/docs/reference/js/auth.md?authuser=0&hl=en#onauthstatechanged}
 * @param callback contains either user or null
 */
export const fbAuthStateListener = (callback: any) => {
  onAuthStateChanged(auth, (user) => {
    if (user) {
      //user is signed in
      callback(user);
    } else {
      //user is signed out
      callback(null);
    }
  });
};