为什么在使用 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);
}
});
};
我一直在关注这里的代码: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);
}
});
};