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
54
views_reader.py
Normal file
54
views_reader.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from fastapi import APIRouter, Request
|
||||
|
||||
from .crud import get_active_grant, get_door_by_id_or_name, record_log
|
||||
from .models import CheckRequest
|
||||
from .services import trigger_unlock, verify_card
|
||||
|
||||
access_reader_router = APIRouter()
|
||||
|
||||
|
||||
def _client_ip(request: Request) -> str:
|
||||
if "x-real-ip" in request.headers:
|
||||
return request.headers["x-real-ip"]
|
||||
if "x-forwarded-for" in request.headers:
|
||||
return request.headers["x-forwarded-for"]
|
||||
return request.client.host if request.client else ""
|
||||
|
||||
|
||||
# The door reader (Pi + PN532) calls this. Fails closed at every step.
|
||||
# POST /access/api/v1/check
|
||||
# X-Controller-Token: <door.controller_token>
|
||||
# { "doorId": "...", "external_id": "...", "p": "...", "c": "..." }
|
||||
@access_reader_router.post("/api/v1/check")
|
||||
async def check(data: CheckRequest, request: Request):
|
||||
ip = _client_ip(request)
|
||||
door = await get_door_by_id_or_name(data.doorId)
|
||||
|
||||
# Unknown/disabled door, or wrong controller token → deny (no card read logged
|
||||
# against a real door we can't identify; log against the requested id).
|
||||
if not door or not door.enabled:
|
||||
await record_log(data.doorId, data.external_id, "deny", "door_unknown", ip)
|
||||
return {"allow": False, "reason": "door_unknown"}
|
||||
|
||||
token = request.headers.get("x-controller-token", "")
|
||||
if not token or token != door.controller_token:
|
||||
await record_log(door.id, data.external_id, "deny", "bad_controller_token", ip)
|
||||
return {"allow": False, "reason": "bad_controller_token"}
|
||||
|
||||
# 1) Authenticate the card (NTAG424 SUN via boltcards).
|
||||
if not await verify_card(door, data.external_id, data.p, data.c):
|
||||
await record_log(door.id, data.external_id, "deny", "card_invalid", ip)
|
||||
return {"allow": False, "reason": "card_invalid"}
|
||||
|
||||
# 2) Authorize: does this card have an active grant on this door?
|
||||
grant = await get_active_grant(door.id, data.external_id)
|
||||
if not grant:
|
||||
await record_log(door.id, data.external_id, "deny", "not_authorized", ip)
|
||||
return {"allow": False, "reason": "not_authorized"}
|
||||
|
||||
# 3) Actuate: fire the Home Assistant unlock webhook.
|
||||
unlocked = await trigger_unlock(door, data.external_id)
|
||||
decision = "allow" if unlocked else "deny"
|
||||
reason = "unlocked" if unlocked else "unlock_failed"
|
||||
await record_log(door.id, data.external_id, decision, reason, ip)
|
||||
return {"allow": unlocked, "reason": reason}
|
||||
Loading…
Add table
Add a link
Reference in a new issue