feat(doctor-report): wire the doctorReport module toggle to the health-record surface

The doctorReport module toggle was inert: turning it off hid nothing and the
server-side export route accepted requests regardless. Gate the live surface
on the module — the Health-record settings entry now hides client-side when
the module is off, and POST /api/export/health-record refuses with a 403
module.disabled envelope, so the doctor-report surface cannot be reached over a
Bearer token either.
This commit is contained in:
Marc-André Bombeck committed 2026-06-16 02:00:48 +02:00
1 parent 69fe67e2de
commit b6db529dee
5 files changed
+46 -2

No files matched your search

@@ -132,6 +132,21 @@ describe("POST /api/export/health-record — validation", () => {
expect(res.status).toBe(422);
});
it("returns 403 when the doctorReport module is disabled (B3 gate)", async () => {
const { apiError } = await import("@/lib/api-response");
vi.mocked(requireModuleEnabled).mockResolvedValue({
enabled: false,
response: apiError('Module "doctorReport" is not enabled', 403, {
errorCode: "module.disabled",
module: "doctorReport",
}),
} as never);
const { POST } = await import("../route");
const res = await POST(mkReq({ format: "fhir" }));
expect(res.status).toBe(403);
expect(requireModuleEnabled).toHaveBeenCalledWith("user-1", "doctorReport");
});
it("returns 429 when the export rate limit is exhausted", async () => {
vi.mocked(checkRateLimit).mockResolvedValue({
allowed: false,
@@ -31,6 +31,7 @@ import {
safeJson,
} from "@/lib/api-response";
import { checkRateLimit } from "@/lib/rate-limit";
import { requireModuleEnabled } from "@/lib/modules/gate";
import { prisma } from "@/lib/db";
import { decrypt } from "@/lib/crypto";
import {
@@ -56,6 +57,13 @@ export const POST = apiHandler(async (request: NextRequest) => {
const { user } = await requireAuth();
annotate({ action: { name: "export.health-record.build" } });
// v1.18.0 B3 — the whole doctor-report / health-record surface is the
// `doctorReport` module. Refuse with a 403 `module.disabled` envelope when
// the account turned it off — even with a valid Bearer token. The settings
// entry-point already hides client-side; this is the hard enforcement.
const gate = await requireModuleEnabled(user.id, "doctorReport");
if (!gate.enabled) return gate.response;
const rl = await checkRateLimit(`export:${user.id}`, 10, 60 * 60 * 1000);
if (!rl.allowed) {
return apiError("Maximum 10 exports per hour", 429);