mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
fix(meds): route every detail-page sub-route through assertMedicationOwnership
The intake purge endpoint, the parent medication PUT, the parent DELETE and the CSV-import POST each hand-rolled the same `findUnique + userId !== id ? 404` block instead of using the shared `assertMedicationOwnership` helper that every other `src/app/api/medications/[id]/**` handler already uses. The hand-rolled checks read slightly different fields and skipped the helper's narrow `select`, which leaked extra column data into routes that never needed it. Routing each of the four through the helper keeps the 404 leak shape on a single line, prepares the purge endpoint for promotion to a user-discoverable surface on the new detail page, and removes one of the open audit findings against the medications API. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ca1c52ffcb
commit
af2249644b
3 files changed
+49
-15
No files matched your search
@@ -4,12 +4,12 @@ import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
sanitiseZodIssues,
|
||||
} from "@/lib/api-response";
|
||||
import { assertMedicationOwnership } from "@/lib/medications/route-guards";
|
||||
import {
|
||||
recomputeMedicationComplianceForDay,
|
||||
dayKeyForScheduledFor,
|
||||
@@ -32,10 +32,10 @@ export const POST = apiHandler(
|
||||
const { user } = await requireAuth();
|
||||
|
||||
const { id } = await params;
|
||||
const medication = await prisma.medication.findUnique({ where: { id } });
|
||||
if (!medication || medication.userId !== user.id) {
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
// v1.5.5 C-E3-3 — route ownership through the shared helper so the
|
||||
// 404 leak shape stays identical across every `[id]/**` handler.
|
||||
const guard = await assertMedicationOwnership(id, user.id);
|
||||
if (guard) return guard;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request);
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
|
||||
import { apiSuccess, getClientIp } from "@/lib/api-response";
|
||||
import { assertMedicationOwnership } from "@/lib/medications/route-guards";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
type RouteParams = { params: Promise<{ id: string }> };
|
||||
@@ -12,10 +13,20 @@ export const DELETE = apiHandler(
|
||||
const { user } = await requireAuth();
|
||||
|
||||
const { id } = await params;
|
||||
const medication = await prisma.medication.findUnique({ where: { id } });
|
||||
if (!medication || medication.userId !== user.id) {
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
// v1.5.5 C-E3-3 — route purge through the same ownership predicate
|
||||
// every other `src/app/api/medications/[id]/**` handler uses. The
|
||||
// hand-rolled `findUnique + userId !== id ? 404` block returned the
|
||||
// medication row only to read its name for the audit details. The
|
||||
// audit row still wants the name; re-read it after the guard so the
|
||||
// ownership check stays the single source of truth on the 404 leak
|
||||
// shape.
|
||||
const guard = await assertMedicationOwnership(id, user.id);
|
||||
if (guard) return guard;
|
||||
|
||||
const medication = await prisma.medication.findUnique({
|
||||
where: { id },
|
||||
select: { name: true },
|
||||
});
|
||||
|
||||
const { count } = await prisma.medicationIntakeEvent.deleteMany({
|
||||
where: { medicationId: id, userId: user.id },
|
||||
@@ -31,7 +42,11 @@ export const DELETE = apiHandler(
|
||||
await auditLog("medication.intake.purge", {
|
||||
userId: user.id,
|
||||
ipAddress: getClientIp(request),
|
||||
details: { medicationId: id, name: medication.name, deletedCount: count },
|
||||
details: {
|
||||
medicationId: id,
|
||||
name: medication?.name ?? "",
|
||||
deletedCount: count,
|
||||
},
|
||||
});
|
||||
|
||||
annotate({
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
setMedicationCategory,
|
||||
} from "@/lib/medication-category";
|
||||
import { serializeScheduleRecurrence } from "@/lib/medication-schedule";
|
||||
import { assertMedicationOwnership } from "@/lib/medications/route-guards";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
type RouteParams = { params: Promise<{ id: string }> };
|
||||
@@ -60,8 +61,18 @@ export const PUT = apiHandler(
|
||||
const { user } = await requireAuth();
|
||||
|
||||
const { id } = await params;
|
||||
const existing = await prisma.medication.findUnique({ where: { id } });
|
||||
if (!existing || existing.userId !== user.id) {
|
||||
// v1.5.5 C-E3-3 — route ownership check through the shared helper
|
||||
// so the 404 leak shape stays identical across every
|
||||
// `[id]/**` handler. The PUT branch still needs `existing.active`
|
||||
// for the pausedAt-derivation step below, so re-read the narrow
|
||||
// field after the guard passes.
|
||||
const guard = await assertMedicationOwnership(id, user.id);
|
||||
if (guard) return guard;
|
||||
const existing = await prisma.medication.findUnique({
|
||||
where: { id },
|
||||
select: { active: true },
|
||||
});
|
||||
if (!existing) {
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
|
||||
@@ -263,8 +274,16 @@ export const DELETE = apiHandler(
|
||||
const { user } = await requireAuth();
|
||||
|
||||
const { id } = await params;
|
||||
const existing = await prisma.medication.findUnique({ where: { id } });
|
||||
if (!existing || existing.userId !== user.id) {
|
||||
// v1.5.5 C-E3-3 — ownership via the shared helper. The DELETE
|
||||
// branch needs the medication name for the audit-row detail; pull
|
||||
// it after the guard passes.
|
||||
const guard = await assertMedicationOwnership(id, user.id);
|
||||
if (guard) return guard;
|
||||
const existing = await prisma.medication.findUnique({
|
||||
where: { id },
|
||||
select: { name: true },
|
||||
});
|
||||
if (!existing) {
|
||||
return apiError("Medication not found", 404);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user