const { getSupabaseServiceClient } = require('../utils/supabase'); /** * Session 17 — `requireAuth` was 401'ing legitimate users whose Supabase * auth account had never had a matching row inserted into the * `public.users` table. Signup writes to `auth.users` automatically; * the corresponding row in `public.users` was supposed to land via a * trigger or a post-signup handler, but for SSO callbacks and any * legacy account that pre-dated the trigger the row was simply * missing. The audit found this: checkout 401'd as "User profile not * found" for valid sessions. * * The fix is an on-demand upsert. When the select misses, we insert a * default row (tier='free') keyed by the Supabase auth user's id + * email, then re-select. This is idempotent — concurrent requests * land on the upsert's existing-row branch and re-read the same row. * * If the insert ITSELF fails (genuine DB issue, not a missing row), * we 401 with a distinct message ('User profile creation failed') so * the operator can distinguish missing-row recovery from real DB * outages in logs. */ const PROFILE_COLUMNS = 'id, email, tier, scan_count, scan_reset_date, founder_status, grace_period_until, stripe_customer_id'; async function requireAuth(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Authentication required' }); } const token = authHeader.slice(7); const supabase = getSupabaseServiceClient(); const { data: { user }, error } = await supabase.auth.getUser(token); if (error || !user) { return res.status(401).json({ error: 'Invalid or expired token' }); } // First read. PostgREST returns `error.code === 'PGRST116'` when // `.single()` gets zero rows — that's the signal we use to trigger // the on-demand upsert below. Other errors (network, permission) // bubble up as a real 401. let { data: profile, error: profileError } = await supabase .from('users') .select(PROFILE_COLUMNS) .eq('id', user.id) .single(); // Session 17 — if no row exists, create one on the fly. The // `auth.users` row already exists (we just resolved a token against // it), so we're not creating an identity — we're filling in the // app-side row that should have been created at signup time but // wasn't (SSO callbacks, legacy accounts pre-trigger, etc.). // Detect PostgREST's "single requested, zero rows returned" via // either the canonical error code (preferred) or the message // pattern Supabase emits when the code field is stripped by an // older client. Canonical message: // "JSON object requested, multiple (or no) rows returned" const isMissingRow = profileError && ( profileError.code === 'PGRST116' || /\(or no\) rows/i.test(profileError.message || '') || /^no rows$/i.test(profileError.message || '') ); if (!profile && isMissingRow) { const defaultRow = { id: user.id, email: user.email || null, tier: 'free', scan_count: 0, scan_reset_date: new Date().toISOString().slice(0, 10), founder_status: false, }; const { error: upsertErr } = await supabase .from('users') .upsert(defaultRow, { onConflict: 'id' }); if (upsertErr) { console.warn('[requireAuth] users-row upsert failed:', upsertErr.message); return res.status(401).json({ error: 'User profile creation failed' }); } // Re-select. Use the same .single() shape so test mocks keep working. const reread = await supabase .from('users') .select(PROFILE_COLUMNS) .eq('id', user.id) .single(); profile = reread.data; profileError = reread.error; } if (profileError || !profile) { return res.status(401).json({ error: 'User profile not found' }); } req.user = profile; next(); } module.exports = { requireAuth };