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>
54 lines
2.4 KiB
Python
54 lines
2.4 KiB
Python
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}
|