diff --git a/apps/web/src/app/api/webhooks/dodo/route.ts b/apps/web/src/app/api/webhooks/dodo/route.ts index 1144594..792866c 100644 --- a/apps/web/src/app/api/webhooks/dodo/route.ts +++ b/apps/web/src/app/api/webhooks/dodo/route.ts @@ -1,28 +1,31 @@ -import { NextRequest, NextResponse } from 'next/server' -import { db, customers, licenses } from '@/db' -import { eq } from 'drizzle-orm' -import { generateLicenseKey, calculateUpdatesUntil } from '@/lib/license' -import { Resend } from 'resend' +import { NextRequest, NextResponse } from "next/server"; +import { db, customers, licenses } from "@/db"; +import { eq } from "drizzle-orm"; +import { generateLicenseKey, calculateUpdatesUntil } from "@/lib/license"; +import { Resend } from "resend"; -const resend = new Resend(process.env.RESEND_API_KEY) +const resend = new Resend(process.env.RESEND_API_KEY ?? "re_123"); // DodoPayments webhook event types -type DodoEventType = 'payment.completed' | 'payment.refunded' | 'payment.failed' +type DodoEventType = + | "payment.completed" + | "payment.refunded" + | "payment.failed"; interface DodoWebhookPayload { - event: DodoEventType + event: DodoEventType; data: { - payment_id: string - product_id: string + payment_id: string; + product_id: string; customer: { - email: string - name?: string - customer_id: string - } - amount: number - currency: string - metadata?: Record - } + email: string; + name?: string; + customer_id: string; + }; + amount: number; + currency: string; + metadata?: Record; + }; } // Verify DodoPayments webhook signature @@ -31,21 +34,21 @@ async function verifyWebhookSignature( signature: string | null ): Promise { if (!signature || !process.env.DODO_WEBHOOK_SECRET) { - console.warn('Missing webhook signature or secret') - return false + console.warn("Missing webhook signature or secret"); + return false; } // DodoPayments uses HMAC-SHA256 for webhook signatures - const crypto = await import('crypto') + const crypto = await import("crypto"); const expectedSignature = crypto - .createHmac('sha256', process.env.DODO_WEBHOOK_SECRET) + .createHmac("sha256", process.env.DODO_WEBHOOK_SECRET) .update(payload) - .digest('hex') + .digest("hex"); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSignature) - ) + ); } // Send welcome email with license key @@ -56,20 +59,20 @@ async function sendWelcomeEmail( updatesUntil: Date ) { if (!process.env.RESEND_API_KEY) { - console.warn('RESEND_API_KEY not configured, skipping email') - return + console.warn("RESEND_API_KEY not configured, skipping email"); + return; } try { await resend.emails.send({ - from: 'data-peek ', + from: "data-peek ", to: email, - subject: 'Your data-peek Pro license 🎉', + subject: "Your data-peek Pro license 🎉", html: `

Welcome to data-peek Pro!

-

Hi ${name || 'there'},

+

Hi ${name || "there"},

Thank you for purchasing data-peek Pro! Your license is ready to use.

@@ -97,36 +100,39 @@ async function sendWelcomeEmail(

Happy querying!
— The data-peek team

`, - }) + }); } catch (error) { - console.error('Failed to send welcome email:', error) + console.error("Failed to send welcome email:", error); } } export async function POST(request: NextRequest) { try { - const payload = await request.text() - const signature = request.headers.get('x-dodo-signature') + const payload = await request.text(); + const signature = request.headers.get("x-dodo-signature"); // Verify signature in production - if (process.env.NODE_ENV === 'production') { - const isValid = await verifyWebhookSignature(payload, signature) + if (process.env.NODE_ENV === "production") { + const isValid = await verifyWebhookSignature(payload, signature); if (!isValid) { - console.error('Invalid webhook signature') - return NextResponse.json({ error: 'Invalid signature' }, { status: 401 }) + console.error("Invalid webhook signature"); + return NextResponse.json( + { error: "Invalid signature" }, + { status: 401 } + ); } } - const event = JSON.parse(payload) as DodoWebhookPayload + const event = JSON.parse(payload) as DodoWebhookPayload; switch (event.event) { - case 'payment.completed': { - const { data } = event + case "payment.completed": { + const { data } = event; // Find or create customer let customer = await db.query.customers.findFirst({ where: eq(customers.email, data.customer.email), - }) + }); if (!customer) { const [newCustomer] = await db @@ -136,31 +142,31 @@ export async function POST(request: NextRequest) { name: data.customer.name, dodoCustomerId: data.customer.customer_id, }) - .returning() - customer = newCustomer + .returning(); + customer = newCustomer; } else if (!customer.dodoCustomerId) { // Update existing customer with DodoPayments ID await db .update(customers) .set({ dodoCustomerId: data.customer.customer_id }) - .where(eq(customers.id, customer.id)) + .where(eq(customers.id, customer.id)); } // Generate license key - const licenseKey = generateLicenseKey('DPRO') - const updatesUntil = calculateUpdatesUntil() + const licenseKey = generateLicenseKey("DPRO"); + const updatesUntil = calculateUpdatesUntil(); // Create license await db.insert(licenses).values({ customerId: customer.id, licenseKey, - plan: 'pro', - status: 'active', + plan: "pro", + status: "active", maxActivations: 3, dodoPaymentId: data.payment_id, dodoProductId: data.product_id, updatesUntil, - }) + }); // Send welcome email await sendWelcomeEmail( @@ -168,45 +174,52 @@ export async function POST(request: NextRequest) { data.customer.name, licenseKey, updatesUntil - ) + ); - console.log(`License created for ${data.customer.email}: ${licenseKey}`) - break + console.log( + `License created for ${data.customer.email}: ${licenseKey}` + ); + break; } - case 'payment.refunded': { - const { data } = event + case "payment.refunded": { + const { data } = event; // Find and revoke the license const license = await db.query.licenses.findFirst({ where: eq(licenses.dodoPaymentId, data.payment_id), - }) + }); if (license) { await db .update(licenses) - .set({ status: 'revoked' }) - .where(eq(licenses.id, license.id)) + .set({ status: "revoked" }) + .where(eq(licenses.id, license.id)); - console.log(`License revoked for payment ${data.payment_id}`) + console.log(`License revoked for payment ${data.payment_id}`); } - break + break; } - case 'payment.failed': { - const { data } = event - console.log(`Payment failed for ${data.customer.email}: ${data.payment_id}`) + case "payment.failed": { + const { data } = event; + console.log( + `Payment failed for ${data.customer.email}: ${data.payment_id}` + ); // Could send a failed payment notification email here - break + break; } default: - console.log(`Unhandled webhook event: ${event.event}`) + console.log(`Unhandled webhook event: ${event.event}`); } - return NextResponse.json({ received: true }) + return NextResponse.json({ received: true }); } catch (error) { - console.error('Webhook processing error:', error) - return NextResponse.json({ error: 'Webhook processing failed' }, { status: 500 }) + console.error("Webhook processing error:", error); + return NextResponse.json( + { error: "Webhook processing failed" }, + { status: 500 } + ); } }