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:
Marc-André Bombeck committed 2026-10-23 11:29:23 +02:00
1 parent af14d0aa7e
commit ca04ef01b3
19 files changed
+203 -411

No files matched your search

@@ -11,13 +11,8 @@ import type { NextRequest } from "next/server";
import { z } from "zod/v4";
import { apiHandler, requireAdmin } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} 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 { annotate } from "@/lib/logging/context";
import {
@@ -38,10 +33,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
const { user } = await requireAdmin();
annotate({ action: { name: "admin.encryption.keyBackup.confirm" } });
const body = await safeJson(request, { maxBytes: 1024 });
if (body.error) return body.error;
const parsed = confirmSchema.safeParse(body.data);
if (!parsed.success) return returnAllZodIssues(parsed.error);
const parsed = await parseJsonBody(request, confirmSchema, {
maxBytes: 1024,
});
if (!parsed.ok) return parsed.error;
const result = await confirmKeyBackup({
keyId: parsed.data.keyId,
+5 -13
View File
@@ -13,12 +13,8 @@
*/
import { prisma } from "@/lib/db";
import { apiHandler, requireAdmin } from "@/lib/api-handler";
import {
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { hashToken } from "@/lib/auth/hmac";
@@ -62,15 +58,11 @@ export const GET = apiHandler(async () => {
export const POST = apiHandler(async (req: Request) => {
const { user } = await requireAdmin();
const { data: body, error: jsonError } = await safeJson(req, {
const parsed = await parseJsonBody(req, inviteCreateSchema, {
maxBytes: 16 * 1024,
input: (body) => body ?? {},
});
if (jsonError) return jsonError;
const parsed = inviteCreateSchema.safeParse(body ?? {});
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const { expiresInDays, maxUses } = parsed.data;
const rawToken = generateInviteToken();
@@ -1,14 +1,8 @@
import { prisma } from "@/lib/db";
import { apiHandler, requireAdmin } from "@/lib/api-handler";
import { auditLog } from "@/lib/auth/audit";
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 { annotate } from "@/lib/logging/context";
import { encrypt } from "@/lib/crypto";
import { z } from "zod/v4";
@@ -53,19 +47,12 @@ export const POST = apiHandler(async (request: NextRequest) => {
const { user } = await requireAdmin();
annotate({ action: { name: "admin.webPushVapid.generate" } });
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, generateSchema, {
maxBytes: 4 * 1024,
input: (body) => body ?? {},
message: "Invalid request",
});
if (jsonError) return jsonError;
const parsed = generateSchema.safeParse(body ?? {});
if (!parsed.success) {
return apiValidationError(
"Invalid request",
sanitiseZodIssues(parsed.error.issues),
422,
);
}
if (!parsed.ok) return parsed.error;
const existing = await prisma.appSettings.findUnique({
where: { id: "singleton" },