'use client' import { useState } from 'react' import { toast } from 'sonner' import { Zap, Plus } from 'lucide-react' import { PageHeader, PageTitle } from '@/components/ui/page-title' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty' import { useConfirm } from '@/components/ui/confirm' import { createApiButton, updateApiButton, deleteApiButton } from '@/lib/actions' type Btn = { id: string label: string button_group: string sort_order: number method: string base_url: string pinned_host: string url_template: string | null headers: Record | null body_template: string | null auth_mode: string auth_param: string | null secret_ref: string | null required_permission: string confirm: boolean is_enabled: boolean } type LogRow = { id: string; action: string; success: boolean; detail: string | null; occurred_at: string } type Secret = { id: string; title: string } type Perm = { code: string; label: string } const input = 'w-full bg-background border border-border text-foreground font-sans text-sm rounded px-3 py-2 focus:outline-none focus:border-primary' const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] const AUTH_MODES = ['none', 'query', 'header', 'bearer'] function emptyDraft() { return { label: '', button_group: 'General', method: 'POST', base_url: '', pinned_host: '', url_template: '', headersText: '', body_template: '', auth_mode: 'none', auth_param: '', secret_ref: '', required_permission: 'apicall.invoke', confirm: true, } } // Parse "Key: Value" lines into a header map (blank lines ignored). function parseHeaders(text: string): Record { const out: Record = {} for (const line of text.split('\n')) { const i = line.indexOf(':') if (i <= 0) continue const k = line.slice(0, i).trim() const v = line.slice(i + 1).trim() if (k) out[k] = v } return out } export function ApiButtonsClient({ initial, secrets, log, permissions, }: { initial: unknown[] secrets: Secret[] log: unknown[] permissions: Perm[] }) { const confirm = useConfirm() const [buttons, setButtons] = useState(initial as Btn[]) const [showNew, setShowNew] = useState(false) const [d, setD] = useState(emptyDraft()) const [busy, setBusy] = useState(false) const rows = log as LogRow[] function doorTemplate() { setShowNew(true) setD({ ...emptyDraft(), label: 'Open door', button_group: 'Door', method: 'GET', url_template: '?o1', auth_mode: 'query', auth_param: 'e', required_permission: 'door.operate', confirm: true, }) toast.message('Door template loaded', { description: 'Set the base URL, pinned host, and password secret, then create.' }) } async function onCreate(e: React.FormEvent) { e.preventDefault() if (!d.label.trim() || !d.base_url.trim() || !d.pinned_host.trim()) { return toast.error('Label, base URL and pinned host are required') } const headers = parseHeaders(d.headersText) setBusy(true) const res = await createApiButton({ label: d.label.trim(), button_group: d.button_group.trim() || 'General', method: d.method, base_url: d.base_url.trim(), pinned_host: d.pinned_host.trim(), url_template: d.url_template.trim() || null, headers, body_template: d.body_template.trim() || null, auth_mode: d.auth_mode, auth_param: d.auth_param.trim() || null, secret_ref: d.secret_ref || null, required_permission: d.required_permission, confirm: d.confirm, }) setBusy(false) if ('error' in res && res.error) return toast.error(res.error) const id = (res as { data: { id: string } }).data.id setButtons(prev => [ { id, label: d.label.trim(), button_group: d.button_group.trim() || 'General', sort_order: 0, method: d.method, base_url: d.base_url.trim(), pinned_host: d.pinned_host.trim(), url_template: d.url_template.trim() || null, headers, body_template: d.body_template.trim() || null, auth_mode: d.auth_mode, auth_param: d.auth_param.trim() || null, secret_ref: d.secret_ref || null, required_permission: d.required_permission, confirm: d.confirm, is_enabled: true, }, ...prev, ]) setD(emptyDraft()) setShowNew(false) toast.success('Button created') } async function onToggleEnabled(b: Btn) { const res = await updateApiButton({ buttonId: b.id, is_enabled: !b.is_enabled }) if ('error' in res && res.error) return toast.error(res.error) setButtons(prev => prev.map(x => (x.id === b.id ? { ...x, is_enabled: !x.is_enabled } : x))) } async function onDelete(b: Btn) { const ok = await confirm({ title: 'Delete button', description: `"${b.label}" will be removed. Existing API-call log entries are kept.`, confirmText: 'Delete', destructive: true, }) if (!ok) return const res = await deleteApiButton({ buttonId: b.id }) if ('error' in res && res.error) return toast.error(res.error) setButtons(prev => prev.filter(x => x.id !== b.id)) toast.success('Deleted') } const permLabel = (code: string) => permissions.find(p => p.code === code)?.label ?? code return ( <> API buttons

A button fires a configured HTTP request when an authorized member presses it. The request runs through the same hardened egress as door control: the server only ever calls the exact pinned host (any public or private host you set, except cloud-metadata/link-local), never follows redirects, caps time and response size, and records every press in the API-call log with secrets redacted. The secret is read from your encrypted Secrets vault and injected server-side, never stored here. Each button is gated by a permission you choose; a member sees only the buttons they may press.

{showNew && (
setD({ ...d, label: e.target.value })} /> setD({ ...d, button_group: e.target.value })} /> setD({ ...d, base_url: e.target.value })} /> setD({ ...d, pinned_host: e.target.value })} /> setD({ ...d, url_template: e.target.value })} /> {(d.auth_mode === 'query' || d.auth_mode === 'header') && ( setD({ ...d, auth_param: e.target.value })} /> )} {d.auth_mode !== 'none' && ( )}