← 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 });
}