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
+22
-34
@@ -8,6 +8,7 @@ import { validateCustomization } from '../engine/customization.js'
|
||||
import { testWebhook } from '../engine/webhook-test.js'
|
||||
import { rateLimit } from '../middleware/rate-limit.js'
|
||||
import { isCreatorPubkey } from '../lib/constants.js'
|
||||
import { loginSchema, registerSchema, registerHumanSchema, updateBotSchema, pubkeySchema, formatZodError } from '../lib/validators.js'
|
||||
|
||||
export const authRouter = new Hono()
|
||||
|
||||
@@ -26,12 +27,11 @@ authRouter.get("/check-name/:name", async (c) => {
|
||||
|
||||
// Login with Nostr pubkey (rate limited: 30 per minute per IP)
|
||||
authRouter.post('/login', rateLimit(60_000, 30), async (c) => {
|
||||
const body = await c.req.json()
|
||||
const { pubkey } = body
|
||||
|
||||
if (!pubkey || typeof pubkey !== 'string' || pubkey.length !== 64) {
|
||||
const parsed = loginSchema.safeParse(await c.req.json().catch(() => ({})))
|
||||
if (!parsed.success) {
|
||||
return c.json({ error: 'Invalid pubkey.' }, 400)
|
||||
}
|
||||
const { pubkey } = parsed.data
|
||||
|
||||
const rows = await db.select({
|
||||
id: schema.bots.id,
|
||||
@@ -130,20 +130,15 @@ authRouter.post('/login', rateLimit(60_000, 30), async (c) => {
|
||||
|
||||
// Register a new bot with Nostr pubkey
|
||||
authRouter.post('/register', rateLimit(600_000, 10), async (c) => {
|
||||
const body = await c.req.json()
|
||||
const { pubkey, name, webhookUrl, archetype, profilePicUrl, customization: rawCustomization } = body
|
||||
|
||||
if (!pubkey || typeof pubkey !== 'string' || pubkey.length !== 64) {
|
||||
return c.json({ error: 'Invalid pubkey.' }, 400)
|
||||
}
|
||||
|
||||
if (!name || typeof name !== 'string' || name.length < 2 || name.length > 12) {
|
||||
return c.json({ error: 'Name must be 2-12 characters.' }, 400)
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
||||
return c.json({ error: 'Name must be alphanumeric, hyphens, or underscores.' }, 400)
|
||||
const parsed = registerSchema.safeParse(await c.req.json().catch(() => ({})))
|
||||
if (!parsed.success) {
|
||||
return c.json({ error: formatZodError(parsed.error, {
|
||||
pubkey: 'Invalid pubkey.',
|
||||
name: 'Name must be 2-12 alphanumeric characters, hyphens, or underscores.',
|
||||
webhookUrl: 'webhookUrl must be a valid URL.',
|
||||
}, 'Invalid registration data.') }, 400)
|
||||
}
|
||||
const { pubkey, name, webhookUrl, archetype, profilePicUrl, customization: rawCustomization } = parsed.data
|
||||
|
||||
// Validate customization
|
||||
const custResult = validateCustomization(rawCustomization)
|
||||
@@ -242,20 +237,14 @@ authRouter.post('/register', rateLimit(600_000, 10), async (c) => {
|
||||
|
||||
// Register a human player (no webhook required)
|
||||
authRouter.post('/register-human', rateLimit(600_000, 10), async (c) => {
|
||||
const body = await c.req.json()
|
||||
const { pubkey, name, profilePicUrl, avatarSeed } = body
|
||||
|
||||
if (!pubkey || typeof pubkey !== 'string' || pubkey.length !== 64) {
|
||||
return c.json({ error: 'Invalid pubkey.' }, 400)
|
||||
}
|
||||
|
||||
if (!name || typeof name !== 'string' || name.length < 2 || name.length > 12) {
|
||||
return c.json({ error: 'Name must be 2-12 characters.' }, 400)
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
||||
return c.json({ error: 'Name must be alphanumeric, hyphens, or underscores.' }, 400)
|
||||
const parsed = registerHumanSchema.safeParse(await c.req.json().catch(() => ({})))
|
||||
if (!parsed.success) {
|
||||
return c.json({ error: formatZodError(parsed.error, {
|
||||
pubkey: 'Invalid pubkey.',
|
||||
name: 'Name must be 2-12 alphanumeric characters, hyphens, or underscores.',
|
||||
}, 'Invalid registration data.') }, 400)
|
||||
}
|
||||
const { pubkey, name, profilePicUrl, avatarSeed } = parsed.data
|
||||
|
||||
const normalizedName = name.toLowerCase()
|
||||
|
||||
@@ -306,12 +295,11 @@ authRouter.post('/register-human', rateLimit(600_000, 10), async (c) => {
|
||||
|
||||
// Update bot webhook and/or customization (requires pubkey match)
|
||||
authRouter.post('/update', rateLimit(60_000, 10), async (c) => {
|
||||
const body = await c.req.json()
|
||||
const { pubkey, webhookUrl, profilePicUrl, customization: rawCustomization } = body
|
||||
|
||||
if (!pubkey || typeof pubkey !== 'string' || pubkey.length !== 64) {
|
||||
const parsed = updateBotSchema.safeParse(await c.req.json().catch(() => ({})))
|
||||
if (!parsed.success) {
|
||||
return c.json({ error: 'Invalid pubkey.' }, 400)
|
||||
}
|
||||
const { pubkey, webhookUrl, profilePicUrl, customization: rawCustomization } = parsed.data
|
||||
|
||||
const rows = await db.select({ id: schema.bots.id, customization: schema.bots.customization })
|
||||
.from(schema.bots)
|
||||
|
||||
Reference in New Issue
Block a user