feat: access extension — NFC door access via boltcards SUN + Home Assistant
Promoted from the door-portal scratch repo to its own repo for install via the aiolabs catalog. Authenticates a tapped Bolt Card via boltcards /verify, authorizes against per-door grants, and fires the door's local Home Assistant webhook to unlock a Z-Wave lock. Fails closed; logs every attempt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
40564f06ec
22 changed files with 986 additions and 0 deletions
75
models.py
Normal file
75
models.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Door(BaseModel):
|
||||
"""A physical door/resource that a tap can open."""
|
||||
|
||||
id: str
|
||||
wallet: str
|
||||
name: str
|
||||
# Secret the reader (Pi) must present in X-Controller-Token to hit /check.
|
||||
controller_token: str
|
||||
# Local Home Assistant webhook that fires lock.unlock (LAN-only). When empty,
|
||||
# /check still authenticates + authorizes and logs, but performs no unlock
|
||||
# (useful for bring-up / bench testing before the lock is wired).
|
||||
ha_webhook_url: str
|
||||
# boltcards API base used to SUN-verify the tapped card, e.g.
|
||||
# http://localhost:5000/boltcards/api/v1 (same on-prem LNbits). Falls back to
|
||||
# the instance base URL when empty (see services.verify_card).
|
||||
boltcards_base_url: str
|
||||
unlock_timeout_ms: int
|
||||
enabled: bool
|
||||
time: datetime
|
||||
|
||||
|
||||
class CreateDoor(BaseModel):
|
||||
name: str
|
||||
ha_webhook_url: str = ""
|
||||
boltcards_base_url: str = ""
|
||||
# Generated server-side when left blank.
|
||||
controller_token: str = ""
|
||||
unlock_timeout_ms: int = 2500
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
class Grant(BaseModel):
|
||||
"""A card's permission to open a specific door."""
|
||||
|
||||
id: str
|
||||
door_id: str
|
||||
# The Bolt Card's boltcards `external_id` (the authenticated identity).
|
||||
external_id: str
|
||||
label: str
|
||||
enabled: bool
|
||||
# Optional hard expiry; null = no expiry.
|
||||
expires_at: datetime | None
|
||||
time: datetime
|
||||
|
||||
|
||||
class CreateGrant(BaseModel):
|
||||
door_id: str
|
||||
external_id: str
|
||||
label: str = ""
|
||||
expires_at: datetime | None = None
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
class AccessLog(BaseModel):
|
||||
id: str
|
||||
door_id: str
|
||||
external_id: str
|
||||
decision: str # "allow" | "deny"
|
||||
reason: str
|
||||
ip: str
|
||||
time: datetime
|
||||
|
||||
|
||||
class CheckRequest(BaseModel):
|
||||
"""What the door reader (Pi) POSTs to /access/api/v1/check."""
|
||||
|
||||
doorId: str
|
||||
external_id: str
|
||||
p: str
|
||||
c: str
|
||||
Loading…
Add table
Add a link
Reference in a new issue