mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
refactor(api): read route bodies through parseJsonBody
132 route handlers spelled out safeJson, safeParse and the 422 refusal by hand in one of six shapes. Each block becomes one parseJsonBody call with the same size cap, wide-event action, issue count, error code, message and status it had, so every response is the one it was. The comments inside the replaced refusal blocks go with them. A ratchet (route-body-parse-ratchet) counts the route files that still call safeJson themselves; the count may only fall.
This commit is contained in:
1 parent
07e1e83ee8
commit
5b49fa6680
134 files changed
+838
-1874
No files matched your search
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"$comment": "safeJson calls per route file under src/app; new body-reading routes use parseJsonBody. Maintained by src/__tests__/route-body-parse-ratchet.test.ts.",
|
||||
"count": 106,
|
||||
"total": 106,
|
||||
"entries": {
|
||||
"app/api/admin/ai-settings/route.ts": 1,
|
||||
"app/api/admin/drain-per-sample-cumulative/route.ts": 1,
|
||||
"app/api/admin/encryption/key-backup/confirm/route.ts": 1,
|
||||
"app/api/admin/encryption/key-backup/verify/route.ts": 1,
|
||||
"app/api/admin/invites/route.ts": 1,
|
||||
"app/api/admin/notifications/reminder-check/route.ts": 1,
|
||||
"app/api/admin/rollups/recompute/route.ts": 1,
|
||||
"app/api/admin/settings/web-push-vapid/generate/route.ts": 1,
|
||||
"app/api/admin/users/[id]/reset-password/route.ts": 1,
|
||||
"app/api/ai/test/route.ts": 1,
|
||||
"app/api/anamnesis/emergency/route.ts": 1,
|
||||
"app/api/anamnesis/facts/[id]/route.ts": 1,
|
||||
"app/api/anamnesis/facts/route.ts": 1,
|
||||
"app/api/auth/login/route.ts": 1,
|
||||
"app/api/auth/me/locale/route.ts": 1,
|
||||
"app/api/auth/me/mfa/disable/route.ts": 1,
|
||||
"app/api/auth/me/mfa/totp/confirm/route.ts": 1,
|
||||
"app/api/auth/me/timezone/route.ts": 1,
|
||||
"app/api/auth/mfa/verify/route.ts": 1,
|
||||
"app/api/auth/mfa/webauthn/verify/route.ts": 1,
|
||||
"app/api/auth/passkey/login-options/route.ts": 1,
|
||||
"app/api/auth/profile/route.ts": 1,
|
||||
"app/api/auth/refresh/route.ts": 1,
|
||||
"app/api/biomarkers/[id]/route.ts": 1,
|
||||
"app/api/biomarkers/route.ts": 1,
|
||||
"app/api/coach/about-me/questions/route.ts": 1,
|
||||
"app/api/coach/about-me/route.ts": 1,
|
||||
"app/api/consent/ai/route.ts": 1,
|
||||
"app/api/custom-metrics/[id]/entries/[entryId]/route.ts": 1,
|
||||
"app/api/custom-metrics/[id]/entries/route.ts": 1,
|
||||
"app/api/custom-metrics/[id]/route.ts": 1,
|
||||
"app/api/custom-metrics/route.ts": 1,
|
||||
"app/api/cycle/day-logs/bulk/route.ts": 1,
|
||||
"app/api/cycle/day-logs/route.ts": 1,
|
||||
"app/api/cycle/period/route.ts": 1,
|
||||
"app/api/dashboard/chart-overlay-prefs/route.ts": 1,
|
||||
"app/api/dashboard/widgets/route.ts": 1,
|
||||
"app/api/dawarich/connect/route.ts": 1,
|
||||
"app/api/dawarich/disconnect/route.ts": 1,
|
||||
"app/api/dawarich/settings/route.ts": 1,
|
||||
"app/api/dawarich/sync/route.ts": 1,
|
||||
"app/api/devices/route.ts": 1,
|
||||
"app/api/documents/inbound/[id]/extract/route.ts": 1,
|
||||
"app/api/documents/inbound/[id]/index/route.ts": 1,
|
||||
"app/api/documents/inbound/[id]/suggest/route.ts": 1,
|
||||
"app/api/documents/inbound/[id]/summary/route.ts": 1,
|
||||
"app/api/fitbit/credentials/route.ts": 1,
|
||||
"app/api/google-health/credentials/route.ts": 1,
|
||||
"app/api/illness/episodes/[id]/resolve/route.ts": 1,
|
||||
"app/api/import/route.ts": 1,
|
||||
"app/api/ingest/medication/route.ts": 1,
|
||||
"app/api/insights/chat/messages/[id]/feedback/route.ts": 1,
|
||||
"app/api/insights/ecg/route.ts": 1,
|
||||
"app/api/insights/feedback/route.ts": 1,
|
||||
"app/api/insights/generate/route.ts": 1,
|
||||
"app/api/insights/layout/route.ts": 1,
|
||||
"app/api/insights/provider-chain/route.ts": 1,
|
||||
"app/api/integrations/healthkit/route.ts": 1,
|
||||
"app/api/internal/deploy-webhook/route.ts": 1,
|
||||
"app/api/labs/[id]/route.ts": 1,
|
||||
"app/api/labs/ocr/commit/route.ts": 1,
|
||||
"app/api/labs/ocr/extract/route.ts": 1,
|
||||
"app/api/labs/route.ts": 1,
|
||||
"app/api/measurement-reminders/[id]/route.ts": 1,
|
||||
"app/api/measurement-reminders/[id]/snooze/route.ts": 1,
|
||||
"app/api/measurement-reminders/route.ts": 1,
|
||||
"app/api/measurements/[id]/route.ts": 1,
|
||||
"app/api/measurements/batch/route.ts": 1,
|
||||
"app/api/measurements/by-external-ids/route.ts": 1,
|
||||
"app/api/measurements/route.ts": 1,
|
||||
"app/api/medications/[id]/intake/[eventId]/route.ts": 1,
|
||||
"app/api/medications/[id]/intake/import/route.ts": 1,
|
||||
"app/api/medications/[id]/intake/route.ts": 1,
|
||||
"app/api/medications/[id]/schedule-revisions/[revisionId]/route.ts": 1,
|
||||
"app/api/medications/[id]/schedule-revisions/route.ts": 1,
|
||||
"app/api/medications/extract/route.ts": 1,
|
||||
"app/api/medications/intake/bulk/route.ts": 1,
|
||||
"app/api/medications/intake/route.ts": 1,
|
||||
"app/api/medications/layout/route.ts": 1,
|
||||
"app/api/medications/route.ts": 1,
|
||||
"app/api/mental-health/assessments/route.ts": 1,
|
||||
"app/api/mood-entries/[id]/route.ts": 1,
|
||||
"app/api/mood-entries/bulk/route.ts": 1,
|
||||
"app/api/mood-entries/route.ts": 1,
|
||||
"app/api/mood/tags/layout/route.ts": 1,
|
||||
"app/api/nightscout/connect/route.ts": 1,
|
||||
"app/api/nutrients/batch/route.ts": 1,
|
||||
"app/api/onboarding/answers/route.ts": 1,
|
||||
"app/api/onboarding/complete/route.ts": 1,
|
||||
"app/api/onboarding/disclaimer/route.ts": 1,
|
||||
"app/api/onboarding/restart/route.ts": 1,
|
||||
"app/api/onboarding/tour/route.ts": 1,
|
||||
"app/api/oura/credentials/route.ts": 1,
|
||||
"app/api/polar/credentials/route.ts": 1,
|
||||
"app/api/record-settings/[family]/route.ts": 1,
|
||||
"app/api/search/route.ts": 1,
|
||||
"app/api/settings/email/route.ts": 1,
|
||||
"app/api/settings/ntfy/route.ts": 1,
|
||||
"app/api/settings/telegram/route.ts": 1,
|
||||
"app/api/settings/webhook/route.ts": 1,
|
||||
"app/api/strava/credentials/route.ts": 1,
|
||||
"app/api/user/ai-provider/route.ts": 1,
|
||||
"app/api/user/profile/route.ts": 1,
|
||||
"app/api/whoop/credentials/route.ts": 1,
|
||||
"app/api/withings/credentials/route.ts": 1,
|
||||
"app/api/workouts/batch/route.ts": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Route handlers read their JSON body through `parseJsonBody`.
|
||||
*
|
||||
* `parseJsonBody` (`src/lib/api/body.ts`) is `safeJson`, the Zod
|
||||
* `safeParse`, the validation-failed wide-event action and the multi-issue
|
||||
* 422 envelope in one call. Before it, every body-accepting route spelled
|
||||
* those steps out by hand, and the copies drifted: some annotated the
|
||||
* refusal and some did not, some answered a flat 400 where the rest answered
|
||||
* the multi-issue 422.
|
||||
*
|
||||
* The routes that still call `safeJson` themselves are counted per file in
|
||||
* `baselines/route-safe-json.json`. Most of them do something between
|
||||
* reading and validating (an optimistic-concurrency token split off the body,
|
||||
* a count cap answered before the schema runs, a refund of a spent quota on a
|
||||
* malformed body), which is a reason to keep the read separate, not a reason
|
||||
* to copy it into a new route. A new route file that calls `safeJson`, or a
|
||||
* listed one that gains a call, fails. A file that sheds one fails until the
|
||||
* lower count is written back with
|
||||
* `RATCHET_WRITE=1 pnpm vitest run src/__tests__/route-body-parse-ratchet.test.ts`.
|
||||
*/
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { runRatchet, writeBackHint } from "./helpers/ratchet";
|
||||
import {
|
||||
readSourceText,
|
||||
requireFloor,
|
||||
stripComments,
|
||||
walkSourceFiles,
|
||||
} from "./helpers/source-files";
|
||||
|
||||
const GUARD = "src/__tests__/route-body-parse-ratchet.test.ts";
|
||||
const APP = join(process.cwd(), "src/app");
|
||||
|
||||
const SAFE_JSON_CALL = /\bsafeJson\s*(?:<[^>()]*>)?\s*\(/g;
|
||||
const PARSE_JSON_BODY_CALL = /\bparseJsonBody\s*\(/g;
|
||||
|
||||
function count(text: string, pattern: RegExp): number {
|
||||
return [...text.matchAll(pattern)].length;
|
||||
}
|
||||
|
||||
describe("route body parsing", () => {
|
||||
const routes = walkSourceFiles(APP, { floor: 400 }).filter((rel) =>
|
||||
rel.endsWith("/route.ts"),
|
||||
);
|
||||
const safeJson: Record<string, number> = {};
|
||||
const helper: string[] = [];
|
||||
for (const rel of routes) {
|
||||
const code = stripComments(readSourceText(join(APP, rel)));
|
||||
const n = count(code, SAFE_JSON_CALL);
|
||||
if (n > 0) safeJson[`app/${rel}`] = n;
|
||||
if (count(code, PARSE_JSON_BODY_CALL) > 0) helper.push(rel);
|
||||
}
|
||||
|
||||
it("finds the routes on both sides", () => {
|
||||
requireFloor("route files", routes, 400);
|
||||
requireFloor(
|
||||
"routes reading their body through parseJsonBody",
|
||||
helper,
|
||||
100,
|
||||
);
|
||||
requireFloor("routes still calling safeJson", Object.keys(safeJson), 1);
|
||||
});
|
||||
|
||||
it("adds no route that calls safeJson itself", () => {
|
||||
const r = runRatchet("route-safe-json.json", safeJson, {
|
||||
comment: `safeJson calls per route file under src/app; new body-reading routes use parseJsonBody. Maintained by ${GUARD}.`,
|
||||
});
|
||||
expect(
|
||||
r.grew,
|
||||
"A route that reads its body with safeJson. Use parseJsonBody " +
|
||||
"(src/lib/api/body.ts); its options cover the size cap, the " +
|
||||
"validation-failed action, the error code and the message.",
|
||||
).toEqual([]);
|
||||
expect(r.stale, writeBackHint(GUARD)).toEqual([]);
|
||||
});
|
||||
|
||||
it("counts a call however it is spelled", () => {
|
||||
expect(count("await safeJson(request)", SAFE_JSON_CALL)).toBe(1);
|
||||
expect(
|
||||
count("await safeJson<Body>(req, { maxBytes: 1 })", SAFE_JSON_CALL),
|
||||
).toBe(1);
|
||||
expect(count("await safeJson (\n request,\n)", SAFE_JSON_CALL)).toBe(1);
|
||||
expect(count("import { safeJson } from", SAFE_JSON_CALL)).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -54,13 +54,8 @@ import {
|
||||
requireRecentProof,
|
||||
MFA_STEP_UP_MAX_AGE_SECONDS,
|
||||
} 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 { prisma } from "@/lib/db";
|
||||
import { getAuditLogRetentionDays } from "@/lib/jobs/audit-log-cleanup";
|
||||
@@ -163,17 +158,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
return apiError("Too many invitations, try again later", 429);
|
||||
}
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, inviteGrantSchema, {
|
||||
maxBytes: 8 * 1024,
|
||||
errorCode: "sharing.invite.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = inviteGrantSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "sharing.invite.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const { identifier, expiresAt, access, scope } = parsed.data;
|
||||
|
||||
|
||||
@@ -19,12 +19,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import {
|
||||
decideHandover,
|
||||
@@ -55,12 +51,10 @@ export const GET = apiHandler(async (_request: NextRequest) => {
|
||||
export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAuth();
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, handoverDecisionSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = handoverDecisionSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
let result;
|
||||
try {
|
||||
|
||||
@@ -55,13 +55,8 @@ import {
|
||||
requireActorAuth,
|
||||
SharingAccessDeniedError,
|
||||
} 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 { prisma } from "@/lib/db";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
@@ -83,17 +78,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
);
|
||||
}
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, switchAccountSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
errorCode: "sharing.switch.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = switchAccountSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "sharing.switch.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const { accountId, expectedEpoch } = parsed.data;
|
||||
const ip = getClientIp(request);
|
||||
|
||||
@@ -8,12 +8,8 @@
|
||||
* (`activity_goals_json`) and the activity calendar reads them.
|
||||
*/
|
||||
import { apiHandler, requireAuth } 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 { auditLog } from "@/lib/auth/audit";
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
import { prisma, toJson } from "@/lib/db";
|
||||
@@ -56,16 +52,11 @@ export const GET = apiHandler(async () => {
|
||||
export const PATCH = apiHandler(async (request: Request) => {
|
||||
const { user } = await requireAuth();
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, activityGoalsSchema, {
|
||||
maxBytes: 1024,
|
||||
action: "activity.goals.validation-failed",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = activityGoalsSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
annotate({ action: { name: "activity.goals.validation-failed" } });
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const next = mergeActivityGoals(await readGoals(user.id), parsed.data);
|
||||
await prisma.user.update({
|
||||
|
||||
@@ -15,13 +15,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
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 { prisma } from "@/lib/db";
|
||||
import { getGlobalBoss } from "@/lib/jobs/boss-instance";
|
||||
@@ -99,12 +94,10 @@ export const GET = apiHandler(async () => {
|
||||
export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAdmin();
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, pulseDayMeanActionSchema, {
|
||||
maxBytes: 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = pulseDayMeanActionSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const action = parsed.data.action;
|
||||
annotate({
|
||||
action: { name: "admin.pulse_day_mean.requested" },
|
||||
|
||||
@@ -52,13 +52,8 @@ import type { NextRequest } from "next/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { apiHandler, requireAdmin } from "@/lib/api-handler";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { resolveAssistantFlags } from "@/lib/feature-flags";
|
||||
@@ -180,16 +175,10 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAdmin();
|
||||
annotate({ action: { name: "admin.settings.assistant-flags.update" } });
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, assistantFlagsSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = assistantFlagsSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
// v1.4.43 — multi-issue 422.
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
// Field by field from the parsed body; the schema is strict, so every key
|
||||
// here is one the route owns.
|
||||
|
||||
@@ -33,13 +33,8 @@ import type { NextRequest } from "next/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { apiHandler, requireAdmin } from "@/lib/api-handler";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
@@ -95,15 +90,10 @@ export const PATCH = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAdmin();
|
||||
annotate({ action: { name: "admin.settings.module-availability.update" } });
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, moduleAvailabilitySchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = moduleAvailabilitySchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const patch: Record<string, boolean> = {};
|
||||
for (const [key, value] of Object.entries(parsed.data)) {
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireAdmin } from "@/lib/api-handler";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { encrypt } from "@/lib/crypto";
|
||||
import { adminSettingsSchema } from "@/lib/validations/admin";
|
||||
@@ -109,16 +104,10 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAdmin();
|
||||
annotate({ action: { name: "admin.settings.update" } });
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, adminSettingsSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = adminSettingsSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
// v1.4.43 — multi-issue 422.
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const data = parsed.data;
|
||||
const updates: Record<string, unknown> = {};
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireAdmin } from "@/lib/api-handler";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { NextRequest } from "next/server";
|
||||
import { z } from "zod/v4";
|
||||
@@ -49,16 +44,10 @@ export const PUT = apiHandler(
|
||||
},
|
||||
});
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, updateUserSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = updateUserSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
// v1.4.43 — multi-issue 422.
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const target = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
|
||||
@@ -19,13 +19,8 @@ import {
|
||||
destroyedDetails,
|
||||
overwriteDetails,
|
||||
} from "@/lib/sharing/audit-details";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
|
||||
import { allergyUpdateSchema } from "@/lib/validations/allergy";
|
||||
import { toAllergyDTO } from "@/lib/records/dto";
|
||||
@@ -82,17 +77,11 @@ export const PATCH = apiHandler(
|
||||
return apiError("Allergy not found", 404);
|
||||
}
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, allergyUpdateSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
errorCode: "allergy.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = allergyUpdateSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "allergy.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const entry = parsed.data;
|
||||
|
||||
// Field-by-field — only fields actually present in the body are written.
|
||||
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
apiSuccess,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
|
||||
import {
|
||||
@@ -92,21 +92,13 @@ async function postAllergy(request: NextRequest): Promise<Response> {
|
||||
// regardless.
|
||||
const { user } = await requireRecordAuth("manage", "profile");
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, allergyCreateSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
action: "allergy.validation-failed",
|
||||
countIssues: true,
|
||||
errorCode: "allergy.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = allergyCreateSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
annotate({
|
||||
action: { name: "allergy.validation-failed" },
|
||||
meta: { issue_count: parsed.error.issues.length },
|
||||
});
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "allergy.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const entry = parsed.data;
|
||||
|
||||
|
||||
@@ -16,13 +16,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireRecordAuth } 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 { withIdempotency } from "@/lib/idempotency";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
@@ -46,16 +41,11 @@ async function postAuditC(request: NextRequest): Promise<Response> {
|
||||
});
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, auditCRequestSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
errorCode: "anamnesis.auditC.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = auditCRequestSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "anamnesis.auditC.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const input = parsed.data;
|
||||
|
||||
|
||||
@@ -12,13 +12,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } 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 { updateLifeAiSections } from "@/lib/profile/life-circumstances";
|
||||
@@ -27,16 +22,11 @@ import { lifeAiSectionsPatchSchema } from "@/lib/validations/life-circumstances"
|
||||
export const PATCH = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAuth();
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, lifeAiSectionsPatchSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
errorCode: "anamnesis.lifeSections.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = lifeAiSectionsPatchSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "anamnesis.lifeSections.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const outcome = await updateLifeAiSections(
|
||||
user.id,
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { apiSuccess, returnAllZodIssues, safeJson } from "@/lib/api-response";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { confirmLifeSection } from "@/lib/profile/life-circumstances";
|
||||
import { lifeConfirmSchema } from "@/lib/validations/life-circumstances";
|
||||
@@ -16,16 +17,11 @@ import { lifeConfirmSchema } from "@/lib/validations/life-circumstances";
|
||||
export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAuth();
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, lifeConfirmSchema, {
|
||||
maxBytes: 1024,
|
||||
errorCode: "anamnesis.lifeConfirm.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = lifeConfirmSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "anamnesis.lifeConfirm.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const lastConfirmedAt = await confirmLifeSection(
|
||||
user.id,
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler } from "@/lib/api-handler";
|
||||
import { apiSuccess, returnAllZodIssues, safeJson } from "@/lib/api-response";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import {
|
||||
@@ -35,12 +36,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
);
|
||||
if (gate.refusal) return gate.refusal;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, claimPreviewSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = claimPreviewSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const preview = await previewHandover(parsed.data.token);
|
||||
annotate({
|
||||
|
||||
@@ -24,12 +24,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { recordSignInDevice } from "@/lib/auth/login-alert";
|
||||
import { checkPasswordStrength, hashPassword } from "@/lib/auth/password";
|
||||
@@ -59,12 +55,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
if (gate.refusal) return gate.refusal;
|
||||
const ip = gate.ip ?? "unknown";
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, claimSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = claimSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const { token, username, email, password } = parsed.data;
|
||||
|
||||
// The link first. Without this the collision probe below would answer 409
|
||||
|
||||
@@ -20,12 +20,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -44,12 +40,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const gate = await claimGate(request, "sso", (await getSession()) !== null);
|
||||
if (gate.refusal) return gate.refusal;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, claimSsoSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = claimSsoSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const { token, username } = parsed.data;
|
||||
|
||||
// The link first, so the username probe below is no oracle for anybody
|
||||
|
||||
@@ -10,13 +10,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } 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 { prisma } from "@/lib/db";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
@@ -40,16 +35,11 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
});
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, aiUsagePutSchema, {
|
||||
maxBytes: 1024,
|
||||
errorCode: "ai.usage.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = aiUsagePutSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "ai.usage.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const { level } = parsed.data;
|
||||
const settings = await loadAiUsageSettings();
|
||||
|
||||
@@ -25,12 +25,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { z } from "zod/v4";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -56,14 +52,10 @@ export const PATCH = apiHandler(
|
||||
const { user } = await requireAuth();
|
||||
const { id } = await context.params;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, devicePatchSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = devicePatchSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
// Ownership boundary — scope the update by userId so a cross-user id
|
||||
// can't be patched, and 404 rather than leak existence.
|
||||
|
||||
@@ -20,13 +20,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { apiHandler, requireAuth } 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 { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -79,16 +74,11 @@ export const PATCH = apiHandler(async (req: Request) => {
|
||||
|
||||
// The body is a single boolean — bound the parse so a malformed or
|
||||
// oversized payload is rejected before it is materialised.
|
||||
const { data: body, error: jsonError } = await safeJson(req, {
|
||||
const parsed = await parseJsonBody(req, patchBodySchema, {
|
||||
maxBytes: 1024,
|
||||
action: "auth.me.diabetes.patch.invalid_shape",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = patchBodySchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
annotate({ action: { name: "auth.me.diabetes.patch.invalid_shape" } });
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const next = parsed.data.hasDiabetes;
|
||||
|
||||
|
||||
@@ -26,13 +26,8 @@
|
||||
* resolved next state so the client can hard-set the optimistic update.
|
||||
*/
|
||||
import { apiHandler, requireAuth } 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 { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { getAiCapability } from "@/lib/ai/capabilities/gate";
|
||||
@@ -134,18 +129,11 @@ export const PATCH = apiHandler(async (req: Request) => {
|
||||
|
||||
// The body is a single boolean — bound the parse so a malformed or oversized
|
||||
// payload is rejected before it is materialised.
|
||||
const { data: body, error: jsonError } = await safeJson(req, {
|
||||
const parsed = await parseJsonBody(req, documentsAutoAiReadPatchSchema, {
|
||||
maxBytes: 1024,
|
||||
action: "auth.me.documentsAutoAiRead.patch.invalid_shape",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = documentsAutoAiReadPatchSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
annotate({
|
||||
action: { name: "auth.me.documentsAutoAiRead.patch.invalid_shape" },
|
||||
});
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const next = parsed.data.documentsAutoAiRead;
|
||||
|
||||
|
||||
@@ -25,13 +25,8 @@
|
||||
* update without an extra round-trip.
|
||||
*/
|
||||
import { apiHandler, requireAuth } 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 { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -81,16 +76,11 @@ export const PATCH = apiHandler(async (req: Request) => {
|
||||
|
||||
// The body is a single enum value — bound the parse so a malformed or
|
||||
// oversized payload is rejected before it is materialised.
|
||||
const { data: body, error: jsonError } = await safeJson(req, {
|
||||
const parsed = await parseJsonBody(req, glucoseUnitPatchSchema, {
|
||||
maxBytes: 1024,
|
||||
action: "auth.me.glucose-unit.patch.invalid_shape",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = glucoseUnitPatchSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
annotate({ action: { name: "auth.me.glucose-unit.patch.invalid_shape" } });
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const next = parsed.data.glucoseUnit;
|
||||
|
||||
|
||||
@@ -19,13 +19,8 @@
|
||||
* field-by-field write (no mass assignment).
|
||||
*/
|
||||
import { apiHandler, requireAuth } 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 { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -77,18 +72,11 @@ export const PATCH = apiHandler(async (request: NextRequest) => {
|
||||
return response;
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, injectionSitePrefsPatchSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
action: "auth.me.injection-site-prefs.patch.invalid_shape",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = injectionSitePrefsPatchSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
annotate({
|
||||
action: { name: "auth.me.injection-site-prefs.patch.invalid_shape" },
|
||||
});
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
// Dedup while preserving canonical order from the enum tuple.
|
||||
const submitted = new Set(parsed.data.globalExcludedInjectionSites);
|
||||
|
||||
@@ -17,13 +17,8 @@
|
||||
* resolved next state so the client can hard-set the optimistic update.
|
||||
*/
|
||||
import { apiHandler, requireAuth } 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 { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -71,16 +66,11 @@ export const PATCH = apiHandler(async (req: Request) => {
|
||||
|
||||
// The body is a single boolean — bound the parse so a malformed or oversized
|
||||
// payload is rejected before it is materialised.
|
||||
const { data: body, error: jsonError } = await safeJson(req, {
|
||||
const parsed = await parseJsonBody(req, labsLocalOcrPatchSchema, {
|
||||
maxBytes: 1024,
|
||||
action: "auth.me.labsLocalOcr.patch.invalid_shape",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = labsLocalOcrPatchSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
annotate({ action: { name: "auth.me.labsLocalOcr.patch.invalid_shape" } });
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const next = parsed.data.labsLocalOcrEnabled;
|
||||
|
||||
|
||||
@@ -10,14 +10,8 @@
|
||||
*/
|
||||
import { NextRequest } from "next/server";
|
||||
import { apiHandler, requireMfaManagementAuth } 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 { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -34,19 +28,11 @@ export const PATCH = apiHandler(
|
||||
const { user } = auth;
|
||||
const { id } = await params;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, mfaWebauthnRenameSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
message: "Invalid request",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = mfaWebauthnRenameSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiValidationError(
|
||||
"Invalid request",
|
||||
sanitiseZodIssues(parsed.error.issues),
|
||||
422,
|
||||
);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const existing = await prisma.webauthnMfaCredential.findUnique({
|
||||
where: { id },
|
||||
|
||||
@@ -8,14 +8,8 @@
|
||||
*/
|
||||
import { NextRequest } from "next/server";
|
||||
import { apiHandler, requireMfaManagementAuth } 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 { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -30,19 +24,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const auth = await requireMfaManagementAuth();
|
||||
const { user } = auth;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, mfaWebauthnRegisterVerifySchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
message: "Invalid request",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = mfaWebauthnRegisterVerifySchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiValidationError(
|
||||
"Invalid request",
|
||||
sanitiseZodIssues(parsed.error.issues),
|
||||
422,
|
||||
);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const { challengeId, credential, name } = parsed.data;
|
||||
|
||||
const verification = await verifyMfaRegistration(
|
||||
|
||||
@@ -25,13 +25,8 @@
|
||||
* `res.ok` and never the status.
|
||||
*/
|
||||
import { apiHandler, requireAuth } 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 { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -85,18 +80,11 @@ export const PATCH = apiHandler(async (req: Request) => {
|
||||
|
||||
// The body is a single enum value — bound the parse so a malformed or
|
||||
// oversized payload is rejected before it is materialised.
|
||||
const { data: body, error: jsonError } = await safeJson(req, {
|
||||
const parsed = await parseJsonBody(req, unitPreferencePatchSchema, {
|
||||
maxBytes: 1024,
|
||||
action: "auth.me.unit-preference.patch.invalid_shape",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = unitPreferencePatchSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
annotate({
|
||||
action: { name: "auth.me.unit-preference.patch.invalid_shape" },
|
||||
});
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const next = parsed.data.unitPreference;
|
||||
|
||||
|
||||
@@ -17,13 +17,8 @@
|
||||
* assignment). Idempotent — always returns the resolved next state.
|
||||
*/
|
||||
import { apiHandler, requireAuth } 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 { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -69,18 +64,11 @@ export const PATCH = apiHandler(async (req: Request) => {
|
||||
return response;
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(req, {
|
||||
const parsed = await parseJsonBody(req, useCentralCodexPatchSchema, {
|
||||
maxBytes: 1024,
|
||||
action: "auth.me.useCentralCodex.patch.invalid_shape",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = useCentralCodexPatchSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
annotate({
|
||||
action: { name: "auth.me.useCentralCodex.patch.invalid_shape" },
|
||||
});
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const next = parsed.data.useCentralCodex;
|
||||
|
||||
|
||||
@@ -15,13 +15,8 @@
|
||||
* the resolved next state in the response.
|
||||
*/
|
||||
import { apiHandler, requireAuth } 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 { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -67,16 +62,11 @@ export const PATCH = apiHandler(async (req: Request) => {
|
||||
return response;
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(req, {
|
||||
const parsed = await parseJsonBody(req, workoutRoutesPatchSchema, {
|
||||
maxBytes: 1024,
|
||||
action: "auth.me.workoutRoutes.patch.invalid_shape",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = workoutRoutesPatchSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
annotate({ action: { name: "auth.me.workoutRoutes.patch.invalid_shape" } });
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const next = parsed.data.storeWorkoutRoutes;
|
||||
|
||||
|
||||
@@ -13,13 +13,8 @@
|
||||
*/
|
||||
import { NextRequest } from "next/server";
|
||||
import { apiHandler } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
apiValidationError,
|
||||
safeJson,
|
||||
sanitiseZodIssues,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { ensureDbCompatibility } from "@/lib/db-compat";
|
||||
import { checkAuthSurfaceRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
|
||||
@@ -44,19 +39,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
await ensureDbCompatibility();
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, mfaWebauthnLoginOptionsSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
message: "Invalid request",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = mfaWebauthnLoginOptionsSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiValidationError(
|
||||
"Invalid request",
|
||||
sanitiseZodIssues(parsed.error.issues),
|
||||
422,
|
||||
);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const challenge = await loadActiveChallenge(parsed.data.mfaTicket);
|
||||
if (!challenge) {
|
||||
|
||||
@@ -20,12 +20,8 @@
|
||||
* `requireFreshMfa` step-up.
|
||||
*/
|
||||
import { NextRequest } from "next/server";
|
||||
import {
|
||||
apiError,
|
||||
apiValidationError,
|
||||
safeJson,
|
||||
sanitiseZodIssues,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { apiHandler } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
@@ -81,19 +77,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
await ensureDbCompatibility();
|
||||
|
||||
// (3) Body — bounded, then the exact wire contract.
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, nativeHandoffTokenSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
message: "Invalid request",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = nativeHandoffTokenSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiValidationError(
|
||||
"Invalid request",
|
||||
sanitiseZodIssues(parsed.error.issues),
|
||||
422,
|
||||
);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const { code, codeVerifier } = parsed.data;
|
||||
|
||||
// (4) Lookup + flow check + replay/expiry/PKCE + atomic single-use consume.
|
||||
|
||||
@@ -23,12 +23,8 @@
|
||||
* `requireFreshMfa` step-up, so an SSO login can never satisfy step-up.
|
||||
*/
|
||||
import { NextRequest } from "next/server";
|
||||
import {
|
||||
apiError,
|
||||
apiValidationError,
|
||||
safeJson,
|
||||
sanitiseZodIssues,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { apiHandler } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
@@ -87,19 +83,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
await ensureDbCompatibility();
|
||||
|
||||
// (3) Body — bounded, then the exact wire contract.
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, oidcNativeTokenSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
message: "Invalid request",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = oidcNativeTokenSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiValidationError(
|
||||
"Invalid request",
|
||||
sanitiseZodIssues(parsed.error.issues),
|
||||
422,
|
||||
);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const { code, codeVerifier } = parsed.data;
|
||||
|
||||
// (4–8) Lookup + replay/expiry/PKCE + atomic single-use consume.
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { NextRequest } from "next/server";
|
||||
import {
|
||||
apiHandler,
|
||||
@@ -24,20 +19,10 @@ export const PATCH = apiHandler(
|
||||
const { user } = await requireAuth();
|
||||
const { id } = await params;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, passkeyRenameSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = passkeyRenameSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
// Multi-issue, like every sibling that parses a body. The flat
|
||||
// `apiError("Invalid request", 422)` this replaces discarded the issues,
|
||||
// so a person renaming a passkey could not tell an empty name from one
|
||||
// over the 64-character limit — the form had nothing to put beside the
|
||||
// field.
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const passkey = await prisma.passkey.findUnique({
|
||||
where: { id },
|
||||
|
||||
@@ -7,13 +7,8 @@ import {
|
||||
} from "@/lib/auth/password";
|
||||
import { changePasswordSchema } from "@/lib/validations/auth";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import {
|
||||
recordReproofFailure,
|
||||
refundReproof,
|
||||
@@ -46,16 +41,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const limited = await throttleReproof(user.id, ip, "password_change");
|
||||
if (limited) return limited;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, changePasswordSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = changePasswordSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
// v1.4.43 — multi-issue 422.
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const { currentPassword, newPassword } = parsed.data;
|
||||
const credential = await prisma.user.findUnique({
|
||||
|
||||
@@ -9,12 +9,8 @@ import { hashPassword, checkPasswordStrength } from "@/lib/auth/password";
|
||||
import { createSession, getSession } from "@/lib/auth/session";
|
||||
import { recordSignInDevice } from "@/lib/auth/login-alert";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, apiError } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { checkAuthSurfaceRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
|
||||
import { ensureDbCompatibility } from "@/lib/db-compat";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
@@ -100,17 +96,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
} catch {
|
||||
// Table may not exist yet; allow registration
|
||||
}
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, registerSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = registerSchema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
// v1.4.43 — multi-issue 422.
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const {
|
||||
email,
|
||||
|
||||
@@ -42,14 +42,8 @@ import {
|
||||
recentProofMethods,
|
||||
requireCookieAuth,
|
||||
} 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 { prisma } from "@/lib/db";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
@@ -68,19 +62,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const { user, session } = await requireCookieAuth();
|
||||
const ip = getClientIp(request);
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, stepUpMintSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
message: "Invalid request",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = stepUpMintSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiValidationError(
|
||||
"Invalid request",
|
||||
sanitiseZodIssues(parsed.error.issues),
|
||||
422,
|
||||
);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const proof = parsed.data;
|
||||
|
||||
// Refuse a proof that could not satisfy the gate BEFORE spending a guess on
|
||||
|
||||
@@ -18,13 +18,8 @@
|
||||
*/
|
||||
import { NextRequest } from "next/server";
|
||||
import { apiHandler, requireBearerAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
apiValidationError,
|
||||
safeJson,
|
||||
sanitiseZodIssues,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { checkAuthSurfaceRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
|
||||
@@ -49,19 +44,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
});
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, stepUpOptionsSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
message: "Invalid request",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = stepUpOptionsSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiValidationError(
|
||||
"Invalid request",
|
||||
sanitiseZodIssues(parsed.error.issues),
|
||||
422,
|
||||
);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
if (parsed.data.method === "webauthn") {
|
||||
const result = await createMfaAuthenticationOptions(user.id);
|
||||
|
||||
@@ -30,14 +30,8 @@
|
||||
*/
|
||||
import { NextRequest } from "next/server";
|
||||
import { apiHandler, requireBearerAuth } 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 { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import {
|
||||
@@ -110,19 +104,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
});
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, stepUpMintSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
message: "Invalid request",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = stepUpMintSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiValidationError(
|
||||
"Invalid request",
|
||||
sanitiseZodIssues(parsed.error.issues),
|
||||
422,
|
||||
);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const method: StepUpMethod = parsed.data.method;
|
||||
// The shared verifier: the same challenge ownership and ceremony checks, and
|
||||
|
||||
@@ -19,13 +19,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireRecordAuth } 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 { invalidateUserHealthScore } from "@/lib/cache/invalidate";
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
@@ -47,16 +42,11 @@ async function mergeBiomarkers(
|
||||
const { user } = await requireRecordAuth("manage", "labs");
|
||||
const { id } = await params;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, biomarkerMergeSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
errorCode: "labs.biomarker.merge.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = biomarkerMergeSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "labs.biomarker.merge.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
if (parsed.data.sourceIds.includes(id)) {
|
||||
return apiError("The target cannot be one of the sources", 422, {
|
||||
errorCode: "labs.biomarker.merge.invalid",
|
||||
|
||||
@@ -23,12 +23,8 @@ import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { checkAuthSurfaceRateLimit } from "@/lib/rate-limit";
|
||||
import { shouldEmitSecureCookie } from "@/lib/auth/secure-cookie";
|
||||
import { resolveShareGateState } from "@/lib/clinician-share/resolve-share-token";
|
||||
@@ -69,13 +65,10 @@ export const POST = apiHandler(
|
||||
return apiError("Too many attempts", 429);
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, unlockShareLinkSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = unlockShareLinkSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
// A token with no live gate, or one carrying no passphrase, cannot be
|
||||
// unlocked. `verifyPassphrase` is constant-time and returns false for a
|
||||
|
||||
@@ -28,13 +28,8 @@
|
||||
* prose and must not land in plaintext audit details.
|
||||
*/
|
||||
import { apiHandler, requireAuth } 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 { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -111,15 +106,10 @@ export const POST = apiHandler(async (req: Request) => {
|
||||
return response;
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(req, {
|
||||
const parsed = await parseJsonBody(req, aboutMeAdoptSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = aboutMeAdoptSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const { question, answer } = parsed.data;
|
||||
const normAnswer = normalise(answer);
|
||||
|
||||
@@ -25,12 +25,8 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
@@ -77,15 +73,10 @@ export const PATCH = apiHandler(async (req: NextRequest, ctx: RouteCtx) => {
|
||||
|
||||
const { id } = await ctx.params;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(req, {
|
||||
const parsed = await parseJsonBody(req, coachPlanPatchSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = coachPlanPatchSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
// Field-by-field data assembly (no mass assignment): only the lifecycle
|
||||
// fields the body carried are written; the metric + encrypted text are never
|
||||
|
||||
@@ -24,13 +24,8 @@ import { prisma } from "@/lib/db";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { apiHandler, requireAuth } 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 { annotate } from "@/lib/logging/context";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import {
|
||||
@@ -65,13 +60,12 @@ async function postAction(request: NextRequest): Promise<Response> {
|
||||
return apiError("coach.suggestion.rateLimited", 429);
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
maxBytes: 4 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = coachReminderSuggestionActionSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
const parsed = await parseJsonBody(
|
||||
request,
|
||||
coachReminderSuggestionActionSchema,
|
||||
{ maxBytes: 4 * 1024 },
|
||||
);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const { cadenceId, action } = parsed.data;
|
||||
|
||||
if (!isCadenceId(cadenceId)) {
|
||||
|
||||
@@ -23,12 +23,8 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
@@ -71,13 +67,10 @@ export const PATCH = apiHandler(async (req: NextRequest, ctx: RouteCtx) => {
|
||||
|
||||
const { id } = await ctx.params;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(req, {
|
||||
const parsed = await parseJsonBody(req, coachReminderPatchSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = coachReminderPatchSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
// Field-by-field (no mass assignment): only the lifecycle / timing fields the
|
||||
// body carried are written; the note + encrypted text are never touched here.
|
||||
|
||||
@@ -22,12 +22,8 @@
|
||||
* / due / surfaced), soonest-due first.
|
||||
*/
|
||||
import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess, returnAllZodIssues } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
@@ -151,13 +147,10 @@ export const POST = apiHandler(async (req: Request) => {
|
||||
);
|
||||
if (!rate.allowed) return apiError("Too many requests", 429);
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(req, {
|
||||
const parsed = await parseJsonBody(req, coachReminderCreateSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = coachReminderCreateSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
// Resolve the optional `when` against the closed grammar; an invalid token
|
||||
// 422s rather than silently dropping the timing the user asked for.
|
||||
|
||||
@@ -26,13 +26,8 @@ import { prisma } from "@/lib/db";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { apiHandler, requireAuth } 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 { annotate } from "@/lib/logging/context";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
|
||||
@@ -63,13 +58,10 @@ async function postAction(request: NextRequest): Promise<Response> {
|
||||
);
|
||||
if (!rate.allowed) return apiError("coach.action.rateLimited", 429);
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, coachSuggestedActionSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = coachSuggestedActionSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const action = parsed.data;
|
||||
|
||||
// ── checkup.create → a preventive-care Vorsorge reminder ─────
|
||||
|
||||
@@ -17,12 +17,8 @@
|
||||
* grants.
|
||||
*/
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { checkConsentRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
|
||||
@@ -52,15 +48,11 @@ export const POST = apiHandler(async (req: Request) => {
|
||||
let confirmed = false;
|
||||
const contentType = req.headers.get("content-type") ?? "";
|
||||
if (contentType.includes("application/json")) {
|
||||
const { data: body, error: jsonError } = await safeJson(req, {
|
||||
const parsed = await parseJsonBody(req, webConsentGrantBody, {
|
||||
maxBytes: 1024,
|
||||
action: "consent.ai.web-grant.invalid_shape",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = webConsentGrantBody.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
annotate({ action: { name: "consent.ai.web-grant.invalid_shape" } });
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
confirmed = parsed.data.confirm === true;
|
||||
}
|
||||
if (!confirmed) {
|
||||
|
||||
@@ -18,12 +18,8 @@ import { z } from "zod/v4";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import {
|
||||
apiSuccess,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { invalidateUserCorrelationPatterns } from "@/lib/cache/invalidate";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
@@ -43,15 +39,10 @@ export const POST = apiHandler(
|
||||
const { user } = await requireAuth();
|
||||
const { id } = await params;
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, restoreSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = restoreSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const { count } = await prisma.customMetricEntry.updateMany({
|
||||
where: {
|
||||
|
||||
@@ -17,13 +17,8 @@ import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { overwriteDetails } from "@/lib/sharing/audit-details";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { requireCycleEnabled } from "@/lib/cycle/gate";
|
||||
import {
|
||||
ensureCycleForBleedingDay,
|
||||
@@ -68,21 +63,13 @@ export const PATCH = apiHandler(
|
||||
return apiError("Day-log not found", 404);
|
||||
}
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, cycleDayLogPatchSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
action: "cycle.day-log.patch.validation-failed",
|
||||
countIssues: true,
|
||||
errorCode: "cycle.day-log.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = cycleDayLogPatchSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
annotate({
|
||||
action: { name: "cycle.day-log.patch.validation-failed" },
|
||||
meta: { issue_count: parsed.error.issues.length },
|
||||
});
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "cycle.day-log.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const body = parsed.data;
|
||||
|
||||
|
||||
@@ -9,13 +9,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { prisma } from "@/lib/db";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
@@ -53,16 +48,11 @@ export const PATCH = apiHandler(
|
||||
const { key } = await params;
|
||||
if (!isCustomSymptomKey(key)) return apiError("Not a custom symptom", 404);
|
||||
|
||||
const { data: rawJsonBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, updateCustomSymptomSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
errorCode: "cycle.symptom.custom.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = updateCustomSymptomSchema.safeParse(rawJsonBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "cycle.symptom.custom.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const owned = await prisma.cycleSymptom.findFirst({
|
||||
where: { key, userId: user.id },
|
||||
|
||||
@@ -17,13 +17,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { prisma } from "@/lib/db";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
@@ -107,16 +102,11 @@ async function postCustomSymptom(request: NextRequest): Promise<Response> {
|
||||
return apiError("Too many requests, try again later", 429);
|
||||
}
|
||||
|
||||
const { data: rawJsonBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, createCustomSymptomSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
errorCode: "cycle.symptom.custom.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = createCustomSymptomSchema.safeParse(rawJsonBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "cycle.symptom.custom.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const activeCount = await prisma.cycleSymptom.count({
|
||||
where: { userId: user.id, isActive: true },
|
||||
|
||||
@@ -18,12 +18,8 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { invalidateUserHealthScore } from "@/lib/cache/invalidate";
|
||||
@@ -52,15 +48,10 @@ export const POST = apiHandler(async (req: NextRequest) => {
|
||||
return response;
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(req, {
|
||||
const parsed = await parseJsonBody(req, dismissPriorityItemSchema, {
|
||||
maxBytes: 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = dismissPriorityItemSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const { itemKey } = parsed.data;
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
apiValidationError,
|
||||
safeJson,
|
||||
sanitiseZodIssues,
|
||||
} from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
|
||||
@@ -101,21 +101,12 @@ export const POST = apiHandler(
|
||||
});
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, documentChatRequestSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
errorCode: "documents.chat.invalid",
|
||||
message: "Invalid document chat request",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = documentChatRequestSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiValidationError(
|
||||
"Invalid document chat request",
|
||||
sanitiseZodIssues(parsed.error.issues),
|
||||
422,
|
||||
{
|
||||
errorCode: "documents.chat.invalid",
|
||||
},
|
||||
);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const { conversationId, message, locale: bodyLocale } = parsed.data;
|
||||
|
||||
const rl = await checkRateLimit(
|
||||
|
||||
@@ -17,13 +17,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } 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 { prisma } from "@/lib/db";
|
||||
import { commitApprovedFact, FactCommitError } from "@/lib/documents/commit";
|
||||
@@ -56,13 +51,10 @@ async function confirmDocument(
|
||||
});
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, inboundConfirmSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = inboundConfirmSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const approved: { factId: string; recordType: string; recordId: string }[] =
|
||||
[];
|
||||
|
||||
@@ -11,12 +11,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { prisma } from "@/lib/db";
|
||||
import {
|
||||
encryptFactData,
|
||||
@@ -100,13 +96,10 @@ export const PATCH = apiHandler(
|
||||
});
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, inboundFactEditSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = inboundFactEditSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
// The edit cannot change the fact's resource type.
|
||||
if (parsed.data.factType !== fact.factType) {
|
||||
|
||||
@@ -10,14 +10,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth, requireRecordAuth } 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 {
|
||||
@@ -164,20 +158,11 @@ export const PATCH = apiHandler(
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request);
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = documentUpdateSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiValidationError(
|
||||
"Invalid document update",
|
||||
sanitiseZodIssues(parsed.error.issues),
|
||||
422,
|
||||
{
|
||||
errorCode: "documents.inbound.invalidUpdate",
|
||||
},
|
||||
);
|
||||
}
|
||||
const parsed = await parseJsonBody(request, documentUpdateSchema, {
|
||||
errorCode: "documents.inbound.invalidUpdate",
|
||||
message: "Invalid document update",
|
||||
});
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const existing = await prisma.inboundDocument.findFirst({
|
||||
where: { id, userId: user.id, deletedAt: null },
|
||||
|
||||
@@ -13,13 +13,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } 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 { prisma } from "@/lib/db";
|
||||
import { linkTargets, unlinkTargets } from "@/lib/links";
|
||||
@@ -61,15 +56,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
return response;
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, documentBulkSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = documentBulkSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const { ids, action, kind, episodeId, encounterId } = parsed.data;
|
||||
const uniqueIds = [...new Set(ids)];
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler";
|
||||
import { apiSuccess, returnAllZodIssues, safeJson } from "@/lib/api-response";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { prisma, toJson } from "@/lib/db";
|
||||
import {
|
||||
DOCUMENTS_LAYOUT_ARRANGEMENTS,
|
||||
@@ -67,16 +68,11 @@ export const PUT = apiHandler(async (request: Request) => {
|
||||
const gate = await requireModuleEnabled(user.id, "inboundDocuments");
|
||||
if (!gate.enabled) return gate.response;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, layoutPutSchema, {
|
||||
maxBytes: 1024,
|
||||
action: "documents.layout.validation-failed",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = layoutPutSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
annotate({ action: { name: "documents.layout.validation-failed" } });
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
// Preserve-when-absent: one stored read fills whichever field the body
|
||||
// left out. Only fields the user has chosen are written, so a field never
|
||||
|
||||
@@ -36,13 +36,8 @@ import { Buffer } from "node:buffer";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiValidationError,
|
||||
getClientIp,
|
||||
safeJson,
|
||||
sanitiseZodIssues,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { getAiCapability } from "@/lib/ai/capabilities/gate";
|
||||
import { prisma } from "@/lib/db";
|
||||
import {
|
||||
@@ -176,18 +171,11 @@ export const POST = apiHandler(
|
||||
if (system instanceof Response) return system;
|
||||
const userId = auth.user.id;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, documentSourceImportSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
message: "Invalid import request",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = documentSourceImportSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiValidationError(
|
||||
"Invalid import request",
|
||||
sanitiseZodIssues(parsed.error.issues),
|
||||
422,
|
||||
);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const { sourceId, kind, link } = parsed.data;
|
||||
// Only an id this system could have issued goes into a request path.
|
||||
if (!isSourceDocumentId(system, sourceId)) {
|
||||
|
||||
@@ -18,14 +18,8 @@
|
||||
import { 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 { decrypt, encrypt } from "@/lib/crypto";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -70,18 +64,11 @@ export const PUT = apiHandler(
|
||||
);
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, documentSourceSaveSchema, {
|
||||
maxBytes: 8 * 1024,
|
||||
message: "Invalid connection details",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = documentSourceSaveSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiValidationError(
|
||||
"Invalid connection details",
|
||||
sanitiseZodIssues(parsed.error.issues),
|
||||
422,
|
||||
);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const verdict = evaluateSourceBaseUrl(parsed.data.baseUrl);
|
||||
if (!verdict.ok) {
|
||||
|
||||
@@ -20,12 +20,8 @@ import { NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, apiError } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { encounterLinkSchema } from "@/lib/validations/encounters";
|
||||
import { linkTargets, unlinkTargets } from "@/lib/links";
|
||||
import { loadEncounterLinks } from "@/lib/encounters/service";
|
||||
@@ -42,17 +38,11 @@ async function handle(
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, encounterLinkSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
errorCode: "encounter.link.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = encounterLinkSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "encounter.link.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const apply = verb === "link" ? linkTargets : unlinkTargets;
|
||||
const result = await apply(prisma, {
|
||||
|
||||
@@ -22,13 +22,8 @@ import { invalidateUserHealthContext } from "@/lib/cache/invalidate";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { overwriteDetails } from "@/lib/sharing/audit-details";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
|
||||
import { encounterUpdateSchema } from "@/lib/validations/encounters";
|
||||
import { encounterKindLabel } from "@/lib/encounters/kind-label";
|
||||
@@ -107,17 +102,11 @@ export const PATCH = apiHandler(
|
||||
return apiError("Visit not found", 404);
|
||||
}
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, encounterUpdateSchema, {
|
||||
maxBytes: 32 * 1024,
|
||||
errorCode: "encounter.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = encounterUpdateSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "encounter.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const entry = parsed.data;
|
||||
const { timezone, locale } = await resolveOwnerNotificationContext(user.id);
|
||||
|
||||
@@ -23,8 +23,8 @@ import {
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { apiListSuccess } from "@/lib/api/list-cap";
|
||||
import { combinedCap, listPage } from "@/lib/http/list-query";
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
@@ -180,21 +180,13 @@ export const POST = apiHandler(withIdempotency<[NextRequest]>(postEncounter));
|
||||
async function postEncounter(request: NextRequest): Promise<Response> {
|
||||
const { user, grantId } = await requireRecordAuth("write", "profile");
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, encounterCreateSchema, {
|
||||
maxBytes: 32 * 1024,
|
||||
action: "encounter.visit.validation-failed",
|
||||
countIssues: true,
|
||||
errorCode: "encounter.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = encounterCreateSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
annotate({
|
||||
action: { name: "encounter.visit.validation-failed" },
|
||||
meta: { issue_count: parsed.error.issues.length },
|
||||
});
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "encounter.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const entry = parsed.data;
|
||||
const occurredAt = new Date(entry.occurredAt);
|
||||
|
||||
@@ -15,12 +15,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import {
|
||||
@@ -49,17 +45,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
return apiError("Too many requests. Please retry shortly.", 429);
|
||||
}
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, environmentBackfillSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
errorCode: "environment.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = environmentBackfillSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "environment.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const profile = await prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { apiSuccess, returnAllZodIssues, safeJson } from "@/lib/api-response";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -26,21 +27,13 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
const gate = await requireModuleEnabled(user.id, "environment");
|
||||
if (!gate.enabled) return gate.response;
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, homeLocationSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
action: "environment.home.validation-failed",
|
||||
countIssues: true,
|
||||
errorCode: "environment.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = homeLocationSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
annotate({
|
||||
action: { name: "environment.home.validation-failed" },
|
||||
meta: { issue_count: parsed.error.issues.length },
|
||||
});
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "environment.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const { lat, lon, label, timezone } = parsed.data;
|
||||
// Stamp the effective-from instant: from now on this home resolves days; days
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { apiSuccess, returnAllZodIssues, safeJson } from "@/lib/api-response";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -27,17 +28,11 @@ export const PATCH = apiHandler(async (request: NextRequest) => {
|
||||
const gate = await requireModuleEnabled(user.id, "environment");
|
||||
if (!gate.enabled) return gate.response;
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, environmentPreferencesSchema, {
|
||||
maxBytes: 1024,
|
||||
errorCode: "environment.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = environmentPreferencesSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "environment.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const before = await prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { apiSuccess, returnAllZodIssues, safeJson } from "@/lib/api-response";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -29,21 +30,13 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const gate = await requireModuleEnabled(user.id, "environment");
|
||||
if (!gate.enabled) return gate.response;
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, travelLocationSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
action: "environment.travel.validation-failed",
|
||||
countIssues: true,
|
||||
errorCode: "environment.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = travelLocationSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
annotate({
|
||||
action: { name: "environment.travel.validation-failed" },
|
||||
meta: { issue_count: parsed.error.issues.length },
|
||||
});
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "environment.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const entry = parsed.data;
|
||||
const location = {
|
||||
|
||||
@@ -31,12 +31,8 @@ import { z } from "zod/v4";
|
||||
import { apiHandler, requireRecentProof } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import {
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
@@ -105,15 +101,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
// The body is a single passphrase string — small and bounded. 8 KB is far
|
||||
// above any legitimate passphrase while rejecting a large body before parse.
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, encryptedExportSchema, {
|
||||
maxBytes: 8 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = encryptedExportSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const { passphrase } = parsed.data;
|
||||
await auth.commitElevation();
|
||||
|
||||
|
||||
@@ -29,12 +29,8 @@ import {
|
||||
} from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import {
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import { prisma, toJson } from "@/lib/db";
|
||||
@@ -122,15 +118,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
// The selection payload is small and bounded. 64 KB is far above any
|
||||
// legitimate one while still rejecting a multi-megabyte body before it
|
||||
// reaches `JSON.parse`. A DoS ceiling, not a tight bound.
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, exportSelectionSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = exportSelectionSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const input = parsed.data;
|
||||
|
||||
const minted = selectionFromRequest(input.selection);
|
||||
|
||||
@@ -14,13 +14,8 @@ import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { overwriteDetails } from "@/lib/sharing/audit-details";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
|
||||
import { familyHistoryUpdateSchema } from "@/lib/validations/family-history";
|
||||
import { toFamilyHistoryEntryDTO } from "@/lib/records/dto";
|
||||
@@ -76,17 +71,11 @@ export const PATCH = apiHandler(
|
||||
return apiError("Family history entry not found", 404);
|
||||
}
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, familyHistoryUpdateSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
errorCode: "familyHistory.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = familyHistoryUpdateSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "familyHistory.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const entry = parsed.data;
|
||||
|
||||
// Field-by-field — only fields actually present in the body are written.
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
apiSuccess,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
|
||||
import {
|
||||
@@ -80,21 +80,13 @@ async function postFamilyHistory(request: NextRequest): Promise<Response> {
|
||||
// it (`recordSharing.anamnesis.relativeNote`).
|
||||
const { user } = await requireRecordAuth("manage", "profile");
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, familyHistoryCreateSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
action: "family-history.validation-failed",
|
||||
countIssues: true,
|
||||
errorCode: "familyHistory.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = familyHistoryCreateSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
annotate({
|
||||
action: { name: "family-history.validation-failed" },
|
||||
meta: { issue_count: parsed.error.issues.length },
|
||||
});
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "familyHistory.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const entry = parsed.data;
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ import {
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { requireIllnessEnabled } from "@/lib/illness/gate";
|
||||
import {
|
||||
illnessDayLogInputSchema,
|
||||
@@ -152,21 +152,13 @@ export const POST = apiHandler(
|
||||
const owned = await loadOwnedEpisode(id, user.id);
|
||||
if (!owned.ok) return owned.response;
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, illnessDayLogInputSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
action: "illness.day-log.validation-failed",
|
||||
countIssues: true,
|
||||
errorCode: "illness.day-log.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = illnessDayLogInputSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
annotate({
|
||||
action: { name: "illness.day-log.validation-failed" },
|
||||
meta: { issue_count: parsed.error.issues.length },
|
||||
});
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "illness.day-log.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const tz = user.timezone ?? DEFAULT_TIMEZONE;
|
||||
const result = await upsertIllnessDayLog(user.id, id, parsed.data, tz);
|
||||
|
||||
@@ -15,13 +15,8 @@ import { invalidateUserHealthContext } from "@/lib/cache/invalidate";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { overwriteDetails } from "@/lib/sharing/audit-details";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { requireIllnessEnabled } from "@/lib/illness/gate";
|
||||
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
|
||||
import { illnessEpisodeUpdateSchema } from "@/lib/validations/illness";
|
||||
@@ -98,17 +93,11 @@ export const PATCH = apiHandler(
|
||||
return apiError("Episode not found", 404);
|
||||
}
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, illnessEpisodeUpdateSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
errorCode: "illness.episode.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = illnessEpisodeUpdateSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "illness.episode.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const entry = parsed.data;
|
||||
|
||||
// Window invariant against the MERGED state: a partial edit touching only
|
||||
|
||||
@@ -22,8 +22,8 @@ import {
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { requireIllnessEnabled } from "@/lib/illness/gate";
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
|
||||
@@ -108,21 +108,13 @@ async function postEpisode(request: NextRequest): Promise<Response> {
|
||||
const gate = await requireIllnessEnabled(user.id);
|
||||
if (!gate.enabled) return gate.response;
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, illnessEpisodeCreateSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
action: "illness.episode.validation-failed",
|
||||
countIssues: true,
|
||||
errorCode: "illness.episode.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = illnessEpisodeCreateSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
annotate({
|
||||
action: { name: "illness.episode.validation-failed" },
|
||||
meta: { issue_count: parsed.error.issues.length },
|
||||
});
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "illness.episode.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const entry = parsed.data;
|
||||
|
||||
|
||||
@@ -11,13 +11,8 @@ import { type NextRequest } from "next/server";
|
||||
|
||||
import { requireAiCapability } from "@/lib/ai/capabilities/gate";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
apiValidationError,
|
||||
safeJson,
|
||||
sanitiseZodIssues,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
|
||||
@@ -72,21 +67,12 @@ export const POST = apiHandler(
|
||||
return response;
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, coachAttachmentCreateSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
errorCode: "coach.fenced.invalid",
|
||||
message: "Invalid attachment request",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = coachAttachmentCreateSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiValidationError(
|
||||
"Invalid attachment request",
|
||||
sanitiseZodIssues(parsed.error.issues),
|
||||
422,
|
||||
{
|
||||
errorCode: "coach.fenced.invalid",
|
||||
},
|
||||
);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const { documentId } = parsed.data;
|
||||
|
||||
// Ownership of the conversation — NOT filtered on `documentScoped` (this is
|
||||
|
||||
@@ -13,13 +13,8 @@ import type { NextRequest } from "next/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { COACH_CONVERSATION_TITLE_MAX } from "@/lib/ai/coach/types";
|
||||
@@ -75,17 +70,11 @@ export const PATCH = apiHandler(async (request: NextRequest, ctx: RouteCtx) => {
|
||||
const { id } = await ctx.params;
|
||||
if (!id) return apiError("coach.conversation.notFound", 404);
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, renameConversationSchema, {
|
||||
maxBytes: 4 * 1024,
|
||||
errorCode: "coach.conversation.invalidTitle",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = renameConversationSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "coach.conversation.invalidTitle",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const renamed = await renameConversation(auth.user.id, id, parsed.data.title);
|
||||
if (!renamed) {
|
||||
|
||||
@@ -17,12 +17,8 @@ import { type NextRequest } from "next/server";
|
||||
|
||||
import { requireAiCapability } from "@/lib/ai/capabilities/gate";
|
||||
import { apiHandler, requireAuth, HttpError } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiValidationError,
|
||||
safeJson,
|
||||
sanitiseZodIssues,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
|
||||
@@ -65,21 +61,12 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
await requireAiCapability("coach", { pickDecides: true });
|
||||
await requireAiCapability("documentAi", { pickDecides: true });
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, fencedChatRequestSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
errorCode: "coach.fenced.invalid",
|
||||
message: "Invalid fenced chat request",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = fencedChatRequestSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiValidationError(
|
||||
"Invalid fenced chat request",
|
||||
sanitiseZodIssues(parsed.error.issues),
|
||||
422,
|
||||
{
|
||||
errorCode: "coach.fenced.invalid",
|
||||
},
|
||||
);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const {
|
||||
conversationId,
|
||||
message,
|
||||
|
||||
@@ -30,12 +30,8 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
|
||||
@@ -73,12 +69,10 @@ export const PATCH = apiHandler(async (req: NextRequest, ctx: RouteCtx) => {
|
||||
}
|
||||
|
||||
const { id } = await ctx.params;
|
||||
const { data: body, error: jsonError } = await safeJson(req, {
|
||||
const parsed = await parseJsonBody(req, coachFactPatchSchema, {
|
||||
maxBytes: 2 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = coachFactPatchSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const text = parsed.data.fact.replace(/\s+/g, " ");
|
||||
const live = {
|
||||
|
||||
@@ -31,12 +31,8 @@
|
||||
* Erasure that depends on the feature being on is no erasure.
|
||||
*/
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { decryptFromBytes } from "@/lib/ai/coach/bytes-codec";
|
||||
@@ -142,12 +138,10 @@ export const POST = apiHandler(async (req: Request) => {
|
||||
return response;
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(req, {
|
||||
const parsed = await parseJsonBody(req, coachFactCreateSchema, {
|
||||
maxBytes: 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = coachFactCreateSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const remembered = await rememberMessageAsFact({
|
||||
userId: user.id,
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
apiSuccess,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { invalidateUserCorrelationPatterns } from "@/lib/cache/invalidate";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -28,13 +28,12 @@ export const PATCH = apiHandler(
|
||||
const parsedId = patternIdSchema.safeParse((await params).id);
|
||||
if (!parsedId.success) return returnAllZodIssues(parsedId.error, 422);
|
||||
const id = parsedId.data;
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
maxBytes: 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = updateCorrelationPatternSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
const parsed = await parseJsonBody(
|
||||
request,
|
||||
updateCorrelationPatternSchema,
|
||||
{ maxBytes: 1024 },
|
||||
);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const pattern = await prisma.correlationPattern.findFirst({
|
||||
where: { id, userId: user.id, isCurrent: true },
|
||||
|
||||
@@ -11,12 +11,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { linkTargets, unlinkTargets } from "@/lib/links";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
@@ -32,16 +28,11 @@ async function handle(
|
||||
const { user } = await requireAuth();
|
||||
const { id } = await params;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, labResultLinkSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
errorCode: "labs.link.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = labResultLinkSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "labs.link.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const apply = verb === "link" ? linkTargets : unlinkTargets;
|
||||
const result = await apply(prisma, {
|
||||
|
||||
@@ -15,13 +15,8 @@ import { z } from "zod/v4";
|
||||
|
||||
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
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 { prisma } from "@/lib/db";
|
||||
import { invalidateUserHealthScore } from "@/lib/cache/invalidate";
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
@@ -59,17 +54,11 @@ async function postRestore(request: NextRequest): Promise<Response> {
|
||||
return apiError("Too many restore requests, try again later", 429);
|
||||
}
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, restoreSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
errorCode: "labs.restore.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = restoreSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "labs.restore.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const { ids } = parsed.data;
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
apiSuccess,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import {
|
||||
lifeEventIdPathSchema,
|
||||
@@ -48,17 +48,11 @@ export const PATCH = apiHandler(
|
||||
const existing = await findOwnLifeEvent(user.id, path.data.id);
|
||||
if (!existing) return notFound();
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, lifeEventUpdateSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
errorCode: "life_event.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = lifeEventUpdateSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "life_event.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const merged = mergeLifeEventEdit(existing, parsed.data);
|
||||
if (!merged.ok) {
|
||||
return returnAllZodIssues(merged.error, 422, {
|
||||
|
||||
@@ -20,12 +20,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } 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 { auditLog } from "@/lib/auth/audit";
|
||||
import { lifeEventCreateSchema } from "@/lib/day/wire-schemas";
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
@@ -51,17 +47,11 @@ export const POST = apiHandler(withIdempotency<[NextRequest]>(postLifeEvent));
|
||||
async function postLifeEvent(request: NextRequest): Promise<Response> {
|
||||
const { user } = await requireAuth();
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, lifeEventCreateSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
errorCode: "life_event.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = lifeEventCreateSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "life_event.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const created = await createLifeEvent(user.id, parsed.data);
|
||||
|
||||
|
||||
@@ -6,12 +6,8 @@ import {
|
||||
requireCookieAuth,
|
||||
requireFreshMfa,
|
||||
} from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import {
|
||||
@@ -92,12 +88,12 @@ export const POST = apiHandler(
|
||||
if (!rateLimit.allowed) {
|
||||
return apiError("Too many invitations, try again later", 429);
|
||||
}
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
maxBytes: 8 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = inviteManagedProfileGuardianSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
const parsed = await parseJsonBody(
|
||||
request,
|
||||
inviteManagedProfileGuardianSchema,
|
||||
{ maxBytes: 8 * 1024 },
|
||||
);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const { id } = await params;
|
||||
|
||||
const invitee = await prisma.user.findFirst({
|
||||
|
||||
@@ -23,12 +23,8 @@ import {
|
||||
requireCookieAuth,
|
||||
requireFreshMfa,
|
||||
} from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { buildHandoverUrl } from "@/lib/auth/handover-token";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { handoverMethods } from "@/lib/managed-profiles/claim-gate";
|
||||
@@ -111,12 +107,10 @@ export const POST = apiHandler(
|
||||
if (handoverMethods().length === 0) return noMethodRefusal();
|
||||
const { id } = await params;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, createHandoverSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = createHandoverSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
let created;
|
||||
try {
|
||||
|
||||
@@ -6,12 +6,8 @@ import {
|
||||
requireCookieAuth,
|
||||
requireFreshMfa,
|
||||
} from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import {
|
||||
deleteManagedProfile,
|
||||
ManagedProfileLifecycleError,
|
||||
@@ -132,13 +128,10 @@ export const PATCH = apiHandler(
|
||||
}
|
||||
const { id } = await params;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, updateManagedProfileSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = updateManagedProfileSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
// The parsed patch goes to the service, which assembles the Prisma payload
|
||||
// field by field. One assembly rather than one here and one there: two
|
||||
|
||||
@@ -5,12 +5,8 @@ import {
|
||||
MFA_STEP_UP_MAX_AGE_SECONDS,
|
||||
requireFreshMfa,
|
||||
} from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { createManagedProfile } from "@/lib/managed-profiles/create";
|
||||
import { toManagedProfileView } from "@/lib/managed-profiles/lifecycle";
|
||||
@@ -48,13 +44,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
if (!rateLimit.allowed) {
|
||||
return apiError("Too many profiles created, try again later", 429);
|
||||
}
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, createManagedProfileSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = createManagedProfileSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const { profile, creatorGrant } = await createManagedProfile({
|
||||
creatorId: user.id,
|
||||
|
||||
@@ -22,12 +22,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth, requireRecentProof } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiError, apiSuccess } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { issueApiToken } from "@/lib/auth/issue-token";
|
||||
@@ -84,15 +80,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
return apiError("API is globally disabled", 403);
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, createMcpTokenSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = createMcpTokenSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
// Build the permission array explicitly from the closed scope choice —
|
||||
// never spread, never wildcard. `read` → read-only; `read_write` → read plus
|
||||
|
||||
@@ -20,13 +20,8 @@ import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
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 { withIdempotency } from "@/lib/idempotency";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { invalidateUserMeasurements } from "@/lib/cache/invalidate";
|
||||
@@ -64,17 +59,11 @@ async function postBulkDelete(request: NextRequest): Promise<Response> {
|
||||
return apiError("Too many bulk deletions, try again later", 429);
|
||||
}
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, bulkDeleteSchema, {
|
||||
maxBytes: 256 * 1024,
|
||||
errorCode: "measurement.bulk-delete.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = bulkDeleteSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "measurement.bulk-delete.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const { ids } = parsed.data;
|
||||
const now = new Date();
|
||||
|
||||
@@ -22,13 +22,8 @@ import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
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 { withIdempotency } from "@/lib/idempotency";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { invalidateUserMeasurements } from "@/lib/cache/invalidate";
|
||||
@@ -65,17 +60,11 @@ async function postRestore(request: NextRequest): Promise<Response> {
|
||||
return apiError("Too many restore requests, try again later", 429);
|
||||
}
|
||||
|
||||
const { data: rawBody, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, restoreSchema, {
|
||||
maxBytes: 256 * 1024,
|
||||
errorCode: "measurement.restore.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = restoreSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "measurement.restore.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const { ids } = parsed.data;
|
||||
|
||||
|
||||
@@ -5,13 +5,8 @@
|
||||
*/
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
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 { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { invalidateUserMedications } from "@/lib/cache/invalidate";
|
||||
@@ -44,16 +39,11 @@ export const PATCH = apiHandler(
|
||||
const { user } = await requireAuth();
|
||||
const { id, courseId } = await params;
|
||||
|
||||
const { data: raw, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, updateMedicationCourseSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
errorCode: "medications.course.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = updateMedicationCourseSchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "medications.course.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
try {
|
||||
const updated = await updateCourse({
|
||||
|
||||
@@ -13,13 +13,8 @@
|
||||
*/
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
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 { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { invalidateUserMedications } from "@/lib/cache/invalidate";
|
||||
@@ -71,16 +66,11 @@ async function postCourse(
|
||||
const { user } = await requireAuth();
|
||||
const { id } = await params;
|
||||
|
||||
const { data: raw, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, createMedicationCourseSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
errorCode: "medications.course.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = createMedicationCourseSchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "medications.course.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
try {
|
||||
const created = await createCourse({
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import {
|
||||
apiSuccess,
|
||||
apiError,
|
||||
returnAllZodIssues,
|
||||
safeJson,
|
||||
} from "@/lib/api-response";
|
||||
import { apiSuccess, apiError } from "@/lib/api-response";
|
||||
import { parseJsonBody } from "@/lib/api/body";
|
||||
import { assertMedicationOwnership } from "@/lib/medications/route-guards";
|
||||
import { efficacyTargetOverrideSchema } from "@/lib/validations/medication-efficacy";
|
||||
|
||||
@@ -32,15 +28,10 @@ export const PUT = apiHandler(
|
||||
const guard = await assertMedicationOwnership(id, user.id);
|
||||
if (guard) return guard;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, efficacyTargetOverrideSchema, {
|
||||
maxBytes: 8 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = efficacyTargetOverrideSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const { clear, measurementType, biomarkerId, primary } = parsed.data;
|
||||
|
||||
// Clearing reverts to the derived resolution — drop every override row.
|
||||
|
||||
@@ -18,13 +18,8 @@ import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
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 { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
|
||||
import { glp1PostBodySchema } from "@/lib/validations/medication";
|
||||
import { assertMedicationOwnership } from "@/lib/medications/route-guards";
|
||||
@@ -283,16 +278,10 @@ async function postGlp1Update(
|
||||
});
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, glp1PostBodySchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = glp1PostBodySchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
// v1.4.43 — multi-issue 422.
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const ip = getClientIp(request);
|
||||
|
||||
|
||||
@@ -17,13 +17,8 @@ import { NextRequest } from "next/server";
|
||||
|
||||
import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireRecordAuth } 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 { invalidateUserMedications } from "@/lib/cache/invalidate";
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
@@ -91,16 +86,11 @@ async function putIngredients(
|
||||
});
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, medicationIngredientsPutSchema, {
|
||||
maxBytes: 16 * 1024,
|
||||
errorCode: "medications.ingredients.invalid",
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = medicationIngredientsPutSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "medications.ingredients.invalid",
|
||||
});
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const check = canonicalComposition(parsed.data.ingredients);
|
||||
if (!check.ok) {
|
||||
|
||||
@@ -2,13 +2,8 @@ import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
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 { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
|
||||
import { bulkDeleteIntakeEventsSchema } from "@/lib/validations/medication";
|
||||
import { restoreForIntake } from "@/lib/medications/inventory/consumption";
|
||||
@@ -84,14 +79,10 @@ export const POST = apiHandler(
|
||||
});
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, bulkDeleteIntakeEventsSchema, {
|
||||
maxBytes: 256 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = bulkDeleteIntakeEventsSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
const { eventIds } = parsed.data;
|
||||
|
||||
// Tz the rollups are anchored on. The detail-page caller persists
|
||||
|
||||
@@ -28,13 +28,8 @@ import {
|
||||
overwriteDetails,
|
||||
} from "@/lib/sharing/audit-details";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
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 { updateInventoryItemSchema } from "@/lib/validations/medication";
|
||||
import {
|
||||
computeExpiresAt,
|
||||
@@ -53,16 +48,10 @@ export const PATCH = apiHandler(
|
||||
const { user } = await requireRecordAuth("manage", "medications");
|
||||
const { id, itemId } = await params;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
const parsed = await parseJsonBody(request, updateInventoryItemSchema, {
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = updateInventoryItemSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
// v1.4.43 — multi-issue 422.
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const {
|
||||
markAsFirstUseAt,
|
||||
|
||||
Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user