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:
co-authored by
Claude Opus 4.6
parent
55d0f84251
commit
2051a95e13
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user