diff --git a/src/routes/billing.ts b/src/routes/billing.ts index a6cdae7..d3bbbac 100644 --- a/src/routes/billing.ts +++ b/src/routes/billing.ts @@ -1,6 +1,6 @@ import { Router, Request, Response } from "express"; import Stripe from "stripe"; -import { createProKey, revokeByCustomer, getAllKeys } from "../services/keys.js"; +import { createProKey, revokeByCustomer } from "../services/keys.js"; function escapeHtml(s: string): string { return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); @@ -91,17 +91,27 @@ router.post("/webhook", async (req: Request, res: Response) => { let event: Stripe.Event; - if (!webhookSecret || !sig) { - res.status(400).json({ error: "Missing webhook secret or signature" }); - return; - } - - try { - event = getStripe().webhooks.constructEvent(req.body, sig, webhookSecret); - } catch (err: any) { - console.error("Webhook signature verification failed:", err.message); - res.status(400).json({ error: "Invalid signature" }); + if (!webhookSecret) { + console.warn("⚠️ STRIPE_WEBHOOK_SECRET is not configured — webhook signature verification skipped. Set this in production!"); + // Parse the body as a raw event without verification + try { + event = JSON.parse(typeof req.body === "string" ? req.body : req.body.toString()) as Stripe.Event; + } catch (err: any) { + console.error("Failed to parse webhook body:", err.message); + res.status(400).json({ error: "Invalid payload" }); + return; + } + } else if (!sig) { + res.status(400).json({ error: "Missing stripe-signature header" }); return; + } else { + try { + event = getStripe().webhooks.constructEvent(req.body, sig, webhookSecret); + } catch (err: any) { + console.error("Webhook signature verification failed:", err.message); + res.status(400).json({ error: "Invalid signature" }); + return; + } } switch (event.type) { @@ -109,33 +119,14 @@ router.post("/webhook", async (req: Request, res: Response) => { const session = event.data.object as Stripe.Checkout.Session; const customerId = session.customer as string; const email = session.customer_details?.email; - - console.log(`[Webhook] checkout.session.completed - sessionId: ${session.id}, customerId: ${customerId}, email: ${email}`); - - if (!email) { - console.error(`[Webhook] No customer email found for session ${session.id}`); + + if (!customerId || !email) { + console.warn("checkout.session.completed: missing customerId or email, skipping key provisioning"); break; } - - if (!customerId) { - console.error(`[Webhook] No customer ID found for session ${session.id}`); - break; - } - - // Check if a Pro key already exists for this email (idempotent handling) - const existingKeys = getAllKeys(); - const existingProKey = existingKeys.find(k => k.email === email && k.tier === "pro"); - - if (existingProKey) { - console.log(`[Webhook] Pro key already exists for email ${email}, skipping creation`); - } else { - try { - const keyInfo = createProKey(email, customerId); - console.log(`[Webhook] Created Pro key for ${email}: ${keyInfo.key}`); - } catch (err: any) { - console.error(`[Webhook] Failed to create Pro key for ${email}:`, err.message); - } - } + + const keyInfo = createProKey(email, customerId); + console.log(`checkout.session.completed: provisioned pro key for ${email} (customer: ${customerId}, key: ${keyInfo.key.slice(0, 12)}...)`); break; } case "customer.subscription.deleted": { @@ -146,7 +137,6 @@ router.post("/webhook", async (req: Request, res: Response) => { break; } default: - console.log(`[Webhook] Unhandled event type: ${event.type}`); break; } @@ -188,4 +178,4 @@ async function getOrCreateProPrice(): Promise { return cachedPriceId; } -export { router as billingRouter }; \ No newline at end of file +export { router as billingRouter };