← back to Trademarks Copyright
src/app/api/drops/stripe-webhook/route.ts
54 lines
import { NextRequest, NextResponse } from "next/server";
import { query } from "@/lib/db";
import { verifyWebhookSignature } from "@/lib/stripe";
export async function POST(req: NextRequest) {
const raw = await req.text();
const sig = req.headers.get("stripe-signature");
// Require a valid signature whenever STRIPE_WEBHOOK_SECRET is set.
// Without the secret, the webhook is disabled (never processes) rather than
// being silently open — fail closed.
if (!process.env.STRIPE_WEBHOOK_SECRET) {
return NextResponse.json(
{ error: "webhook disabled — STRIPE_WEBHOOK_SECRET not configured" },
{ status: 503 }
);
}
if (!verifyWebhookSignature(raw, sig)) {
return NextResponse.json({ error: "invalid signature" }, { status: 400 });
}
let evt: { type: string; data: { object: { metadata?: { subscriber_token?: string; tier?: string }; customer?: string; subscription?: string } } };
try { evt = JSON.parse(raw); } catch { return NextResponse.json({ error: "bad json" }, { status: 400 }); }
if (evt.type === "checkout.session.completed") {
const meta = evt.data.object.metadata ?? {};
const token = meta.subscriber_token;
const tier = meta.tier;
if (!token || !tier) return NextResponse.json({ ignored: true });
await query(
`UPDATE subscribers
SET tier = $1,
stripe_customer_id = COALESCE($2, stripe_customer_id),
stripe_sub_id = COALESCE($3, stripe_sub_id)
WHERE token = $4`,
[tier, evt.data.object.customer ?? null, evt.data.object.subscription ?? null, token]
);
return NextResponse.json({ ok: true, upgraded: tier });
}
if (evt.type === "customer.subscription.deleted") {
const subId = evt.data.object.subscription;
if (subId) {
await query(
`UPDATE subscribers SET tier = 'trial', status = 'cancelled', cancelled_at = NOW()
WHERE stripe_sub_id = $1`,
[subId]
);
}
}
return NextResponse.json({ received: true });
}