security hardening
This commit is contained in:
@@ -5,7 +5,7 @@ import {
|
||||
generateAndEmailInvoice,
|
||||
isManualPaymentOrder,
|
||||
} from "../services/invoice/automations.server";
|
||||
import { isDuplicateWebhook } from "../services/webhooks/dedupe.server";
|
||||
import { reserveWebhook } from "../services/webhooks/dedupe.server";
|
||||
import { runWebhookInBackground } from "../services/webhooks/background.server";
|
||||
|
||||
/**
|
||||
@@ -18,42 +18,56 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const { shop, topic, payload, session, admin } = await authenticate.webhook(request);
|
||||
console.log(`Received ${topic} webhook for ${shop}`);
|
||||
|
||||
// Idempotency against Shopify retries — see webhooks/dedupe.server.ts.
|
||||
if (await isDuplicateWebhook(request, shop, topic)) return new Response();
|
||||
// Reserve/commit dedupe — see webhooks/dedupe.server.ts. `null` => already
|
||||
// done/in-flight; commit on success / release on failure happen in the
|
||||
// background queue so a failed send is retried by Shopify, not dropped.
|
||||
const reservation = await reserveWebhook(request, shop, topic);
|
||||
if (!reservation) return new Response();
|
||||
|
||||
if (!session || !admin) {
|
||||
// App was uninstalled before the webhook drained — nothing to do.
|
||||
await reservation.commit();
|
||||
return new Response();
|
||||
}
|
||||
|
||||
const orderId = payload?.id;
|
||||
if (orderId == null) return new Response();
|
||||
if (orderId == null) {
|
||||
await reservation.commit();
|
||||
return new Response();
|
||||
}
|
||||
|
||||
const customerLocale =
|
||||
typeof payload?.customer_locale === "string" ? payload.customer_locale : undefined;
|
||||
|
||||
// Respond fast; do the heavy lifting after the response (see notes in
|
||||
// webhooks.orders.create.tsx for the rationale).
|
||||
runWebhookInBackground(`${topic} order=${orderId} shop=${shop}`, async () => {
|
||||
const settings = await db.shopSettings.findUnique({ where: { shopDomain: shop } });
|
||||
if (!settings?.autoEmailOnFulfilledNonWireTransfer) return;
|
||||
runWebhookInBackground(
|
||||
`${topic} order=${orderId} shop=${shop}`,
|
||||
async () => {
|
||||
const settings = await db.shopSettings.findUnique({ where: { shopDomain: shop } });
|
||||
if (!settings?.autoEmailOnFulfilledNonWireTransfer) return;
|
||||
|
||||
const orderGid = `gid://shopify/Order/${String(orderId).replace(/^.*\//, "")}`;
|
||||
if (await isManualPaymentOrder(admin, orderGid)) {
|
||||
// Manual / wire-transfer order — handled by Automation 1, skip here.
|
||||
return;
|
||||
}
|
||||
const orderGid = `gid://shopify/Order/${String(orderId).replace(/^.*\//, "")}`;
|
||||
if (await isManualPaymentOrder(admin, orderGid)) {
|
||||
// Manual / wire-transfer order — handled by Automation 1, skip here.
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await generateAndEmailInvoice({
|
||||
shopDomain: shop,
|
||||
admin,
|
||||
orderId,
|
||||
customerLocale,
|
||||
});
|
||||
if (!result.ok) {
|
||||
console.warn(`auto-email (fulfilled) failed for order ${orderId} on ${shop}: ${result.reason}`);
|
||||
}
|
||||
});
|
||||
const result = await generateAndEmailInvoice({
|
||||
shopDomain: shop,
|
||||
admin,
|
||||
orderId,
|
||||
customerLocale,
|
||||
});
|
||||
if (!result.ok) {
|
||||
// Throw so the reservation is released and Shopify retries.
|
||||
throw new Error(
|
||||
`auto-email (fulfilled) failed for order ${orderId} on ${shop}: ${result.reason}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
reservation,
|
||||
);
|
||||
|
||||
return new Response();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user