UNPKG

nuxt-supabase-team-auth

Version:

Drop-in Nuxt 3 module for team-based authentication with Supabase

173 lines (151 loc) 4.58 kB
import { ref, readonly } from 'vue' import { useSupabaseClient } from './useSupabaseComposables' // Use any for Session type since we'll get it from @nuxtjs/supabase type Session = any // Global session state const sessionCache = ref<Session | null>(null) const sessionPromise = ref<Promise<Session | null> | null>(null) const lastFetch = ref(0) const CACHE_DURATION = 30000 // 30 seconds // Cache the Supabase client to avoid repeated calls let cachedClient: any = null let authListener: any = null function getClient() { if (!cachedClient) { cachedClient = useSupabaseClient() if (!cachedClient) { throw new Error('Supabase client not available - check your Supabase setup') } } return cachedClient } // Set up auth state listener to invalidate cache on session changes function setupAuthListener() { if (authListener || !import.meta.client) return try { const client = getClient() authListener = client.auth.onAuthStateChange((event: string, session: Session | null) => { // Clear cache whenever auth state changes (including impersonation) if (event === 'SIGNED_IN' || event === 'SIGNED_OUT' || event === 'TOKEN_REFRESHED') { sessionCache.value = session lastFetch.value = Date.now() } }) } catch { // Client not ready yet, will be set up on first getSession call console.debug('Auth listener setup deferred - client not ready') } } /** * Centralized session management composable * Provides cached session access and prevents concurrent getSession() calls */ export function useSession() { /** * Get the current session with intelligent caching * @param force - Force a fresh session fetch, bypassing cache * @returns Session object or null */ const getSession = async (force = false): Promise<Session | null> => { // Set up auth listener on first use if not already done if (!authListener && import.meta.client) { setupAuthListener() } const now = Date.now() // Return cached session if recent and not forcing refresh if (!force && sessionCache.value && (now - lastFetch.value) < CACHE_DURATION) { return sessionCache.value } // Prevent concurrent session fetches by reusing existing promise if (sessionPromise.value) { return await sessionPromise.value } // Create new session fetch promise sessionPromise.value = fetchSession() try { const result = await sessionPromise.value return result } finally { // Clear the promise after completion (success or failure) sessionPromise.value = null } } /** * Internal function to actually fetch the session */ const fetchSession = async (): Promise<Session | null> => { try { const { data: { session }, error } = await getClient().auth.getSession() if (error) { console.warn('Session fetch error:', error) return null } // Update cache sessionCache.value = session lastFetch.value = Date.now() return session } catch (error) { console.error('Failed to fetch session:', error) return null } } /** * Clear the session cache (useful for logout scenarios) */ const clearSession = () => { sessionCache.value = null lastFetch.value = 0 sessionPromise.value = null } /** * Invalidate cache and force fresh session fetch * Useful for impersonation or other session state changes */ const invalidateSession = async () => { clearSession() return await getSession(true) } /** * Get cached session without fetching (may be null/stale) */ const getCachedSession = () => sessionCache.value /** * Check if we have a valid cached session */ const hasCachedSession = () => { const now = Date.now() return sessionCache.value && (now - lastFetch.value) < CACHE_DURATION } return { getSession, clearSession, invalidateSession, getCachedSession, hasCachedSession, // Expose readonly cache for reactive use sessionCache: readonly(sessionCache), } } /** * Reset all global session state - for testing only * @internal */ export function resetSessionState() { sessionCache.value = null sessionPromise.value = null lastFetch.value = 0 cachedClient = null if (authListener) { // Clean up auth listener if it exists if (typeof authListener === 'function') { authListener() } else if (authListener?.unsubscribe) { authListener.unsubscribe() } authListener = null } }