Files
ai_web/server/webhooks.ts
2026-01-27 13:41:31 +08:00

116 lines
3.8 KiB
TypeScript

import express, { Request, Response } from "express";
import { stripe } from "./stripe";
import { ENV } from "./_core/env";
import * as db from "./db";
export function registerWebhookRoutes(app: express.Application) {
// Stripe webhook endpoint - must use raw body for signature verification
app.post(
"/api/stripe/webhook",
express.raw({ type: "application/json" }),
async (req: Request, res: Response) => {
const sig = req.headers["stripe-signature"];
if (!sig) {
console.log("[Webhook] Missing stripe-signature header");
return res.status(400).send("Missing stripe-signature header");
}
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
ENV.stripeWebhookSecret
);
} catch (err: any) {
console.log(`[Webhook] Signature verification failed: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle test events
if (event.id.startsWith("evt_test_")) {
console.log("[Webhook] Test event detected, returning verification response");
return res.json({
verified: true,
});
}
console.log(`[Webhook] Received event: ${event.type} (${event.id})`);
try {
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object;
console.log(`[Webhook] Checkout session completed: ${session.id}`);
// Extract metadata
const bountyId = session.metadata?.bounty_id;
const userId = session.metadata?.user_id;
const type = session.metadata?.type;
if (type === "bounty_escrow" && bountyId) {
// Update bounty as escrowed
await db.updateBounty(parseInt(bountyId), {
isEscrowed: true,
stripePaymentIntentId: session.payment_intent as string,
});
console.log(`[Webhook] Bounty ${bountyId} marked as escrowed`);
// Notify the bounty publisher
if (userId) {
await db.createNotification({
userId: parseInt(userId),
type: "payment_received",
title: "赏金托管成功",
content: "您的悬赏赏金已成功托管,悬赏现已开放接单",
relatedId: parseInt(bountyId),
relatedType: "bounty",
});
}
}
break;
}
case "payment_intent.succeeded": {
const paymentIntent = event.data.object;
console.log(`[Webhook] Payment intent succeeded: ${paymentIntent.id}`);
break;
}
case "transfer.created": {
const transfer = event.data.object;
console.log(`[Webhook] Transfer created: ${transfer.id}`);
const bountyId = transfer.metadata?.bounty_id;
if (bountyId) {
await db.updateBounty(parseInt(bountyId), {
stripeTransferId: transfer.id,
isPaid: true,
});
console.log(`[Webhook] Bounty ${bountyId} marked as paid`);
}
break;
}
case "account.updated": {
const account = event.data.object;
console.log(`[Webhook] Account updated: ${account.id}`);
// Account status changes are handled on-demand via API
break;
}
default:
console.log(`[Webhook] Unhandled event type: ${event.type}`);
}
res.json({ received: true });
} catch (error) {
console.error(`[Webhook] Error processing event: ${error}`);
res.status(500).json({ error: "Webhook handler failed" });
}
}
);
}