refactor: add centralized Zod validators for all API inputs

Create server/src/lib/validators.ts with reusable schemas for all API
inputs (auth, fights, bets, payments, tournaments, queue, docs).
Import and use in all route handlers, replacing inline validation.
Add formatZodError helper for user-friendly error messages.
77 test cases in validators.test.ts cover valid, invalid, boundary,
and attack inputs (SQL injection, XSS, prototype pollution).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-13 09:16:04 +00:00
co-authored by Claude Opus 4.6
parent 55d0f84251
commit 2051a95e13
11 changed files with 549 additions and 122 deletions
+15 -18
View File
@@ -10,6 +10,7 @@ import {
getTournamentBracket,
listTournaments,
} from '../engine/tournaments.js'
import { createTournamentSchema, joinTournamentSchema, startTournamentSchema, formatZodError } from '../lib/validators.js'
export const tournamentsRouter = new Hono()
@@ -30,27 +31,21 @@ tournamentsRouter.get('/:id', async (c) => {
// Create a tournament (admin only — creator pubkey required)
tournamentsRouter.post('/', async (c) => {
const body = await c.req.json<{
pubkey: string
name: string
format?: 'single_elim' | 'round_robin'
size?: 8 | 16 | 32
entrySats?: number
}>()
const parsed = createTournamentSchema.safeParse(await c.req.json().catch(() => ({})))
if (!parsed.success) {
return c.json({ error: formatZodError(parsed.error, {
name: 'Tournament name required (1-100 chars)',
size: 'Size must be 8, 16, or 32',
}, 'Invalid tournament data') }, 400)
}
const body = parsed.data
if (!isCreatorPubkey(body.pubkey)) {
return c.json({ error: 'Only the creator can create tournaments' }, 403)
}
if (!body.name || body.name.length < 1 || body.name.length > 100) {
return c.json({ error: 'Tournament name required (1-100 chars)' }, 400)
}
const format = body.format ?? 'single_elim'
const size = body.size ?? 8
if (![8, 16, 32].includes(size)) {
return c.json({ error: 'Size must be 8, 16, or 32' }, 400)
}
const id = createTournament(body.name, format, size, body.entrySats ?? 0)
return c.json({ id, name: body.name, format, size }, 201)
@@ -59,9 +54,9 @@ tournamentsRouter.post('/', async (c) => {
// Join a tournament
tournamentsRouter.post('/:id/join', async (c) => {
const tournamentId = c.req.param('id')
const body = await c.req.json<{ pubkey: string; paymentId?: string }>()
if (!body.pubkey) return c.json({ error: 'pubkey required' }, 400)
const parsed = joinTournamentSchema.safeParse(await c.req.json().catch(() => ({})))
if (!parsed.success) return c.json({ error: formatZodError(parsed.error, { pubkey: 'pubkey required' }, 'pubkey required') }, 400)
const body = parsed.data
// Look up bot by pubkey
const bot = db.select().from(schema.bots)
@@ -81,7 +76,9 @@ tournamentsRouter.post('/:id/join', async (c) => {
// Start a tournament (admin only)
tournamentsRouter.post('/:id/start', async (c) => {
const tournamentId = c.req.param('id')
const body = await c.req.json<{ pubkey: string }>()
const parsed = startTournamentSchema.safeParse(await c.req.json().catch(() => ({})))
if (!parsed.success) return c.json({ error: formatZodError(parsed.error, { pubkey: 'pubkey required' }, 'pubkey required') }, 400)
const body = parsed.data
if (!isCreatorPubkey(body.pubkey)) {
return c.json({ error: 'Only the creator can start tournaments' }, 403)