← back to Stayclaim

src/app/api/submit/route.ts

56 lines

import { NextResponse } from 'next/server';
import { z } from 'zod';
import { pool } from '@/lib/db';
import { rateLimit } from '@/lib/rate-limit';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

const Body = z.object({
  address_line1: z.string().min(3).max(255),
  city: z.string().min(2).max(120),
  state: z.string().min(2).max(2).transform(s => s.toUpperCase()).pipe(z.string().regex(/^[A-Z]{2}$/)),
  postal_code: z.string().max(12).regex(/^[A-Z0-9 \-]{3,12}$/i).optional().or(z.literal('')),
  notes: z.string().max(2000).optional().or(z.literal('')),
  contact_email: z.string().email().max(254),
});

export async function POST(req: Request) {
  const rl = await rateLimit('submit');
  if (!rl.ok) {
    return NextResponse.json(
      { error: 'rate_limited' },
      { status: 429, headers: { 'Retry-After': String(rl.retryAfter ?? 600) } },
    );
  }

  let parsed;
  try {
    parsed = Body.parse(await req.json());
  } catch (e) {
    const detail = e instanceof z.ZodError ? e.flatten() : undefined;
    return NextResponse.json({ error: 'invalid body', detail }, { status: 400 });
  }
  try {
    const r = await pool.query(
      `INSERT INTO address_submission
       (address_line1, city, state, postal_code, notes, contact_email)
       VALUES ($1,$2,$3,$4,$5,$6)
       RETURNING id, status, submitted_at`,
      [
        parsed.address_line1,
        parsed.city,
        parsed.state,
        parsed.postal_code || null,
        parsed.notes || null,
        parsed.contact_email,
      ]
    );
    const { id, status, submitted_at } = r.rows[0];
    return NextResponse.json({ id, status, submitted_at }, { status: 201 });
  } catch (err) {
    console.error('[submit] db insert failed', err);
    return NextResponse.json({ error: 'internal' }, { status: 500 });
  }
}