From b988cddaa7769c12df1c7e2dae040affe545af8f Mon Sep 17 00:00:00 2001 From: Gandalf Date: Tue, 30 Dec 2025 13:48:46 +0100 Subject: [PATCH] Add validation for search page URL parameters Introduce Zod-based validation for search parameters to improve UX and avoid unnecessary API calls with invalid inputs: - Add search-params.ts with schema validation for q, ai, a, p, s params - Validate author parameter format (Hive username rules) - Enforce max length limits on search queries (500 chars) - Validate sort parameter against allowed values Invalid parameters are silently omitted rather than causing errors, providing graceful degradation for malformed URLs. --- apps/blog/app/search/page.tsx | 12 +++--- packages/ui/lib/search-params.ts | 63 ++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 packages/ui/lib/search-params.ts diff --git a/apps/blog/app/search/page.tsx b/apps/blog/app/search/page.tsx index 2a99bee95..3d9e88ee8 100644 --- a/apps/blog/app/search/page.tsx +++ b/apps/blog/app/search/page.tsx @@ -6,6 +6,7 @@ import { searchPosts } from '@transaction/lib/hivesense-api'; import { getByText } from '@transaction/lib/hive-api'; import { getObserverFromCookies } from '@/blog/lib/auth-utils'; import { getLogger } from '@ui/lib/logging'; +import { parseSearchParams } from '@ui/lib/search-params'; interface SearchPageProps { searchParams: { [key: string]: string | string[] | undefined }; @@ -14,11 +15,12 @@ interface SearchPageProps { const logger = getLogger('app'); const SearchPage = async ({ searchParams }: SearchPageProps) => { - const aiParam = searchParams.ai as string | undefined; - const classicQuery = searchParams.q as string | undefined; - const userTopicQuery = searchParams.a as string | undefined; - const topicQuery = searchParams.p as string | undefined; - const sortQuery = searchParams.s as SearchSort | undefined; + const validatedParams = parseSearchParams(searchParams); + const aiParam = validatedParams.ai; + const classicQuery = validatedParams.q; + const userTopicQuery = validatedParams.a; + const topicQuery = validatedParams.p; + const sortQuery = validatedParams.s as SearchSort | undefined; const queryClient = getQueryClient(); try { diff --git a/packages/ui/lib/search-params.ts b/packages/ui/lib/search-params.ts new file mode 100644 index 000000000..6f2f3d1b0 --- /dev/null +++ b/packages/ui/lib/search-params.ts @@ -0,0 +1,63 @@ +import { z } from 'zod'; + +/** + * Validates a Hive account name format for search purposes. + * This is a simplified check to avoid wasted API calls on obviously invalid usernames. + * Full validation (bad actor lists, etc.) happens elsewhere. + */ +const hiveAccountSchema = z + .string() + .min(3, 'Account name must be at least 3 characters') + .max(16, 'Account name must be at most 16 characters') + .regex(/^[a-z]/, 'Account name must start with a letter') + .regex(/^[a-z0-9.-]+$/, 'Account name can only contain lowercase letters, numbers, dots, and hyphens') + .regex(/[a-z0-9]$/, 'Account name must end with a letter or number') + .refine((val) => !val.includes('--'), 'Account name cannot contain consecutive hyphens'); + +/** + * Schema for validating search page URL parameters. + * Ensures parameters are within reasonable bounds before making API calls. + */ +export const searchParamsSchema = z.object({ + q: z.string().max(500, 'Search query too long').optional(), + ai: z.string().max(500, 'Search query too long').optional(), + a: hiveAccountSchema.optional(), + p: z.string().max(500, 'Topic pattern too long').optional(), + s: z.enum(['relevance', 'created']).optional() +}); + +export type SearchParams = z.infer; + +/** + * Parses and validates search parameters from URL. + * Returns validated params or null values for invalid fields. + */ +export function parseSearchParams(params: Record): SearchParams { + const normalized = { + q: typeof params.q === 'string' ? params.q : undefined, + ai: typeof params.ai === 'string' ? params.ai : undefined, + a: typeof params.a === 'string' ? params.a : undefined, + p: typeof params.p === 'string' ? params.p : undefined, + s: typeof params.s === 'string' ? params.s : undefined + }; + + const result = searchParamsSchema.safeParse(normalized); + + if (result.success) { + return result.data; + } + + // Return only valid fields, omit invalid ones + const validParams: SearchParams = {}; + const errors = result.error.flatten().fieldErrors; + + if (!errors.q && normalized.q) validParams.q = normalized.q; + if (!errors.ai && normalized.ai) validParams.ai = normalized.ai; + if (!errors.a && normalized.a) validParams.a = normalized.a; + if (!errors.p && normalized.p) validParams.p = normalized.p; + if (!errors.s && (normalized.s === 'relevance' || normalized.s === 'created')) { + validParams.s = normalized.s; + } + + return validParams; +} -- GitLab