From 5723fd36f97f25791aae1d68f289c7ef0a9e761e Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Thu, 21 May 2026 23:46:29 +0200 Subject: [PATCH] refactor(admin): validate JSON feed update via @hono/zod-validator Moves validation of POST /api/feeds/:feedId/update from inline schema.parse() to zValidator middleware. The route now receives typed validated data via c.req.valid("json"), and returns a structured {success: false, error: ZodIssue[]} on invalid input. Co-Authored-By: Claude Sonnet 4.6 --- src/routes/admin.test.ts | 17 +++++++ src/routes/admin.ts | 102 ++++++++++++++++++++------------------- 2 files changed, 70 insertions(+), 49 deletions(-) diff --git a/src/routes/admin.test.ts b/src/routes/admin.test.ts index a3f3289..cf325f4 100644 --- a/src/routes/admin.test.ts +++ b/src/routes/admin.test.ts @@ -187,6 +187,23 @@ describe("Admin Routes", () => { }); }); + describe("API Feed Update", () => { + it("returns 400 with structured validation error for empty title", async () => { + const authCookie = await loginAndGetCookie(); + const res = await request("/admin/api/feeds/test-feed/update", { + method: "POST", + headers: { + Cookie: authCookie, + "Content-Type": "application/json", + }, + body: JSON.stringify({ title: "", description: "desc" }), + }); + expect(res.status).toBe(400); + const body = await res.json<{ success: boolean }>(); + expect(body.success).toBe(false); + }); + }); + describe("Feed Management", () => { it("should prevent feed deletion without authentication", async () => { const res = await request("/admin/feeds/test-feed/delete", { diff --git a/src/routes/admin.ts b/src/routes/admin.ts index d6b384e..7554524 100644 --- a/src/routes/admin.ts +++ b/src/routes/admin.ts @@ -1,6 +1,7 @@ import { Context, Hono } from "hono"; import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie"; import { html, raw } from "hono/html"; +import { zValidator } from "@hono/zod-validator"; import { z } from "zod"; import { Env, @@ -3756,60 +3757,63 @@ async function removeFeedFromList( } // Update feed via API (for in-place editing) -app.post("/api/feeds/:feedId/update", async (c) => { - // Type assertion for environment variables - const env = c.env as unknown as Env; - const emailStorage = env.EMAIL_STORAGE; - const feedId = c.req.param("feedId"); +app.post( + "/api/feeds/:feedId/update", + zValidator( + "json", + updateFeedSchema.pick({ title: true, description: true }), + (result, c) => { + if (!result.success) + return c.json({ success: false, error: result.error.issues }, 400); + }, + ), + async (c) => { + // Type assertion for environment variables + const env = c.env as unknown as Env; + const emailStorage = env.EMAIL_STORAGE; + const feedId = c.req.param("feedId"); - try { - // Parse JSON data from request - const data = await c.req.json(); - const { title, description } = data; + try { + const { title, description } = c.req.valid("json"); + const parsedData = { title, description, language: "en" as const }; - // Validate inputs - const parsedData = updateFeedSchema.parse({ - title, - description, - language: "en", // We're defaulting to English - }); + // Get existing feed config + const feedConfigKey = `feed:${feedId}:config`; + const existingConfig = (await emailStorage.get(feedConfigKey, { + type: "json", + })) as FeedConfig | null; - // Get existing feed config - const feedConfigKey = `feed:${feedId}:config`; - const existingConfig = (await emailStorage.get(feedConfigKey, { - type: "json", - })) as FeedConfig | null; + if (!existingConfig) { + return c.json({ error: "Feed not found" }, 404); + } - if (!existingConfig) { - return c.json({ error: "Feed not found" }, 404); + // Update feed configuration + await emailStorage.put( + feedConfigKey, + JSON.stringify({ + ...existingConfig, + title: parsedData.title, + description: parsedData.description, + updated_at: Date.now(), + }), + ); + + // Update feed in the list of all feeds + await updateFeedInList( + emailStorage, + feedId, + parsedData.title, + parsedData.description, + ); + + // Return success response + return c.json({ success: true }); + } catch (error) { + console.error("Error updating feed via API:", error); + return c.json({ error: "Error updating feed" }, 400); } - - // Update feed configuration - await emailStorage.put( - feedConfigKey, - JSON.stringify({ - ...existingConfig, - title: parsedData.title, - description: parsedData.description, - updated_at: Date.now(), - }), - ); - - // Update feed in the list of all feeds - await updateFeedInList( - emailStorage, - feedId, - parsedData.title, - parsedData.description, - ); - - // Return success response - return c.json({ success: true }); - } catch (error) { - console.error("Error updating feed via API:", error); - return c.json({ error: "Error updating feed" }, 400); - } -}); + }, +); // Export the Hono app export const handle = app;