Session 17: Audit response — checkout 401 fix, hero prop 404 fix, Slate parsing fix, ALL tab cascade isolation, cookie/nav/footer/autocomplete polish (1438 tests)
This commit is contained in:
+70
-7
@@ -1,5 +1,28 @@
|
||||
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 ')) {
|
||||
@@ -14,17 +37,57 @@ async function requireAuth(req, res, next) {
|
||||
return res.status(401).json({ error: 'Invalid or expired token' });
|
||||
}
|
||||
|
||||
// Fetch user profile from our users table. Session 9 added
|
||||
// `grace_period_until` + `stripe_customer_id` to the select so the
|
||||
// grace-period middleware can read them off `req.user` without a
|
||||
// second round-trip. Both fields default to null when absent so
|
||||
// pre-Stripe users behave identically to before.
|
||||
const { data: profile, error: profileError } = await supabase
|
||||
// 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('id, email, tier, scan_count, scan_reset_date, founder_status, grace_period_until, stripe_customer_id')
|
||||
.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' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user