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
+18
-18
@@ -1,5 +1,6 @@
|
||||
import { Hono } from 'hono'
|
||||
import { toError } from '../lib/utils.js'
|
||||
import { placeBetSchema, depositSchema, withdrawSchema, formatZodError } from '../lib/validators.js'
|
||||
import { getDecodedToken } from '@cashu/cashu-ts'
|
||||
import { db, schema } from '../db/index.js'
|
||||
import { eq, desc } from 'drizzle-orm'
|
||||
@@ -49,16 +50,13 @@ betsRouter.get('/odds/:fightId', async (c) => {
|
||||
|
||||
// Place a bet
|
||||
betsRouter.post('/place', rateLimit(60_000, 10), async (c) => {
|
||||
const body = await c.req.json()
|
||||
const { fightId, pubkey, botId, amountSats, cashuToken } = body
|
||||
|
||||
if (!fightId || !pubkey || !botId || !amountSats || !cashuToken) {
|
||||
return c.json({ error: 'Missing required fields.' }, 400)
|
||||
}
|
||||
|
||||
if (typeof amountSats !== 'number' || !Number.isInteger(amountSats) || amountSats < 1 || amountSats > 1_000_000) {
|
||||
return c.json({ error: 'amountSats must be an integer between 1 and 1,000,000' }, 400)
|
||||
const parsed = placeBetSchema.safeParse(await c.req.json().catch(() => ({})))
|
||||
if (!parsed.success) {
|
||||
return c.json({ error: formatZodError(parsed.error, {
|
||||
amountSats: 'amountSats must be an integer between 1 and 1,000,000',
|
||||
}, 'Missing required fields.') }, 400)
|
||||
}
|
||||
const { fightId, pubkey, botId, amountSats, cashuToken } = parsed.data
|
||||
|
||||
// Validate Cashu token format before any DB lookups
|
||||
try {
|
||||
@@ -161,13 +159,14 @@ betsRouter.get('/history/:pubkey', async (c) => {
|
||||
|
||||
// Lightning deposit — get invoice
|
||||
betsRouter.post('/deposit', rateLimit(60_000, 5), async (c) => {
|
||||
const { amountSats, pubkey } = await c.req.json()
|
||||
if (!amountSats || !pubkey) {
|
||||
return c.json({ error: 'Missing amountSats or pubkey.' }, 400)
|
||||
}
|
||||
if (amountSats < 100 || amountSats > 1_000_000) {
|
||||
return c.json({ error: 'Amount must be 100-1,000,000 sats.' }, 400)
|
||||
const parsed = depositSchema.safeParse(await c.req.json().catch(() => ({})))
|
||||
if (!parsed.success) {
|
||||
return c.json({ error: formatZodError(parsed.error, {
|
||||
amountSats: 'Amount must be 100-1,000,000 sats.',
|
||||
pubkey: 'Missing amountSats or pubkey.',
|
||||
}, 'Missing amountSats or pubkey.') }, 400)
|
||||
}
|
||||
const { amountSats, pubkey } = parsed.data
|
||||
|
||||
const invoice = await createDepositInvoice(amountSats, pubkey)
|
||||
return c.json(invoice)
|
||||
@@ -175,10 +174,11 @@ betsRouter.post('/deposit', rateLimit(60_000, 5), async (c) => {
|
||||
|
||||
// Lightning withdraw — burn Cashu tokens, pay LN invoice
|
||||
betsRouter.post('/withdraw', rateLimit(60_000, 3), async (c) => {
|
||||
const { cashuToken, bolt11 } = await c.req.json()
|
||||
if (!cashuToken || !bolt11) {
|
||||
return c.json({ error: 'Missing cashuToken or bolt11 invoice.' }, 400)
|
||||
const parsed = withdrawSchema.safeParse(await c.req.json().catch(() => ({})))
|
||||
if (!parsed.success) {
|
||||
return c.json({ error: parsed.error.issues[0]?.message || 'Missing cashuToken or bolt11 invoice.' }, 400)
|
||||
}
|
||||
const { cashuToken, bolt11 } = parsed.data
|
||||
|
||||
const result = await withdrawToLightning(cashuToken, bolt11)
|
||||
if (!result.success) {
|
||||
|
||||
Reference in New Issue
Block a user