mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
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:
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);
|
||||
|
||||
@@ -235,6 +235,19 @@ describe("<SettingsShell>", () => {
|
||||
expect(disabled).not.toContain('href="/settings/coach"');
|
||||
});
|
||||
|
||||
it("module-gates the Health-record entry off `user.modules.doctorReport` (B3)", () => {
|
||||
// Fail-open default: doctorReport undefined → entry shown.
|
||||
expect(renderShell({ active: "account" })).toContain(
|
||||
'href="/settings/gesundheitsakte"',
|
||||
);
|
||||
// Explicitly disabled → entry hidden.
|
||||
const disabled = renderShell({
|
||||
active: "account",
|
||||
modules: { doctorReport: false },
|
||||
});
|
||||
expect(disabled).not.toContain('href="/settings/gesundheitsakte"');
|
||||
});
|
||||
|
||||
it("falls back to `account` when the pathname doesn't match a known slug", () => {
|
||||
const html = renderShell({ pathname: "/settings/totally-bogus" });
|
||||
const activeLinkRegex =
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
* the generic CSV / JSON data-out paths. Export & Import now keeps only
|
||||
* the generic data export/import surfaces.
|
||||
*
|
||||
* Not module-gated: the health record is always available.
|
||||
* Module-gated on `doctorReport` (v1.18.0 B3): the nav entry hides when the
|
||||
* module is off (see `SETTINGS_SECTIONS` in `settings-shell.tsx`), and the
|
||||
* server-side `/api/export/health-record` route refuses with a 403
|
||||
* `module.disabled` envelope — so a disabled doctor-report surface cannot be
|
||||
* reached over a Bearer token either.
|
||||
*/
|
||||
|
||||
import { HealthRecordExportPanel } from "@/components/settings/health-record-export-panel";
|
||||
|
||||
@@ -157,11 +157,15 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
|
||||
},
|
||||
{ slug: "api", titleKey: "settings.sections.api.title", icon: KeyRound },
|
||||
// v1.18.0 (S5) — the full health record (PDF + FHIR R4 + zip package)
|
||||
// earns its own home, lifted out of Export & Import. Always available.
|
||||
// earns its own home, lifted out of Export & Import. Gated on the
|
||||
// `doctorReport` module: when the user turns the doctor-report surface off,
|
||||
// the entry hides (the server-side `/api/export/health-record` gate is the
|
||||
// hard enforcement; this hides the entry-point to match).
|
||||
{
|
||||
slug: "gesundheitsakte",
|
||||
titleKey: "settings.sections.gesundheitsakte.title",
|
||||
icon: FileHeart,
|
||||
moduleGate: "doctorReport",
|
||||
},
|
||||
{
|
||||
slug: "export",
|
||||
|
||||
Reference in new issue
Block a user