127 lines
3.1 KiB
TypeScript
127 lines
3.1 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { and, eq } from "drizzle-orm";
|
|
import {
|
|
getMonthlyImageLimitByPlanId,
|
|
isBillingPlanId,
|
|
type BillingPlanId,
|
|
} from "@/lib/billing/plans";
|
|
import { verifyStripeSignature } from "@/lib/billing/stripe";
|
|
import { db } from "@/lib/db";
|
|
import { user } from "@/lib/db/schema";
|
|
|
|
type StripeEvent = {
|
|
id: string;
|
|
type: string;
|
|
data: {
|
|
object: {
|
|
metadata?: Record<string, string>;
|
|
customer_details?: {
|
|
email?: string;
|
|
};
|
|
mode?: string;
|
|
status?: string;
|
|
cancel_at_period_end?: boolean;
|
|
};
|
|
};
|
|
};
|
|
|
|
async function applyPlanToUser({
|
|
userId,
|
|
email,
|
|
planId,
|
|
}: {
|
|
userId?: string;
|
|
email?: string;
|
|
planId: BillingPlanId;
|
|
}) {
|
|
const monthlyImageLimit = getMonthlyImageLimitByPlanId(planId);
|
|
const updateData = {
|
|
monthlyImageLimit,
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
if (userId) {
|
|
await db.update(user).set(updateData).where(eq(user.id, userId));
|
|
return;
|
|
}
|
|
|
|
if (email) {
|
|
await db
|
|
.update(user)
|
|
.set(updateData)
|
|
.where(and(eq(user.email, email), eq(user.isActive, true)));
|
|
}
|
|
}
|
|
|
|
function extractUserRef(object: StripeEvent["data"]["object"]) {
|
|
return {
|
|
userId: object.metadata?.userId,
|
|
email: object.customer_details?.email,
|
|
};
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
|
|
|
|
if (!webhookSecret) {
|
|
return NextResponse.json({ error: "Webhook secret is not configured" }, { status: 503 });
|
|
}
|
|
|
|
const rawPayload = await request.text();
|
|
const signature = request.headers.get("stripe-signature");
|
|
|
|
if (!signature) {
|
|
return NextResponse.json({ error: "Missing stripe-signature header" }, { status: 400 });
|
|
}
|
|
|
|
const verified = verifyStripeSignature({
|
|
payload: rawPayload,
|
|
signatureHeader: signature,
|
|
webhookSecret,
|
|
});
|
|
|
|
if (!verified) {
|
|
return NextResponse.json({ error: "Invalid webhook signature" }, { status: 400 });
|
|
}
|
|
|
|
try {
|
|
const event = JSON.parse(rawPayload) as StripeEvent;
|
|
const object = event.data.object;
|
|
const userRef = extractUserRef(object);
|
|
|
|
if (event.type === "checkout.session.completed") {
|
|
if (object.mode !== "subscription") {
|
|
return NextResponse.json({ received: true });
|
|
}
|
|
|
|
const planId = object.metadata?.planId;
|
|
|
|
if (planId && isBillingPlanId(planId)) {
|
|
await applyPlanToUser({ ...userRef, planId });
|
|
}
|
|
|
|
return NextResponse.json({ received: true });
|
|
}
|
|
|
|
if (event.type === "customer.subscription.updated") {
|
|
const planId = object.metadata?.planId;
|
|
|
|
if (planId && isBillingPlanId(planId) && object.status === "active") {
|
|
await applyPlanToUser({ ...userRef, planId });
|
|
}
|
|
|
|
return NextResponse.json({ received: true });
|
|
}
|
|
|
|
if (event.type === "customer.subscription.deleted") {
|
|
await applyPlanToUser({ ...userRef, planId: "starter" });
|
|
return NextResponse.json({ received: true });
|
|
}
|
|
|
|
return NextResponse.json({ received: true });
|
|
} catch (error) {
|
|
console.error("[Billing Stripe Webhook] Error:", error);
|
|
return NextResponse.json({ error: "Webhook processing failed" }, { status: 500 });
|
|
}
|
|
}
|