summaryrefslogtreecommitdiff
path: root/apps/web/app/api/billing/webhook
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-07 05:41:07 -0800
committerFuwn <[email protected]>2026-02-07 05:41:07 -0800
commita1a405e56a0907ed44bfaba721e0ea632e051141 (patch)
tree99621f7c1407eed732eeb5742fc7458c14ee6d48 /apps/web/app/api/billing/webhook
parentsecurity: remove unsafe-eval CSP, fix host header injection, harden API routes (diff)
downloadasa.news-a1a405e56a0907ed44bfaba721e0ea632e051141.tar.xz
asa.news-a1a405e56a0907ed44bfaba721e0ea632e051141.zip
fix: resolve 6 pre-ship audit bugs
- Webhook entry identifier: use entry GUID instead of feed identifier - Optimistic rollback: add previousTimeline snapshot and onError handler to both useToggleEntryReadState and useToggleEntrySavedState - Rate limiter memory leak: delete Map entries when window expires, use else-if to avoid re-setting after delete - Entries API limit param: use Number.isFinite guard instead of falsy coercion that treats 0 as default - PWA manifest: add PNG raster icon routes (192x192, 512x512) for devices that don't support SVG icons - Billing webhook: throw on DB errors and return 500 so Stripe retries failed events instead of silently losing them
Diffstat (limited to 'apps/web/app/api/billing/webhook')
-rw-r--r--apps/web/app/api/billing/webhook/route.ts60
1 files changed, 37 insertions, 23 deletions
diff --git a/apps/web/app/api/billing/webhook/route.ts b/apps/web/app/api/billing/webhook/route.ts
index 297ef8e..52b1420 100644
--- a/apps/web/app/api/billing/webhook/route.ts
+++ b/apps/web/app/api/billing/webhook/route.ts
@@ -45,6 +45,7 @@ async function updateBillingState(
if (error) {
console.error("failed to update billing state:", error)
+ throw new Error(`failed to update billing state: ${error.message}`)
}
}
@@ -62,7 +63,7 @@ async function handleCheckoutSessionCompleted(
)
const adminClient = createSupabaseAdminClient()
- await adminClient
+ const { error } = await adminClient
.from("user_profiles")
.update({
tier: determineTierFromSubscription(subscription),
@@ -72,6 +73,11 @@ async function handleCheckoutSessionCompleted(
stripe_current_period_end: extractPeriodEnd(subscription),
})
.eq("id", userIdentifier)
+
+ if (error) {
+ console.error("failed to update billing state on checkout:", error)
+ throw new Error(`failed to update billing state on checkout: ${error.message}`)
+ }
}
async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
@@ -153,28 +159,36 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "invalid signature" }, { status: 400 })
}
- switch (event.type) {
- case "checkout.session.completed":
- await handleCheckoutSessionCompleted(
- event.data.object as Stripe.Checkout.Session
- )
- break
- case "customer.subscription.updated":
- await handleSubscriptionUpdated(
- event.data.object as Stripe.Subscription
- )
- break
- case "customer.subscription.deleted":
- await handleSubscriptionDeleted(
- event.data.object as Stripe.Subscription
- )
- break
- case "invoice.payment_failed":
- await handleInvoicePaymentFailed(event.data.object as Stripe.Invoice)
- break
- case "invoice.paid":
- await handleInvoicePaid(event.data.object as Stripe.Invoice)
- break
+ try {
+ switch (event.type) {
+ case "checkout.session.completed":
+ await handleCheckoutSessionCompleted(
+ event.data.object as Stripe.Checkout.Session
+ )
+ break
+ case "customer.subscription.updated":
+ await handleSubscriptionUpdated(
+ event.data.object as Stripe.Subscription
+ )
+ break
+ case "customer.subscription.deleted":
+ await handleSubscriptionDeleted(
+ event.data.object as Stripe.Subscription
+ )
+ break
+ case "invoice.payment_failed":
+ await handleInvoicePaymentFailed(event.data.object as Stripe.Invoice)
+ break
+ case "invoice.paid":
+ await handleInvoicePaid(event.data.object as Stripe.Invoice)
+ break
+ }
+ } catch (handlerError) {
+ console.error("webhook handler failed:", handlerError)
+ return NextResponse.json(
+ { error: "webhook handler failed" },
+ { status: 500 }
+ )
}
return NextResponse.json({ received: true })