Почему аутентификация Firebase не сохраняется при обновлении в Ionic с Vue и Pinia?

Я обычно следую этому коду здесь: https://github.com/aaronksaunders/ionic-v6-firebase-tabs-auth

Проблема, с которой я сталкиваюсь, заключается в том, что мое состояние аутентификации не сохраняется, когда я обновляю страницу при использовании ionic serve и загружаю приложение в веб-браузере.

код для магазина пиния:

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, чтобы смонтировать приложение внутри обратного вызова для инициализации прослушивателя аутентификации, и я попытался сделать свой код точно таким же, как код в main.ts по приведенной выше ссылке. Ни один не решил проблему.

Я также посмотрел на вопрос здесь: https://stackoverflow.com/a/67774186/9230780 Большинство пунктов в ответе не должны быть связаны, потому что в настоящее время я использую эмуляторы firebase для тестирования приложения.

Тем не менее, я проверил правильность моего ключа API. Я вижу, что файлы cookie создаются в браузере, когда я запускаю приложение, поэтому я не думаю, что проблема в том, что они стираются.

В идеале я бы хотел избежать реализации @capacitor/storage здесь, потому что в этом нет необходимости.

Я планирую реализовать эту библиотеку для обработки аутентификации для ios и android: https://github.com/baumblatt/capacitor-firebase-auth но это не должно относиться к веб-версии приложения.

Редактировать: Понял, что мне не хватает части кода, относящейся к вопросу. Не знаю, как я его не скопировал. Добавленный код — это функция прослушивания профиля инициализации.

Есть ли здесь условие гонки? Вы вызываете onAuthStateChanged после того, как состояние аутентификации уже восстановлено?

Daniel Storey 22.04.2022 20:40

существует ли ошибка в исходном коде, который я разместил?

Aaron Saunders 23.04.2022 18:18

@AaronSaunders Ошибка отсутствует в исходном коде. Я извлек состояние on authstate, измененное в отдельную функцию в другом файле, как в вашем коде, и это устранило большую часть проблемы. Я считаю, что проблема вызвана «this.initializeProfileListener()», которая является ссылкой на функцию, которая вызывает прослушиватель снимков для обновления данных профиля всякий раз, когда они обновляются в хранилище. Я работаю над тем, чтобы понять, почему это вызывает проблему, но я не уверен.

Nathan Goren 25.04.2022 15:14
3 метода стилизации элементов HTML
3 метода стилизации элементов HTML
Когда дело доходит до применения какого-либо стиля к нашему HTML, существует три подхода: встроенный, внутренний и внешний. Предпочтительным обычно...
Формы c голосовым вводом в React с помощью Speechly
Формы c голосовым вводом в React с помощью Speechly
Пытались ли вы когда-нибудь заполнить веб-форму в области электронной коммерции, которая требует много кликов и выбора? Вас попросят заполнить дату,...
Стилизация и валидация html-формы без использования JavaScript (только HTML/CSS)
Стилизация и валидация html-формы без использования JavaScript (только HTML/CSS)
Будучи разработчиком веб-приложений, легко впасть в заблуждение, считая, что приложение без JavaScript не имеет права на жизнь. Нам становится удобно...
Flatpickr: простой модуль календаря для вашего приложения на React
Flatpickr: простой модуль календаря для вашего приложения на React
Если вы ищете пакет для быстрой интеграции календаря с выбором даты в ваше приложения, то библиотека Flatpickr отлично справится с этой задачей....
В чем разница между Promise и Observable?
В чем разница между Promise и Observable?
Разберитесь в этом вопросе, и вы значительно повысите уровень своей компетенции.
Что такое cURL в PHP? Встроенные функции и пример GET запроса
Что такое cURL в PHP? Встроенные функции и пример GET запроса
Клиент для URL-адресов, cURL, позволяет взаимодействовать с множеством различных серверов по множеству различных протоколов с синтаксисом URL.
0
3
59
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В итоге я провел рефакторинг магазина pinia, и это решило проблему. Я полагаю, что проблема могла быть вызвана тем, как прослушиватель аутентификации вызвал InitializeProfileListener. У меня не было кода в прослушивателе авторизации, чтобы проверить, был ли уже инициализирован прослушиватель профиля, поэтому каждый раз, когда состояние аутентификации менялось, или он инициализировал новый прослушиватель профиля, не отменяя старый. Я не совсем уверен, что это было причиной проблемы.

Ниже приведен новый код, который работает правильно. Магазин Пиния:

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);
    }
  });
};

Другие вопросы по теме