import { signInWithEmailAndPassword, signInWithPopup, GoogleAuthProvider, signOut as firebaseSignOut, onAuthStateChanged, getAuth, createUserWithEmailAndPassword, } from "firebase/auth"; import { getApp, getApps, initializeApp } from "firebase/app"; import type { User } from "firebase/auth"; export const useAuth = () => { const user = useState("auth-user", () => null); const isLoading = useState("auth-loading", () => true); const error = useState("auth-error", () => null); const firebaseReady = useState("firebase-ready", () => false); const listenerRegistered = useState( "auth-listener-registered", () => false ); const backendUser = useState | null>( "auth-backend-user", () => null ); const config = useRuntimeConfig(); const ensureFirebaseApp = () => { if (!process.client) { return null; } if (!firebaseReady.value) { const firebaseConfig = { apiKey: config.public.firebaseApiKey, authDomain: config.public.firebaseAuthDomain, projectId: config.public.firebaseProjectId, storageBucket: config.public.firebaseStorageBucket, messagingSenderId: config.public.firebaseMessagingSenderId, appId: config.public.firebaseAppId, ...(config.public.firebaseMeasurementId ? { measurementId: config.public.firebaseMeasurementId } : {}), }; if (getApps().length === 0) { initializeApp(firebaseConfig); } firebaseReady.value = true; } try { return getApp(); } catch (err) { console.error("[useAuth] Failed to get Firebase app instance:", err); return null; } }; const getAuthInstance = () => { const app = ensureFirebaseApp(); return app ? getAuth(app) : null; }; const authenticateWithBackend = async (idToken: string) => { try { const response = await $fetch<{ token?: string; user?: Record | null; }>("/auth/login", { baseURL: config.public.backendUrl, method: "POST", headers: { "Content-Type": "application/json", }, body: { idToken, }, }); backendUser.value = response?.user ?? null; return response; } catch (err) { console.error("Backend authentication failed:", err); backendUser.value = null; throw err; } }; const syncBackendWithToken = async (tokenProvider: () => Promise) => { try { const idToken = await tokenProvider(); await authenticateWithBackend(idToken); } catch (err) { console.warn("[useAuth] Failed to sync backend session", err); } }; const registerListener = () => { if (!process.client || listenerRegistered.value) { return; } try { const auth = getAuthInstance(); if (auth) { const existingUser = auth.currentUser; if (existingUser && !user.value) { user.value = existingUser; isLoading.value = false; syncBackendWithToken(() => existingUser.getIdToken()); } listenerRegistered.value = true; onAuthStateChanged(auth, (firebaseUser) => { user.value = firebaseUser; isLoading.value = false; if (firebaseUser) { syncBackendWithToken(() => firebaseUser.getIdToken()); } else { backendUser.value = null; } }); } } catch (err) { console.error("[useAuth] Failed to initialize auth listener:", err); isLoading.value = false; } }; const initAuth = () => { registerListener(); }; // Eagerly register listener when composable is used in client context if (process.client) { registerListener(); } // Sign in with email and password const signInWithEmail = async (email: string, password: string) => { try { error.value = null; isLoading.value = true; const auth = getAuthInstance(); if (!auth) { throw new Error("Firebase not initialized"); } const userCredential = await signInWithEmailAndPassword( auth, email, password ); const idToken = await userCredential.user.getIdToken(); try { await authenticateWithBackend(idToken); } catch (backendErr) { console.warn( "[useAuth] Backend authentication failed after email login:", backendErr ); } user.value = userCredential.user; registerListener(); return userCredential.user; } catch (err: any) { error.value = err.message; throw err; } finally { isLoading.value = false; } }; // Sign in with Google const signInWithGoogle = async () => { try { error.value = null; isLoading.value = true; const auth = getAuthInstance(); if (!auth) { throw new Error("Firebase not initialized"); } const provider = new GoogleAuthProvider(); const userCredential = await signInWithPopup(auth, provider); const idToken = await userCredential.user.getIdToken(); try { await authenticateWithBackend(idToken); } catch (backendErr) { console.warn( "[useAuth] Backend authentication failed after Google login:", backendErr ); } user.value = userCredential.user; registerListener(); return userCredential.user; } catch (err: any) { error.value = err.message; throw err; } finally { isLoading.value = false; } }; const registerWithEmail = async (email: string, password: string) => { try { error.value = null; isLoading.value = true; const auth = getAuthInstance(); if (!auth) { throw new Error("Firebase not initialized"); } const userCredential = await createUserWithEmailAndPassword( auth, email, password ); const idToken = await userCredential.user.getIdToken(); try { await authenticateWithBackend(idToken); } catch (backendErr) { console.warn( "[useAuth] Backend authentication failed after registration:", backendErr ); } user.value = userCredential.user; registerListener(); return userCredential.user; } catch (err: any) { error.value = err.message; throw err; } finally { isLoading.value = false; } }; // Sign out const signOut = async () => { try { const auth = getAuthInstance(); if (!auth) { throw new Error("Firebase not initialized"); } await firebaseSignOut(auth); user.value = null; backendUser.value = null; } catch (err: any) { error.value = err.message; throw err; } }; // Get current user's ID token const getIdToken = async () => { if (!user.value) return null; return await user.value.getIdToken(); }; return { user: readonly(user), isLoading: readonly(isLoading), error: readonly(error), backendUser: readonly(backendUser), initAuth, signInWithEmail, signInWithGoogle, registerWithEmail, signOut, getIdToken, }; };