140 lines
3.7 KiB
TypeScript
140 lines
3.7 KiB
TypeScript
import { randomUUID } from "crypto";
|
|
import type { NextApiRequest, NextApiResponse } from "next";
|
|
import type { Readable } from "node:stream";
|
|
|
|
import { createNextApiContext } from "@kan/api/trpc";
|
|
import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
|
|
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
|
import { createLogger } from "@kan/logger";
|
|
import { createStripeClient } from "@kan/stripe";
|
|
|
|
const log = createLogger("api");
|
|
|
|
async function buffer(readable: Readable) {
|
|
const chunks = [];
|
|
for await (const chunk of readable) {
|
|
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
}
|
|
return Buffer.concat(chunks);
|
|
}
|
|
|
|
export default async function handler(
|
|
req: NextApiRequest,
|
|
res: NextApiResponse,
|
|
) {
|
|
const stripe = createStripeClient();
|
|
const start = Date.now();
|
|
const requestId = randomUUID();
|
|
const procedure = req.url?.split("?")[0] ?? "/api/stripe/webhook";
|
|
|
|
if (req.method !== "POST") {
|
|
return res.status(405).json({ message: "Method not allowed" });
|
|
}
|
|
|
|
const sig = req.headers["stripe-signature"];
|
|
|
|
if (!sig) {
|
|
return res.status(400).json({ message: "No signature found" });
|
|
}
|
|
|
|
try {
|
|
const buf = await buffer(req);
|
|
const rawBody = buf.toString("utf8");
|
|
|
|
const event = stripe.webhooks.constructEvent(
|
|
rawBody,
|
|
sig,
|
|
process.env.STRIPE_WEBHOOK_SECRET_LEGACY!,
|
|
);
|
|
|
|
const { db } = await createNextApiContext(req);
|
|
|
|
switch (event.type) {
|
|
case "checkout.session.completed": {
|
|
const checkoutSession = event.data.object;
|
|
const meta = checkoutSession.metadata;
|
|
|
|
if (!meta?.workspacePublicId) break;
|
|
|
|
const plan = meta.plan === "team" ? "team" : ("pro" as const);
|
|
|
|
if (
|
|
meta.isNewWorkspace === "true" &&
|
|
meta.workspaceName &&
|
|
meta.userId &&
|
|
meta.userEmail
|
|
) {
|
|
const existing = await workspaceRepo.getByPublicId(db, meta.workspacePublicId);
|
|
|
|
if (!existing) {
|
|
const slug = meta.workspaceSlug ?? meta.workspacePublicId;
|
|
|
|
await workspaceRepo.create(db, {
|
|
publicId: meta.workspacePublicId,
|
|
name: meta.workspaceName,
|
|
slug,
|
|
plan,
|
|
createdBy: meta.userId,
|
|
createdByEmail: meta.userEmail,
|
|
...(meta.workspaceDescription && {
|
|
description: meta.workspaceDescription,
|
|
}),
|
|
});
|
|
|
|
await subscriptionRepo.create(db, {
|
|
plan,
|
|
referenceId: meta.workspacePublicId,
|
|
userId: meta.userId,
|
|
stripeCustomerId: checkoutSession.customer as string,
|
|
status: "active",
|
|
});
|
|
}
|
|
} else {
|
|
// Existing workspace upgrade — update plan (and slug for pro)
|
|
await workspaceRepo.update(db, meta.workspacePublicId, {
|
|
plan,
|
|
...(plan === "pro" && meta.workspaceSlug && { slug: meta.workspaceSlug }),
|
|
});
|
|
}
|
|
|
|
break;
|
|
}
|
|
default:
|
|
log.warn({ eventType: event.type }, "Unhandled Stripe event type");
|
|
}
|
|
|
|
log.info(
|
|
{
|
|
requestId,
|
|
procedure,
|
|
transport: "rest",
|
|
duration: Date.now() - start,
|
|
status: 200,
|
|
input: { eventType: event.type, eventId: event.id },
|
|
},
|
|
"API OK",
|
|
);
|
|
|
|
return res.status(200).json({ received: true });
|
|
} catch (err) {
|
|
log.error(
|
|
{
|
|
requestId,
|
|
procedure,
|
|
transport: "rest",
|
|
duration: Date.now() - start,
|
|
status: 400,
|
|
err,
|
|
},
|
|
"API error",
|
|
);
|
|
return res.status(400).json({ message: "Webhook handler failed" });
|
|
}
|
|
}
|
|
|
|
export const config = {
|
|
api: {
|
|
bodyParser: false,
|
|
},
|
|
};
|