export const runtime = "nodejs"; import { NextResponse } from "next/server"; import { z } from "zod"; import { requireSession } from "@/lib/auth/require"; import { listTickets, createManualTicket } from "@/lib/tickets/service"; import { saveAttachment, AttachmentTooLargeError, MAX_ATTACHMENT_BYTES } from "@/lib/attachments/storage"; export async function GET(request: Request) { const { session, response } = await requireSession(); if (!session) return response; const query = new URL(request.url).searchParams.get("q") ?? undefined; const tickets = await listTickets(query, { id: session.user.id, role: session.user.role }); return NextResponse.json({ tickets }); } const createSchema = z.object({ subject: z.string().min(1).max(200), body: z.string().min(1).max(10_000), customerName: z.string().min(1).max(200), customerEmail: z.string().email().optional().or(z.literal("")), }); // multipart/form-data rather than JSON — the modal supports an optional // attachment (incl. Ctrl+V screenshot paste), same as replying on an // existing ticket. export async function POST(request: Request) { const { session, response } = await requireSession(); if (!session) return response; const formData = await request.formData().catch(() => null); if (!formData) return NextResponse.json({ error: "Invalid input" }, { status: 400 }); const parsed = createSchema.safeParse({ subject: formData.get("subject"), body: formData.get("body"), customerName: formData.get("customerName"), customerEmail: formData.get("customerEmail") ?? undefined, }); if (!parsed.success) { return NextResponse.json({ error: "Invalid input" }, { status: 400 }); } const file = formData.get("file"); let attachment: { filename: string; mimeType: string; sizeBytes: number; storageKey: string } | undefined; if (file instanceof File) { if (file.size > MAX_ATTACHMENT_BYTES) { return NextResponse.json({ error: "File too large" }, { status: 413 }); } const buffer = Buffer.from(await file.arrayBuffer()); try { const saved = await saveAttachment(buffer); attachment = { filename: file.name || "file", mimeType: file.type || "application/octet-stream", sizeBytes: saved.sizeBytes, storageKey: saved.storageKey, }; } catch (err) { if (err instanceof AttachmentTooLargeError) { return NextResponse.json({ error: "File too large" }, { status: 413 }); } throw err; } } const result = await createManualTicket({ ...parsed.data, customerEmail: parsed.data.customerEmail || undefined, attachment, }); return NextResponse.json(result, { status: 201 }); }