diff --git a/src/app/dashboard/integrations/webhooks/page.tsx b/src/app/dashboard/integrations/webhooks/page.tsx new file mode 100644 index 0000000..04a8a16 --- /dev/null +++ b/src/app/dashboard/integrations/webhooks/page.tsx @@ -0,0 +1,127 @@ +'use client'; + +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import Link from 'next/link'; +import { apiFetch } from '../../../../lib/api'; +import { useSession } from '../../../../components/session-provider'; + +interface DataSource { id: string; name: string; type: string; status: string; config: Record | null; } + +const WEBHOOK_EVENTS = [ + 'contact.created', 'contact.updated', 'document.created', + 'task.completed', 'decision.created', 'approval.required', + 'anomaly.detected', 'contract.expiring', +]; + +export default function WebhooksPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + const [showCreate, setShowCreate] = useState(false); + const [form, setForm] = useState({ name: '', url: '', events: [] as string[], secret: '' }); + + const { data: sources = [], isLoading } = useQuery({ + queryKey: ['webhooks-sources', tenantId], + queryFn: () => apiFetch('/v1/data-sources', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + + const webhookSources = sources.filter((s) => s.type === 'webhook' || s.config?.type === 'webhook'); + + const createMut = useMutation({ + mutationFn: () => apiFetch('/v1/data-sources', { tenantId, method: 'POST', body: { + name: form.name, type: 'webhook', + config: { url: form.url, events: form.events, secret: form.secret || undefined }, + }}), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['webhooks-sources', tenantId] }); setShowCreate(false); setForm({ name: '', url: '', events: [], secret: '' }); }, + }); + + function toggleEvent(e: string) { + setForm((p) => ({ ...p, events: p.events.includes(e) ? p.events.filter((x) => x !== e) : [...p.events, e] })); + } + + return ( +
+
+
+

Webhooks

+

+ {isLoading ? 'Se încarcă…' : `${webhookSources.length} webhook-uri configurate`} +

+
+ +
+ + {showCreate && ( +
+

Webhook nou

+
+ setForm((p) => ({ ...p, name: e.target.value }))} + className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + setForm((p) => ({ ...p, url: e.target.value }))} + className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + setForm((p) => ({ ...p, secret: e.target.value }))} + className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring font-mono text-xs" /> +
+
+

Evenimente

+
+ {WEBHOOK_EVENTS.map((ev) => ( + + ))} +
+
+
+ + +
+
+ )} + +
+

Securitate webhook

+
+

• Toate webhook-urile sunt semnate HMAC-SHA256 cu secretul configurat.

+

• Header-ul X-CEO-OS-Signature conține semnătura.

+

• Timestamp-ul este inclus pentru protecție împotriva replay attacks.

+

• Timeout de livrare: 10s. Retry: 3 încercări cu backoff exponențial.

+
+
+ + {isLoading ? ( +
Se încarcă…
+ ) : webhookSources.length === 0 ? ( +
+

🔗

+

Niciun webhook configurat.

+
+ ) : ( +
+ {webhookSources.map((s) => ( +
+
+

{s.name}

+

{(s.config as Record)?.url ?? '—'}

+
+ + {s.status} + +
+ ))} +
+ )} + + ← Toate integrările +
+ ); +}