mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
refactor(api): read the remaining settings and admin route bodies through parseJsonBody
Admin, coach, consent, chart-overlay, device registration, insights feedback and provider chain, ECG ingest, onboarding, search and the AI provider settings move to parseJsonBody with the same message, error code, status and wide-event action they answered with. A refusal whose annotate meta is computed from the issues, or puts its keys in another order than the helper would, keeps that annotate verbatim in onInvalid. The routes that still call safeJson do something between the read and the schema: an optimistic-concurrency token split off the body, a count cap answered first, a refunded quota slot, a conditional read, a non-Zod check, or the body handed to a service.
This commit is contained in:
1 parent
af14d0aa7e
commit
ca04ef01b3
19 files changed
+203
-411
No files matched your search
@@ -31,14 +31,8 @@ import type { NextRequest } from "next/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { apiHandler, requireAuth, HttpError } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
apiValidationError,
|
||||
getClientIp,
|
||||
safeJson,
|
||||
sanitiseZodIssues,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
@@ -79,28 +73,19 @@ async function handlePost(request: NextRequest, ctx: RouteContext) {
|
||||
return apiError("Too many feedback submissions, try again later", 429);
|
||||
}
|
||||
|
||||
const { data: rawBody, error } = await safeJson<unknown>(request, {
|
||||
// The dotted token keeps its place in `error`: this refusal keeps its 422,
|
||||
// so moving the string would be a wire change with nothing forcing it —
|
||||
// the `invalid_json` siblings moved only because their status moved to 400
|
||||
// anyway. `meta.errorCode` publishes the same token in the field a machine
|
||||
// code belongs in, and `details.issues` names the fields that were refused.
|
||||
const parsed = await parseJsonBody(request, coachMessageFeedbackSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
action: "insights.coach.message.feedback",
|
||||
meta: { outcome: "validation_failed" },
|
||||
message: "feedback.body.invalid",
|
||||
errorCode: "feedback.body.invalid",
|
||||
});
|
||||
if (error) return error;
|
||||
const parsed = coachMessageFeedbackSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
annotate({
|
||||
action: { name: "insights.coach.message.feedback" },
|
||||
meta: { outcome: "validation_failed" },
|
||||
});
|
||||
// The dotted token keeps its place in `error`: this refusal keeps its 422,
|
||||
// so moving the string would be a wire change with nothing forcing it —
|
||||
// the `invalid_json` siblings moved only because their status moved to 400
|
||||
// anyway. `meta.errorCode` publishes the same token in the field a machine
|
||||
// code belongs in, and `details.issues` names the fields that were refused.
|
||||
return apiValidationError(
|
||||
"feedback.body.invalid",
|
||||
sanitiseZodIssues(parsed.error.issues),
|
||||
422,
|
||||
{ errorCode: "feedback.body.invalid" },
|
||||
);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const body = parsed.data;
|
||||
|
||||
// Verify ownership only — we no longer touch encryptedContent. The
|
||||
|
||||
@@ -35,12 +35,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
@@ -223,27 +219,24 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
return apiError("Too many ECG submissions, try again later", 429);
|
||||
}
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson<unknown>(request, {
|
||||
const parsed = await parseJsonBody(request, ecgIngestSchema, {
|
||||
maxBytes: MAX_BODY_BYTES,
|
||||
onInvalid: (error) => {
|
||||
// Which field was refused and why, never the value: without it a refused
|
||||
// recording is a bare "invalid" in the log and the cause has to be guessed.
|
||||
annotate({
|
||||
action: { name: "insights.ecg.ingest" },
|
||||
meta: {
|
||||
outcome: "invalid",
|
||||
invalidFields: error.issues
|
||||
.map((issue) => `${issue.path.join(".") || "(body)"}:${issue.code}`)
|
||||
.slice(0, 8)
|
||||
.join(","),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = ecgIngestSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
// Which field was refused and why, never the value: without it a refused
|
||||
// recording is a bare "invalid" in the log and the cause has to be guessed.
|
||||
annotate({
|
||||
action: { name: "insights.ecg.ingest" },
|
||||
meta: {
|
||||
outcome: "invalid",
|
||||
invalidFields: parsed.error.issues
|
||||
.map((issue) => `${issue.path.join(".") || "(body)"}:${issue.code}`)
|
||||
.slice(0, 8)
|
||||
.join(","),
|
||||
},
|
||||
});
|
||||
return returnAllZodIssues(parsed.error);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const body = parsed.data;
|
||||
const result = await persistEcgRecording(
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
apiValidationError,
|
||||
getClientIp,
|
||||
safeJson,
|
||||
sanitiseZodIssues,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
@@ -57,23 +51,17 @@ async function handlePost(request: NextRequest) {
|
||||
return apiError("Too many feedback submissions, try again later", 429);
|
||||
}
|
||||
|
||||
const { data: rawBody, error } = await safeJson<unknown>(request, {
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
if (error) return error;
|
||||
|
||||
const parsed = recommendationFeedbackRequestSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
annotate({
|
||||
action: { name: "insights.recommendation.feedback" },
|
||||
const parsed = await parseJsonBody(
|
||||
request,
|
||||
recommendationFeedbackRequestSchema,
|
||||
{
|
||||
maxBytes: 64 * 1024,
|
||||
action: "insights.recommendation.feedback",
|
||||
meta: { outcome: "validation_failed" },
|
||||
});
|
||||
return apiValidationError(
|
||||
"Invalid feedback payload",
|
||||
sanitiseZodIssues(parsed.error.issues),
|
||||
422,
|
||||
);
|
||||
}
|
||||
message: "Invalid feedback payload",
|
||||
},
|
||||
);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const body = parsed.data;
|
||||
|
||||
const attribution = await resolveFeedbackAttribution(user.id);
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { apiHandler, HttpError, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiValidationError,
|
||||
safeJson,
|
||||
sanitiseZodIssues,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { probeProviderChain, resolveProviderChain } from "@/lib/ai/provider";
|
||||
import {
|
||||
@@ -90,19 +86,11 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAuth();
|
||||
annotate({ action: { name: "insights.provider_chain.update" } });
|
||||
|
||||
const { data: body, error } = await safeJson<unknown>(request, {
|
||||
const parsed = await parseJsonBody(request, providerChainPutSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
message: "Invalid provider chain payload",
|
||||
});
|
||||
if (error) return error;
|
||||
|
||||
const parsed = providerChainPutSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiValidationError(
|
||||
"Invalid provider chain payload",
|
||||
sanitiseZodIssues(parsed.error.issues),
|
||||
422,
|
||||
);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
// Defence-in-depth: reject duplicate provider types so a stale-tab
|
||||
// resubmit cannot create a chain that walks the same entry twice.
|
||||
|
||||
Reference in new issue
Block a user