mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
Let a delegate enter a lab result and the marker it needs
POST /api/labs and POST /api/biomarkers resolve the record. The free-text lab path mints its biomarker under the record's catalogue rather than the caller's — a result added to somebody's record whose marker landed on the helper's account would leave the owner with a reading they cannot chart. Adding a marker the record already tracks stays the ordinary 409 from the same per-account uniqueness the owner would hit themselves. The PUT and DELETE on the sibling routes are unchanged and still refuse under a switch. Both validation breadcrumbs move to auditLog(), the only writer that stamps the acting account.
This commit is contained in:
1 parent
17c4e76165
commit
c6be0778dc
4 files changed
+38
-26
No files matched your search
@@ -34,6 +34,7 @@ vi.mock("next/headers", () => ({
|
||||
|
||||
import { POST } from "../route";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
|
||||
const SESSION = {
|
||||
@@ -53,6 +54,10 @@ beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION as never);
|
||||
vi.mocked(prisma.auditLog.create).mockResolvedValue({} as never);
|
||||
// v1.36.x — the create arm's validation breadcrumb goes through the
|
||||
// `auditLog` helper, the only writer that stamps `actorUserId`. The route
|
||||
// chains `.catch()` onto it, so the stub has to return a promise.
|
||||
vi.mocked(auditLog).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
describe("biomarker create error contract", () => {
|
||||
|
||||
@@ -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,
|
||||
@@ -91,7 +91,11 @@ export const GET = apiHandler(async () => {
|
||||
export const POST = apiHandler(withIdempotency<[NextRequest]>(postBiomarker));
|
||||
|
||||
async function postBiomarker(request: NextRequest) {
|
||||
const { user } = await requireAuth();
|
||||
// v1.36.x — a delegated write: the marker joins the record's catalogue. The
|
||||
// duplicate pre-check and the `@@unique([userId, name])` backstop both scope
|
||||
// to the same resolved id, so a delegate adding a marker the owner already
|
||||
// tracks gets the ordinary 409 rather than a second copy.
|
||||
const { user } = await requireRecordAuth("write");
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
maxBytes: 16 * 1024,
|
||||
@@ -107,17 +111,13 @@ async function postBiomarker(request: NextRequest) {
|
||||
const auditIssues = sanitiseZodIssues(parsed.error.issues, {
|
||||
stripValuesFromMessage: true,
|
||||
});
|
||||
prisma.auditLog
|
||||
.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
action: "labs.biomarker.create.validation-failed",
|
||||
details: JSON.stringify({ issues: auditIssues }),
|
||||
},
|
||||
})
|
||||
.catch(() => {
|
||||
/* swallow — the 422 response is the contract */
|
||||
});
|
||||
// v1.36.x — through `auditLog()`, the only writer that stamps the actor.
|
||||
void auditLog("labs.biomarker.create.validation-failed", {
|
||||
userId: user.id,
|
||||
details: { issues: auditIssues },
|
||||
}).catch(() => {
|
||||
/* swallow — the 422 response is the contract */
|
||||
});
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "labs.biomarker.create.invalid",
|
||||
});
|
||||
|
||||
@@ -50,6 +50,7 @@ vi.mock("next/headers", () => ({
|
||||
import { POST } from "../route";
|
||||
import { PUT } from "../[id]/route";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
|
||||
const SESSION = {
|
||||
@@ -93,6 +94,10 @@ beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION as never);
|
||||
vi.mocked(prisma.auditLog.create).mockResolvedValue({} as never);
|
||||
// v1.36.x — the create arm's validation breadcrumb goes through the
|
||||
// `auditLog` helper, the only writer that stamps `actorUserId`. The route
|
||||
// chains `.catch()` onto it, so the stub has to return a promise.
|
||||
vi.mocked(auditLog).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
describe("lab result error contract", () => {
|
||||
|
||||
+15
-13
@@ -1,7 +1,7 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { fireAndForget } from "@/lib/logging/fire-and-forget";
|
||||
|
||||
import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler";
|
||||
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
@@ -139,7 +139,11 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
export const POST = apiHandler(withIdempotency<[NextRequest]>(postLabResult));
|
||||
|
||||
async function postLabResult(request: NextRequest) {
|
||||
const { user } = await requireAuth();
|
||||
// v1.36.x — a delegated write. The free-text path may MINT a biomarker, and
|
||||
// it mints it under `user.id`: the marker joins the record's catalogue, not
|
||||
// the caller's, which is the only reading of "add a result to your record"
|
||||
// that leaves the owner with a usable catalogue afterwards.
|
||||
const { user } = await requireRecordAuth("write");
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
maxBytes: 16 * 1024,
|
||||
@@ -157,17 +161,15 @@ async function postLabResult(request: NextRequest) {
|
||||
const auditIssues = sanitiseZodIssues(parsed.error.issues, {
|
||||
stripValuesFromMessage: true,
|
||||
});
|
||||
prisma.auditLog
|
||||
.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
action: "labs.create.validation-failed",
|
||||
details: JSON.stringify({ issues: auditIssues }),
|
||||
},
|
||||
})
|
||||
.catch(() => {
|
||||
/* swallow — the 422 response is the contract */
|
||||
});
|
||||
// v1.36.x — through `auditLog()`, the only writer that stamps
|
||||
// `actorUserId`. A bare `prisma.auditLog.create` files a delegate's
|
||||
// malformed payload as if the owner had typed it.
|
||||
void auditLog("labs.create.validation-failed", {
|
||||
userId: user.id,
|
||||
details: { issues: auditIssues },
|
||||
}).catch(() => {
|
||||
/* swallow — the 422 response is the contract */
|
||||
});
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "labs.create.invalid",
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user