Door epic phases 4-5: inbound access-log ingest + universal API-call buttons
Phase 4 (migration 053): pull entry/denied events into door_access_log and match each card to a member, via two transports sharing one ingest core. Poll: a CRON_SECRET-guarded /api/cron/door-ingest reads each opted-in native-HeatSync connection's ?z log through the existing hardened executor and parses it (firmware-characterized, best-effort ring buffer). Webhook: /api/door/inbound/[connection] accepts pushed event JSON authenticated by a per-connection bearer secret from the vault. Both dedupe on (connection_id, dedupe_key) so re-polls and retries are no-ops. Adds an inbound config panel to /door/manage. Phase 5 (migration 054): admins define api_buttons that fire a configured HTTP call (full verbs + headers + body) when an authorized member presses them, through the same hardened egress (now shared between callDoor and the new callApi). Each button carries a required_permission (default the new apicall.invoke code); members see only buttons they may press. Builder at /door/buttons with a door-template preset; members press from /doors. Every press is recorded in api_call_log with secrets redacted. Both subsystems are inert until an admin enables inbound on a connection or creates a button. 624 unit + 50 integration tests; migrations applied top-to-bottom in schema.sql; types and docs updated.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useConfirm } from '@/components/ui/confirm'
|
||||
import { invokeApiButton } from '@/lib/actions'
|
||||
|
||||
type InvokeButton = { id: string; label: string; group: string; confirm: boolean }
|
||||
|
||||
export function ApiButtonsInvoke({ buttons }: { buttons: InvokeButton[] }) {
|
||||
const confirm = useConfirm()
|
||||
const [busy, setBusy] = useState<string | null>(null)
|
||||
|
||||
const groups = new Map<string, InvokeButton[]>()
|
||||
for (const b of buttons) {
|
||||
if (!groups.has(b.group)) groups.set(b.group, [])
|
||||
groups.get(b.group)!.push(b)
|
||||
}
|
||||
|
||||
async function press(b: InvokeButton) {
|
||||
if (b.confirm) {
|
||||
const ok = await confirm({
|
||||
title: `${b.label}?`,
|
||||
description: 'This fires a configured action. Continue?',
|
||||
confirmText: b.label,
|
||||
})
|
||||
if (!ok) return
|
||||
}
|
||||
setBusy(b.id)
|
||||
const res = await invokeApiButton({ buttonId: b.id })
|
||||
setBusy(null)
|
||||
if ('error' in res && res.error) return toast.error(res.error)
|
||||
toast.success(`${b.label} sent`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{Array.from(groups.entries()).map(([group, items]) => (
|
||||
<div key={group}>
|
||||
<p className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground/70 mb-2">{group}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{items.map(b => (
|
||||
<Button key={b.id} size="sm" disabled={busy === b.id} onClick={() => press(b)}>
|
||||
{b.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user