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:
@@ -4,8 +4,9 @@ import { PageHeader, PageTitle } from '@/components/ui/page-title'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty'
|
||||
import { DoorClosed } from 'lucide-react'
|
||||
import { listSelfEntryDoors, getMyCards, listMyDoorActivity } from '@/lib/actions'
|
||||
import { listSelfEntryDoors, getMyCards, listMyDoorActivity, listInvokableButtons } from '@/lib/actions'
|
||||
import { DoorSelfEntry } from '../dashboard/door-self-entry'
|
||||
import { ApiButtonsInvoke } from './api-buttons-invoke'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -27,16 +28,18 @@ export default async function DoorsPage() {
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
if (!user) redirect('/login')
|
||||
|
||||
const [doorsRes, cardsRes, activityRes] = await Promise.all([
|
||||
const [doorsRes, cardsRes, activityRes, buttonsRes] = await Promise.all([
|
||||
listSelfEntryDoors(),
|
||||
getMyCards(),
|
||||
listMyDoorActivity(),
|
||||
listInvokableButtons(),
|
||||
])
|
||||
const doors = ('data' in doorsRes ? doorsRes.data : []) as { id: string; name: string }[]
|
||||
const cards = ('data' in cardsRes ? cardsRes.data : []) as Card[]
|
||||
const activity = ('data' in activityRes ? activityRes.data : []) as Activity[]
|
||||
const buttons = ('data' in buttonsRes ? buttonsRes.data : []) as { id: string; label: string; group: string; confirm: boolean }[]
|
||||
|
||||
const nothing = doors.length === 0 && cards.length === 0 && activity.length === 0
|
||||
const nothing = doors.length === 0 && cards.length === 0 && activity.length === 0 && buttons.length === 0
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -69,6 +72,16 @@ export default async function DoorsPage() {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{buttons.length > 0 && (
|
||||
<section>
|
||||
<h2 className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground mb-3">Actions</h2>
|
||||
<ApiButtonsInvoke buttons={buttons} />
|
||||
<p className="font-mono text-[10px] text-muted-foreground mt-2">
|
||||
Each action fires a configured request. Every press is logged.
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{cards.length > 0 && (
|
||||
<section>
|
||||
<h2 className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground mb-3">My access cards</h2>
|
||||
|
||||
Reference in New Issue
Block a user