// auth.jsx — Supabase auth client + session hook + authedFetch helper. // // Boot order: // 1. App.jsx calls window.CoachAuth.boot() before first render. // 2. boot() fetches /api/config to get the public Supabase URL + key, // then constructs the supabase client and stores it on window.CoachAuth. // 3. useAuth() subscribes to auth state changes and re-renders on // sign-in / sign-out. // 4. authedFetch(path, options) attaches the current access_token as a // Bearer header. Every /api/* call goes through this. const { useEffect, useState, useCallback } = React; let _client = null; let _readyPromise = null; async function boot() { if (_readyPromise) return _readyPromise; _readyPromise = (async () => { const r = await fetch('/api/config'); if (!r.ok) throw new Error('config fetch failed: ' + r.status); const cfg = await r.json(); const { createClient } = window.supabase; _client = createClient(cfg.supabase_url, cfg.supabase_publishable_key, { auth: { // Persist session in localStorage; keep alive across reloads. persistSession: true, autoRefreshToken: true, // True so the email-confirmation redirect (returns the user to // `/#access_token=…&type=signup`) auto-establishes the session. // Supabase JS strips the hash after consuming it. detectSessionInUrl: true, }, }); return _client; })(); return _readyPromise; } function getClient() { if (!_client) throw new Error('CoachAuth not booted yet'); return _client; } // React hook — re-renders on sign-in / sign-out / token refresh. function useAuth() { const [state, setState] = useState({ loading: true, session: null }); useEffect(() => { let mounted = true; const client = getClient(); client.auth.getSession().then(({ data }) => { if (mounted) setState({ loading: false, session: data.session }); }); const { data: sub } = client.auth.onAuthStateChange((_event, session) => { if (mounted) setState({ loading: false, session }); }); return () => { mounted = false; sub.subscription.unsubscribe(); }; }, []); return state; } async function signIn(email, password) { const { error } = await getClient().auth.signInWithPassword({ email, password }); if (error) throw error; } async function signUp(email, password) { const { data, error } = await getClient().auth.signUp({ email, password }); if (error) throw error; return data; } async function signOut() { await getClient().auth.signOut(); } // Wraps fetch — adds Authorization: Bearer if we have a session. // Returns parsed JSON on 2xx; throws an Error with the server's detail on // 4xx/5xx so callers don't need to write try/catch ladders. async function authedFetch(path, options = {}) { const client = getClient(); const { data } = await client.auth.getSession(); const token = data.session?.access_token; const headers = new Headers(options.headers || {}); if (token) headers.set('Authorization', 'Bearer ' + token); if (options.body && !headers.has('Content-Type')) { headers.set('Content-Type', 'application/json'); } const r = await fetch(path, { ...options, headers }); let body = null; try { body = await r.json(); } catch (_e) { /* not json */ } if (!r.ok) { const msg = body?.detail?.detail || body?.detail || body?.error || ('http_' + r.status); const err = new Error(typeof msg === 'string' ? msg : JSON.stringify(msg)); err.status = r.status; err.body = body; throw err; } return body; } window.CoachAuth = { boot, getClient, useAuth, signIn, signUp, signOut, authedFetch };