mirror of
https://github.com/Rohithgilla12/data-peek
synced 2026-05-23 01:18:29 +00:00
fix(web): update webhook verification to use Standard Webhooks spec
- Use Dodo Payments SDK's unwrap() method for signature verification - Extract proper Standard Webhooks headers (webhook-id, webhook-signature, webhook-timestamp) - Add idempotency handling using webhook-id to prevent duplicate processing - Remove custom HMAC verification in favor of SDK-based approach 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8781cb946c
commit
968cd9521d
1 changed files with 47 additions and 37 deletions
|
|
@ -6,7 +6,10 @@ import { Resend } from "resend";
|
|||
import DodoPayments from "dodopayments";
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY ?? "re_123");
|
||||
const dodo = new DodoPayments({ bearerToken: process.env.DODO_API_KEY });
|
||||
const dodo = new DodoPayments({
|
||||
bearerToken: process.env.DODO_API_KEY,
|
||||
webhookKey: process.env.DODO_WEBHOOK_SECRET,
|
||||
});
|
||||
|
||||
// DodoPayments webhook event types
|
||||
type DodoEventType =
|
||||
|
|
@ -52,29 +55,6 @@ function getEventId(event: DodoWebhookPayload): string {
|
|||
return data.payment_id || data.subscription_id || data.id || `${type}-${timestamp}`;
|
||||
}
|
||||
|
||||
// Verify DodoPayments webhook signature
|
||||
async function verifyWebhookSignature(
|
||||
payload: string,
|
||||
signature: string | null
|
||||
): Promise<boolean> {
|
||||
if (!signature || !process.env.DODO_WEBHOOK_SECRET) {
|
||||
console.warn("Missing webhook signature or secret");
|
||||
return false;
|
||||
}
|
||||
|
||||
// DodoPayments uses HMAC-SHA256 for webhook signatures
|
||||
const crypto = await import("crypto");
|
||||
const expectedSignature = crypto
|
||||
.createHmac("sha256", process.env.DODO_WEBHOOK_SECRET)
|
||||
.update(payload)
|
||||
.digest("hex");
|
||||
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(signature),
|
||||
Buffer.from(expectedSignature)
|
||||
);
|
||||
}
|
||||
|
||||
// Send welcome email with license key
|
||||
async function sendWelcomeEmail(
|
||||
email: string,
|
||||
|
|
@ -140,33 +120,63 @@ export async function POST(request: NextRequest) {
|
|||
|
||||
try {
|
||||
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 (!isValid) {
|
||||
console.error("Invalid webhook signature");
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid signature" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
// Extract Standard Webhooks headers
|
||||
const webhookHeaders = {
|
||||
"webhook-id": request.headers.get("webhook-id") ?? "",
|
||||
"webhook-signature": request.headers.get("webhook-signature") ?? "",
|
||||
"webhook-timestamp": request.headers.get("webhook-timestamp") ?? "",
|
||||
};
|
||||
|
||||
let event: DodoWebhookPayload;
|
||||
|
||||
// Verify signature using Dodo Payments SDK (follows Standard Webhooks spec)
|
||||
try {
|
||||
// unwrap() verifies the signature and returns the parsed payload
|
||||
const unwrapped = dodo.webhooks.unwrap(payload, { headers: webhookHeaders });
|
||||
event = unwrapped as unknown as DodoWebhookPayload;
|
||||
} catch (verifyError) {
|
||||
console.error("Webhook signature verification failed:", verifyError);
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid signature" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const event = JSON.parse(payload) as DodoWebhookPayload;
|
||||
// Use webhook-id header for idempotency (as per Standard Webhooks spec)
|
||||
// This prevents duplicate processing due to retries
|
||||
const webhookId = webhookHeaders["webhook-id"];
|
||||
const eventId = webhookId || getEventId(event);
|
||||
|
||||
// Check if we've already processed this webhook (idempotency)
|
||||
const existingEvent = await db.query.webhookEvents.findFirst({
|
||||
where: eq(webhookEvents.eventId, eventId),
|
||||
});
|
||||
|
||||
if (existingEvent?.processed) {
|
||||
console.log(`Webhook already processed: ${eventId}`);
|
||||
return NextResponse.json({ received: true });
|
||||
}
|
||||
|
||||
// Save webhook event to database
|
||||
const [savedEvent] = await db
|
||||
.insert(webhookEvents)
|
||||
.values({
|
||||
eventId: getEventId(event),
|
||||
eventId,
|
||||
eventName: event.type,
|
||||
provider: "dodo",
|
||||
payload: event,
|
||||
processed: false,
|
||||
})
|
||||
.onConflictDoNothing() // Handle race condition with duplicate webhook delivery
|
||||
.returning();
|
||||
|
||||
// If no event was inserted (duplicate), return success
|
||||
if (!savedEvent) {
|
||||
console.log(`Duplicate webhook received: ${eventId}`);
|
||||
return NextResponse.json({ received: true });
|
||||
}
|
||||
|
||||
webhookEventId = savedEvent.id;
|
||||
|
||||
switch (event.type) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue