Session 9: api-football + FootApi + Tank01 adapters, grace period middleware, cookie consent, /pricing page, OOM fix documented (1240 tests)
This commit is contained in:
@@ -14,10 +14,14 @@ async function requireAuth(req, res, next) {
|
||||
return res.status(401).json({ error: 'Invalid or expired token' });
|
||||
}
|
||||
|
||||
// Fetch user profile from our users table
|
||||
// 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
|
||||
.from('users')
|
||||
.select('id, email, tier, scan_count, scan_reset_date, founder_status')
|
||||
.select('id, email, tier, scan_count, scan_reset_date, founder_status, grace_period_until, stripe_customer_id')
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Grace-period downgrade middleware (Session 9).
|
||||
*
|
||||
* Fires at request time on tier-gated routes. The Stripe webhook
|
||||
* (`customer.subscription.deleted` and `invoice.payment_failed`) sets
|
||||
* `users.grace_period_until` to now + 48h on cancellation / payment
|
||||
* failure. Until Session 9, nothing actively checked whether the
|
||||
* grace had expired — so cancelled users could keep paid access
|
||||
* indefinitely. This middleware closes that gap.
|
||||
*
|
||||
* Behavior:
|
||||
* - No `grace_period_until` on req.user → pass through
|
||||
* - Grace still in the future → pass through
|
||||
* - Grace expired → atomically
|
||||
* downgrade `users.tier` AND `user_profiles.tier` to 'free',
|
||||
* clear the grace timestamp, set subscription_status='expired'
|
||||
* on the profile mirror, and rewrite req.user so the downstream
|
||||
* route immediately sees the downgraded tier.
|
||||
*
|
||||
* Mount AFTER `requireAuth` on tier-gated routes. Routes that don't
|
||||
* gate by tier (e.g. /api/bets read-only views) don't need it.
|
||||
*
|
||||
* Failure semantics: if either DB write fails, we still call next()
|
||||
* — the user might briefly retain paid access until the next request,
|
||||
* but at least the route keeps serving. The webhook's grace pointer
|
||||
* stays set, so we'll try again on the next request.
|
||||
*/
|
||||
|
||||
const { getSupabaseServiceClient } = require('../utils/supabase');
|
||||
|
||||
async function checkGracePeriod(req, res, next) {
|
||||
const user = req.user;
|
||||
// No user (unauth route slipping through?) — bail to next.
|
||||
if (!user || !user.grace_period_until) return next();
|
||||
|
||||
const grace = new Date(user.grace_period_until);
|
||||
// Invalid date → treat as no grace.
|
||||
if (!Number.isFinite(grace.getTime())) return next();
|
||||
// Still in grace window — let them keep paid access.
|
||||
if (grace.getTime() > Date.now()) return next();
|
||||
|
||||
// Expired. Downgrade in both tables. We log on failure but DO NOT
|
||||
// throw — the route still serves; we re-try on the next request.
|
||||
try {
|
||||
const supabase = getSupabaseServiceClient();
|
||||
const { error: usersErr } = await supabase
|
||||
.from('users')
|
||||
.update({ tier: 'free', grace_period_until: null })
|
||||
.eq('id', user.id);
|
||||
if (usersErr) {
|
||||
console.warn('[gracePeriod] users update failed:', usersErr.message);
|
||||
}
|
||||
|
||||
const { error: profileErr } = await supabase
|
||||
.from('user_profiles')
|
||||
.update({
|
||||
tier: 'free',
|
||||
subscription_status: 'expired',
|
||||
grace_period_until: null,
|
||||
})
|
||||
.eq('id', user.id);
|
||||
if (profileErr) {
|
||||
console.warn('[gracePeriod] user_profiles update failed:', profileErr.message);
|
||||
}
|
||||
|
||||
// Reflect on req.user so the downstream route sees the downgrade
|
||||
// immediately (no race against a stale closure).
|
||||
req.user.tier = 'free';
|
||||
req.user.grace_period_until = null;
|
||||
} catch (err) {
|
||||
console.warn('[gracePeriod] downgrade error (continuing):', err.message);
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
module.exports = { checkGracePeriod };
|
||||
Reference in New Issue
Block a user