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.
26 lines
843 B
TypeScript
26 lines
843 B
TypeScript
import { requireDoorManagerPage } from '@/lib/door-guard'
|
|
import { listApiButtons, listSecretTitles, listApiCallLog } from '@/lib/actions'
|
|
import { PERMISSIONS } from '@/lib/permissions-catalog'
|
|
import { ApiButtonsClient } from './api-buttons-client'
|
|
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
export default async function ApiButtonsPage() {
|
|
await requireDoorManagerPage()
|
|
|
|
const [buttons, secrets, log] = await Promise.all([
|
|
listApiButtons(),
|
|
listSecretTitles(),
|
|
listApiCallLog(),
|
|
])
|
|
|
|
return (
|
|
<ApiButtonsClient
|
|
initial={'data' in buttons ? (buttons.data as unknown[]) : []}
|
|
secrets={'data' in secrets ? (secrets.data as { id: string; title: string }[]) : []}
|
|
log={'data' in log ? (log.data as unknown[]) : []}
|
|
permissions={PERMISSIONS.map(p => ({ code: p.code, label: p.label }))}
|
|
/>
|
|
)
|
|
}
|