diff --git a/src/app/api/allergies/route.ts b/src/app/api/allergies/route.ts index 8ae18805b..f29719ded 100644 --- a/src/app/api/allergies/route.ts +++ b/src/app/api/allergies/route.ts @@ -11,7 +11,7 @@ import { NextRequest } from "next/server"; import { prisma } from "@/lib/db"; -import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler"; +import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { auditLog } from "@/lib/auth/audit"; import { @@ -67,7 +67,10 @@ export const GET = apiHandler(async (request: NextRequest) => { export const POST = apiHandler(withIdempotency<[NextRequest]>(postAllergy)); async function postAllergy(request: NextRequest): Promise { - const { user } = await requireAuth(); + // v1.36.x — a delegated write, and the cleanest of them: the row is a plain + // statement about the record's own body, and the caller appears in the audit + // trail rather than in the row. + const { user } = await requireRecordAuth("write"); const { data: rawBody, error: jsonError } = await safeJson(request, { maxBytes: 16 * 1024, diff --git a/src/app/api/custom-metrics/[id]/entries/route.ts b/src/app/api/custom-metrics/[id]/entries/route.ts index b6963086c..dd9d45344 100644 --- a/src/app/api/custom-metrics/[id]/entries/route.ts +++ b/src/app/api/custom-metrics/[id]/entries/route.ts @@ -1,6 +1,6 @@ import { NextRequest } from "next/server"; -import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler"; +import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { apiError, apiSuccess, @@ -89,7 +89,11 @@ async function postCustomMetricEntry( request: NextRequest, { params }: RouteParams, ) { - const { user } = await requireAuth(); + // v1.36.x — a delegated write. The ownership hop runs first and now resolves + // against the record, so a delegate can only add a value to a metric the + // OWNER defined; their own metric of the same name is unreachable from in + // here and 404s. + const { user } = await requireRecordAuth("write"); const { id } = await params; const metric = await prisma.customMetric.findFirst({ @@ -114,17 +118,13 @@ async function postCustomMetricEntry( const auditIssues = sanitiseZodIssues(parsed.error.issues, { stripValuesFromMessage: true, }); - prisma.auditLog - .create({ - data: { - userId: user.id, - action: "custom-metric.entry.create.validation-failed", - details: JSON.stringify({ issues: auditIssues, customMetricId: id }), - }, - }) - .catch(() => { - /* swallow — the 422 response is the contract */ - }); + // v1.36.x — through `auditLog()`, the only writer that stamps the actor. + void auditLog("custom-metric.entry.create.validation-failed", { + userId: user.id, + details: { issues: auditIssues, customMetricId: id }, + }).catch(() => { + /* swallow — the 422 response is the contract */ + }); return returnAllZodIssues(parsed.error, 422); } diff --git a/src/app/api/family-history/route.ts b/src/app/api/family-history/route.ts index d58cb44d4..d560de87e 100644 --- a/src/app/api/family-history/route.ts +++ b/src/app/api/family-history/route.ts @@ -12,7 +12,7 @@ import { NextRequest } from "next/server"; import { prisma } from "@/lib/db"; -import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler"; +import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { auditLog } from "@/lib/auth/audit"; import { @@ -62,7 +62,11 @@ export const POST = apiHandler( ); async function postFamilyHistory(request: NextRequest): Promise { - const { user } = await requireAuth(); + // v1.36.x — a delegated write, and the one where third-party health data is + // present by design: the row describes the record's relatives. It is stored + // as the record's own, exactly as the delegable READ arm above already + // serves it, and the audit trail names who entered it. + const { user } = await requireRecordAuth("write"); const { data: rawBody, error: jsonError } = await safeJson(request, { maxBytes: 16 * 1024, diff --git a/src/app/api/illness/episodes/route.ts b/src/app/api/illness/episodes/route.ts index a4a0d0d11..fcb41029c 100644 --- a/src/app/api/illness/episodes/route.ts +++ b/src/app/api/illness/episodes/route.ts @@ -13,7 +13,7 @@ import { NextRequest } from "next/server"; import { prisma } from "@/lib/db"; -import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler"; +import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { auditLog } from "@/lib/auth/audit"; import { @@ -74,7 +74,11 @@ export const GET = apiHandler(async (request: NextRequest) => { export const POST = apiHandler(withIdempotency<[NextRequest]>(postEpisode)); async function postEpisode(request: NextRequest): Promise { - const { user } = await requireAuth(); + // v1.36.x — a delegated write. The module gate below follows the RECORD, not + // the caller: whether illness tracking exists is a property of the record, so + // a delegate who runs the module on their own account does not thereby gain + // an episode surface inside somebody else's. + const { user } = await requireRecordAuth("write"); const gate = await requireIllnessEnabled(user.id); if (!gate.enabled) return gate.response;