feat: Zod validation on all API inputs (PRD §5.2)
26 files with Zod schemas, 27 safeParse calls. All POST/PATCH handlers now validate input: - Missing required fields → 400 with structured error - Invalid types (string vs number) → 400 - Out-of-range values (probability >100, negative amounts) → 400 - Valid data still works (behavior preserved) Pattern: schema.safeParse(await req.json()) → 400 on failure
This commit is contained in:
1 parent
3d56770669
commit
2614ce4e3d
25 files changed
+316
-116
No files matched your search
@@ -1,34 +1,34 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const createDealSchema = z.object({
|
||||
companyId: z.string().min(1),
|
||||
title: z.string().min(1).max(200),
|
||||
valueCents: z.number().int().min(0),
|
||||
stage: z.string().optional().default('lead'),
|
||||
probability: z.number().int().min(0).max(100).optional().default(10),
|
||||
expectedClose: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
// POST /api/aos/crm/deals — create a new deal in the pipeline
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = (await req.json()) as {
|
||||
companyId: string
|
||||
title: string
|
||||
valueCents: number
|
||||
stage?: string
|
||||
probability?: number
|
||||
expectedClose?: string | null
|
||||
}
|
||||
|
||||
if (!body.companyId) {
|
||||
return NextResponse.json({ error: 'companyId required' }, { status: 400 })
|
||||
}
|
||||
if (!body.title?.trim()) {
|
||||
return NextResponse.json({ error: 'title required' }, { status: 400 })
|
||||
const parsed = createDealSchema.safeParse(await req.json())
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: 'validation failed', details: parsed.error.flatten() }, { status: 400 })
|
||||
}
|
||||
const body = parsed.data
|
||||
|
||||
const deal = await db.deal.create({
|
||||
data: {
|
||||
companyId: body.companyId,
|
||||
title: body.title.trim(),
|
||||
valueCents: Math.max(0, Math.round(body.valueCents)),
|
||||
stage: body.stage ?? 'lead',
|
||||
probability: body.probability ?? 10,
|
||||
stage: body.stage,
|
||||
probability: body.probability,
|
||||
expectedClose: body.expectedClose ? new Date(body.expectedClose) : null,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const maxDuration = 30
|
||||
|
||||
const importSchema = z.object({
|
||||
csv: z.string().min(1),
|
||||
mapping: z.record(z.string(), z.number()),
|
||||
})
|
||||
|
||||
// POST /api/aos/crm/import
|
||||
// Import companies + persons from a CSV string.
|
||||
// Body: { csv: string, mapping: { name: number, vatId: number, ... } }
|
||||
@@ -13,13 +19,14 @@ export const maxDuration = 30
|
||||
// Duplicate detection: skips companies with the same name or website.
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { csv, mapping } = (await req.json()) as {
|
||||
csv: string
|
||||
mapping: Record<string, number>
|
||||
const parsed = importSchema.safeParse(await req.json())
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: 'validation failed', details: parsed.error.flatten() }, { status: 400 })
|
||||
}
|
||||
const { csv, mapping } = parsed.data
|
||||
|
||||
if (!csv?.trim()) return NextResponse.json({ error: 'csv data required' }, { status: 400 })
|
||||
if (!mapping?.name && mapping.name !== 0) return NextResponse.json({ error: 'mapping.name required' }, { status: 400 })
|
||||
if (!csv.trim()) return NextResponse.json({ error: 'csv data required' }, { status: 400 })
|
||||
if (mapping.name === undefined) return NextResponse.json({ error: 'mapping.name required' }, { status: 400 })
|
||||
|
||||
// Parse CSV (simple RFC 4180 parser — handles quoted fields with commas)
|
||||
const lines = parseCSV(csv)
|
||||
|
||||
Reference in new issue
Block a user