import type { BetterAuthClientOptions, InferSessionFromClient, InferUserFromClient, User } from 'better-auth/client'; import { createAuthClient } from 'better-auth/vue'; import { Err, Ok, type Result } from '~~/types/result'; import { assert } from '~~/utils/assert'; export enum AuthError { NotAuthenticated = 'NOT_AUTHENTICATED', SignInFailed = 'SIGN_IN_FAILED', SignUpFailed = 'SIGN_UP_FAILED', SignOutFailed = 'SIGN_OUT_FAILED', NetworkError = 'NETWORK_ERROR', } type sessionData = { session: InferSessionFromClient | null; user: InferUserFromClient | null; }; export const useAuth = () => { const authClient = createAuthClient({}); const session = useState | null>('auth:session', () => null); const user = useState | null>('auth:user', () => null); const sessionFetching = import.meta.server ? ref(false) : useState('auth:sessionFetching', () => false); let sessionPromise: Promise> | null = null; const fetchSession = async (): Promise> => { if (sessionPromise) { console.log('already fetching session'); return sessionPromise; } let finish: (value: Result) => void; sessionFetching.value = true; sessionPromise = new Promise(async (resolve, reject) => { finish = resolve; }); let data: { session: InferSessionFromClient; user: InferUserFromClient; } | null = null; try { if (import.meta.server) { data = ( await useFetch<{ session: InferSessionFromClient; user: InferUserFromClient; }>('/api/auth/get-session') ).data.value ?? null; } else { data = (await authClient.getSession()).data; } } catch (error) { console.error('Failed to fetch session:', error); sessionFetching.value = false; finish!(Err(AuthError.NetworkError)); return sessionPromise; } session.value = data?.session || null; user.value = data?.user || null; sessionFetching.value = false; sessionFetching.value = false; finish!(Ok({ session: data?.session || null, user: data?.user || null })); return sessionPromise; }; if (import.meta.client) { authClient.$store.listen('$sessionSignal', async (signal) => { if (!signal) return; await fetchSession(); if (!session.value) return; const triplit = useTriplitClient(); if ('updateOptions' in triplit) { triplit.updateOptions({ token: session.value.token, }); } }); } const signIn = async ( email: string, password: string ): Promise> => { sessionFetching.value = true; try { const { data, error } = await authClient.signIn.email({ email, password, }); if (error) { console.error('Sign in failed:', error); return Err({ error: AuthError.SignInFailed, data: error }); } if (!data) { return Err({ error: AuthError.SignInFailed }); } user.value = data.user; const triplit = useTriplitClient(); if ('startSession' in triplit && data.token) { await triplit.startSession(data.token); } clearNuxtData(); return Ok({ user: data.user, token: data.token }); } catch (err) { console.error('Sign in error:', err); return Err({ error: AuthError.NetworkError }); } finally { sessionFetching.value = false; } }; const signUp = async ( email: string, password: string, name: string ): Promise> => { sessionFetching.value = true; try { const { data, error } = await authClient.signUp.email({ email, password, name, }); if (error) { console.error('Sign up failed:', error); return Err({ error: AuthError.SignUpFailed, data: error }); } if (!data || !data.token) { return Err({ error: AuthError.SignUpFailed }); } user.value = data.user; const triplit = useTriplitClient(); assert('startSession' in triplit); await triplit.startSession(data.token); clearNuxtData(); return Ok({ user: data.user, token: data.token }); } catch (err) { console.error('Sign up error:', err); return Err({ error: AuthError.NetworkError }); } finally { sessionFetching.value = false; } }; const signOut = async (): Promise> => { sessionFetching.value = true; try { const { error } = await authClient.signOut(); if (error) { console.error('Sign out failed:', error); return Err(AuthError.SignOutFailed); } user.value = null; session.value = null; const triplit = useTriplitClient(); assert('disconnect' in triplit); triplit.disconnect(); clearNuxtData(); return Ok(undefined); } catch (err) { console.error('Sign out error:', err); return Err(AuthError.NetworkError); } finally { sessionFetching.value = false; } }; return { client: authClient, session, user, loggedIn: computed(() => !!session.value), signIn, signUp, signOut, fetchSession, }; };