diff --git a/packages/api-gateway/src/__tests__/merchant-suspension.test.ts b/packages/api-gateway/src/__tests__/merchant-suspension.test.ts new file mode 100644 index 0000000..e101d7b --- /dev/null +++ b/packages/api-gateway/src/__tests__/merchant-suspension.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect, vi } from "vitest"; +import { checkMerchantSuspension } from "../middleware/merchantCheck"; + +describe("Merchant Suspension Early Lifecycle Checks", () => { + it("should reject suspended merchants before handling heavy payloads or route handlers", async () => { + const mockRequest = { + headers: { "x-merchant-id": "merch_suspended_99" }, + params: {}, + } as any; + + const mockReply = { + status: vi.fn().mockReturnThis(), + send: vi.fn(), + } as any; + + await checkMerchantSuspension(mockRequest, mockReply); + + // Verify a consistent 403 Forbidden payload is fired upfront + expect(mockReply.status).toHaveBeenCalledWith(403); + expect(mockReply.send).toHaveBeenCalledWith(expect.objectContaining({ + code: "MERCHANT_SUSPENDED" + })); + }); +}); diff --git a/packages/api-gateway/src/app/merchant/routes.ts b/packages/api-gateway/src/app/merchant/routes.ts new file mode 100644 index 0000000..cff8697 --- /dev/null +++ b/packages/api-gateway/src/app/merchant/routes.ts @@ -0,0 +1,9 @@ +import { checkMerchantSuspension } from "../../middleware/merchantCheck"; + +// When defining your route blocks, register it as a preHandler: +fastify.register(async function (merchantRoutes) { + merchantRoutes.addHook("preHandler", checkMerchantSuspension); + + // Your route definitions here run safely *after* the suspension verification + merchantRoutes.get("/listings", async (req, res) => { /* ... */ }); +}); diff --git a/packages/api-gateway/src/middleware/merchantCheck.ts b/packages/api-gateway/src/middleware/merchantCheck.ts new file mode 100644 index 0000000..088226a --- /dev/null +++ b/packages/api-gateway/src/middleware/merchantCheck.ts @@ -0,0 +1,31 @@ +import { FastifyRequest, FastifyReply } from "fastify"; +import { getMerchantStatusFromDb } from "@/lib/db"; // Adjust matching database or cache layer string + +/** + * Early preHandler hook that intercepts requests before body parsing occurs. + * Rejects suspended merchants immediately to prevent resource consumption and information leaks. + */ +export async function checkMerchantSuspension(request: FastifyRequest, reply: FastifyReply) { + // Extract merchant identity context safely from headers or route parameters + const merchantId = (request.headers["x-merchant-id"] || request.params?.["merchantId"]) as string; + + if (!merchantId) { + return; // Pass through if not a merchant-scoped route context + } + + try { + const merchant = await getMerchantStatusFromDb(merchantId); + + if (merchant && merchant.status === "SUSPENDED") { + // Return a consistent suspension error explicitly before heavy work / body parsing begins + return reply.status(403).send({ + error: "Forbidden", + message: "Merchant account is suspended.", + code: "MERCHANT_SUSPENDED" + }); + } + } catch (error) { + // Fail closed for safety if the lookup fails + return reply.status(500).send({ error: "Internal validation failure." }); + } +}