import { NextRequest, NextResponse } from "next/server"; import crypto from "node:crypto"; import { z } from "zod"; import { db, webhooks, eq } from "@epicure/db"; import { requireSession } from "@/lib/api-auth"; import { validateWebhookUrl } from "@/lib/validate-webhook-url"; const VALID_EVENTS = ["recipe.created", "recipe.updated", "recipe.published", "recipe.deleted"] as const; const CreateWebhookBody = z.object({ url: z.string().min(1).max(2048), events: z.array(z.enum(VALID_EVENTS)).default([]), }); const UpdateWebhookBody = z.object({ url: z.string().min(1).max(2048).optional(), events: z.array(z.enum(VALID_EVENTS)).optional(), active: z.boolean().optional(), }); export async function GET() { const { session, response } = await requireSession(); if (response) return response; const rows = await db .select({ id: webhooks.id, userId: webhooks.userId, url: webhooks.url, events: webhooks.events, active: webhooks.active, createdAt: webhooks.createdAt, }) .from(webhooks) .where(eq(webhooks.userId, session!.user.id)); return NextResponse.json(rows); } export async function POST(req: NextRequest) { const { session, response } = await requireSession(); if (response) return response; const body = await req.json() as unknown; const parsed = CreateWebhookBody.safeParse(body); if (!parsed.success) { return NextResponse.json( { error: "Validation error", issues: parsed.error.issues }, { status: 400 } ); } // Validate URL — enforce https/http only and block SSRF targets const ssrfError = await validateWebhookUrl(parsed.data.url); if (ssrfError) { return NextResponse.json({ error: ssrfError }, { status: 400 }); } const secret = crypto.randomBytes(32).toString("hex"); const id = crypto.randomUUID(); const now = new Date(); await db.insert(webhooks).values({ id, userId: session!.user.id, url: parsed.data.url, events: parsed.data.events, secret, active: true, createdAt: now, }); return NextResponse.json( { id, userId: session!.user.id, url: parsed.data.url, events: parsed.data.events, secret, active: true, createdAt: now.toISOString(), }, { status: 201 } ); }