"use client" import { useState } from "react" import { useRouter } from "next/navigation" import { toast } from "sonner" import { Check, Copy, Eye, EyeOff, Loader2, Plus, Power, RefreshCw, Send, Trash2, Webhook, } from "lucide-react" import { WEBHOOK_EVENTS } from "@/lib/webhooks/events" import { createWebhookEndpoint, deleteWebhookEndpoint, rotateWebhookSecret, sendTestWebhook, updateWebhookEndpoint, type WebhookEndpointDTO, } from "@/app/actions/webhooks" const inputClass = "w-full rounded-lg border border-white/10 bg-white/5 px-4 py-2.5 text-sm text-white placeholder-white/30 outline-none ring-indigo-500 transition focus:border-indigo-500/50 focus:ring-1" function formatDate(value: string | null): string { if (!value) return "Never" const d = new Date(value) return Number.isNaN(d.getTime()) ? "—" : d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) } function eventsSummary(events: string[]): string { if (events.length === 0) return "All events" if (events.length === 1) return events[0] return `${events.length} events` } function SecretField({ secret }: { secret: string }) { const [shown, setShown] = useState(false) const [copied, setCopied] = useState(false) async function copy() { try { await navigator.clipboard.writeText(secret) setCopied(true) toast.success("Signing secret copied") setTimeout(() => setCopied(false), 2000) } catch { toast.error("Copy failed — reveal and copy manually") } } return (
{shown ? secret : "whsec_" + "•".repeat(24)}
) } export function WebhookManager({ initialEndpoints }: { initialEndpoints: WebhookEndpointDTO[] }) { const router = useRouter() const [url, setUrl] = useState("") const [description, setDescription] = useState("") const [allEvents, setAllEvents] = useState(true) const [selected, setSelected] = useState>(new Set()) const [creating, setCreating] = useState(false) const [busyId, setBusyId] = useState(null) function toggleEvent(id: string) { setSelected((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } async function handleCreate(e: React.FormEvent) { e.preventDefault() const trimmed = url.trim() if (!trimmed) return setCreating(true) try { await createWebhookEndpoint({ url: trimmed, description: description.trim() || undefined, events: allEvents ? [] : Array.from(selected), }) setUrl("") setDescription("") setAllEvents(true) setSelected(new Set()) toast.success("Webhook endpoint created") router.refresh() } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to create webhook") } finally { setCreating(false) } } async function withBusy(id: string, fn: () => Promise) { setBusyId(id) try { await fn() } finally { setBusyId(null) } } async function handleTest(ep: WebhookEndpointDTO) { await withBusy(ep.id, async () => { try { const res = await sendTestWebhook(ep.id) if (res.ok) toast.success(`Test delivered (HTTP ${res.responseStatus})`) else toast.error(`Test failed: ${res.error ?? "no response"}`) router.refresh() } catch (err) { toast.error(err instanceof Error ? err.message : "Test failed") } }) } async function handleToggle(ep: WebhookEndpointDTO) { await withBusy(ep.id, async () => { try { await updateWebhookEndpoint(ep.id, { status: ep.status === "active" ? "disabled" : "active" }) toast.success(ep.status === "active" ? "Webhook disabled" : "Webhook enabled") router.refresh() } catch (err) { toast.error(err instanceof Error ? err.message : "Update failed") } }) } async function handleRotate(ep: WebhookEndpointDTO) { if (!confirm("Rotate the signing secret? The current secret stops working immediately.")) return await withBusy(ep.id, async () => { try { await rotateWebhookSecret(ep.id) toast.success("Signing secret rotated") router.refresh() } catch (err) { toast.error(err instanceof Error ? err.message : "Rotate failed") } }) } async function handleDelete(ep: WebhookEndpointDTO) { if (!confirm(`Delete webhook for ${ep.url}? This cannot be undone.`)) return await withBusy(ep.id, async () => { try { await deleteWebhookEndpoint(ep.id) toast.success("Webhook deleted") router.refresh() } catch (err) { toast.error(err instanceof Error ? err.message : "Delete failed") } }) } return (
{/* Create form */}

Add an endpoint

We'll POST a signed JSON payload to this URL whenever a subscribed event happens.

setUrl(e.target.value)} placeholder="https://hooks.zapier.com/hooks/catch/…" className={inputClass} /> setDescription(e.target.value)} placeholder="Description (optional) — e.g. Zapier: new tenant → Slack" className={inputClass} />
{!allEvents && (
{WEBHOOK_EVENTS.map((ev) => ( ))}
)}
{/* Endpoints list */}

Endpoints ({initialEndpoints.length})

{initialEndpoints.length === 0 ? (
No endpoints yet. Add one above to start receiving events.
) : (
    {initialEndpoints.map((ep) => { const busy = busyId === ep.id const disabled = ep.status === "disabled" return (
  • {ep.url}

    {disabled && ( Disabled )} {ep.source === "zapier" && ( Zapier )}
    {eventsSummary(ep.events)} Last success {formatDate(ep.last_success_at)} {ep.failure_count > 0 && ( {ep.failure_count} recent failure{ep.failure_count > 1 ? "s" : ""} )}
    {ep.description && (

    {ep.description}

    )}
  • ) })}
)}
) }