mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
fix(admin): return 422 for a malformed query string
The app-logs, audit-log and host-metrics admin endpoints parsed their query with a throwing parse(), so a bad parameter surfaced as a 500 and a reported incident. Switch each to safeParse and return the standard 422 validation envelope, matching how body parsing is handled.
This commit is contained in:
1 parent
fb54b46034
commit
561a0694cf
4 files changed
+26
-11
No files matched your search
@@ -119,11 +119,20 @@ describe("GET /api/admin/app-logs", () => {
|
||||
expect(json.data.events).toHaveLength(10);
|
||||
});
|
||||
|
||||
it("rejects an invalid level value", async () => {
|
||||
it("rejects an invalid level value with a 422 (not a thrown 500)", async () => {
|
||||
appendLogEvent(makeEvent());
|
||||
// Invalid `level` — Zod parse should throw and the apiHandler test
|
||||
// mock rethrows it (we mock only `requireAdmin`, not the wrapper
|
||||
// error-translator). We simply assert the call rejects.
|
||||
await expect(GET(req("level=critical"))).rejects.toThrow();
|
||||
// Invalid `level` — the query is parsed with `safeParse` and returns the
|
||||
// standard 422 multi-issue envelope rather than throwing (which the
|
||||
// apiHandler catch ladder would otherwise surface as a 500 + incident).
|
||||
const res = await GET(req("level=critical"));
|
||||
expect(res.status).toBe(422);
|
||||
const json = (await res.json()) as {
|
||||
data: null;
|
||||
error: string;
|
||||
details: { issues: unknown[] };
|
||||
};
|
||||
expect(json.data).toBeNull();
|
||||
expect(json.error).toBe("Validation failed");
|
||||
expect(json.details.issues.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { apiHandler, requireAdmin } from "@/lib/api-handler";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import { apiSuccess, returnAllZodIssues } from "@/lib/api-response";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { readLogBuffer, LOG_BUFFER_MAX } from "@/lib/logging/in-memory-buffer";
|
||||
import { redactSecrets } from "@/lib/logging/redact";
|
||||
@@ -35,7 +35,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
annotate({ action: { name: "admin.app-logs.list" } });
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const parsed = querySchema.parse({
|
||||
const result = querySchema.safeParse({
|
||||
traceId: searchParams.get("traceId") ?? undefined,
|
||||
level: searchParams.get("level") ?? undefined,
|
||||
action: searchParams.get("action") ?? undefined,
|
||||
@@ -43,6 +43,8 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
until: searchParams.get("until") ?? undefined,
|
||||
limit: searchParams.get("limit") ?? undefined,
|
||||
});
|
||||
if (!result.success) return returnAllZodIssues(result.error);
|
||||
const parsed = result.data;
|
||||
|
||||
const events = readLogBuffer({
|
||||
traceId: parsed.traceId,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireAdmin } from "@/lib/api-handler";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import { apiSuccess, returnAllZodIssues } from "@/lib/api-response";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { redactSecrets } from "@/lib/logging/redact";
|
||||
import { NextRequest } from "next/server";
|
||||
@@ -56,7 +56,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
annotate({ action: { name: "admin.audit-log.list" } });
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const parsed = querySchema.parse({
|
||||
const result = querySchema.safeParse({
|
||||
limit: searchParams.get("limit") ?? undefined,
|
||||
offset: searchParams.get("offset") ?? undefined,
|
||||
filter: searchParams.get("filter") ?? undefined,
|
||||
@@ -68,6 +68,8 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
since: searchParams.get("since") ?? undefined,
|
||||
until: searchParams.get("until") ?? undefined,
|
||||
});
|
||||
if (!result.success) return returnAllZodIssues(result.error);
|
||||
const parsed = result.data;
|
||||
|
||||
// Resolve perPage: clamp to allowed set, fall back to 50.
|
||||
const perPageResolved =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireAdmin } from "@/lib/api-handler";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import { apiSuccess, returnAllZodIssues } from "@/lib/api-response";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { NextRequest } from "next/server";
|
||||
import { z } from "zod/v4";
|
||||
@@ -59,9 +59,11 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
annotate({ action: { name: "admin.host-metrics.list" } });
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const parsed = querySchema.parse({
|
||||
const result = querySchema.safeParse({
|
||||
since: searchParams.get("since") ?? undefined,
|
||||
});
|
||||
if (!result.success) return returnAllZodIssues(result.error);
|
||||
const parsed = result.data;
|
||||
|
||||
const sinceMs = presetToMs(parsed.since);
|
||||
const cutoff = new Date(Date.now() - sinceMs);
|
||||
|
||||
Reference in new issue
Block a user