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
70
README.md
Normal file
70
README.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Access — LNbits extension
|
||||
|
||||
NFC **door access** for LNbits. A tapped Bolt Card (NTAG424) is authenticated
|
||||
via the `boltcards` SUN check, authorized against per-door **grants**, and — if
|
||||
allowed — the door's local **Home Assistant** webhook is fired to unlock a
|
||||
Z-Wave lock. Every tap is logged.
|
||||
|
||||
Runs on the **on-prem LNbits** on the door's LAN, so the unlock path stays local
|
||||
and the door does not depend on the internet.
|
||||
|
||||
## Flow
|
||||
|
||||
```text
|
||||
Pi + PN532 reader
|
||||
→ POST /access/api/v1/check (X-Controller-Token: <door token>)
|
||||
{ "doorId": "...", "external_id": "...", "p": "...", "c": "..." }
|
||||
1. authenticate → boltcards /verify (valid, non-replayed SUN?)
|
||||
2. authorize → active grant for this external_id on this door?
|
||||
3. actuate → POST door.ha_webhook_url (→ lock.unlock → Z-Wave)
|
||||
4. log → access.logs (allow/deny + reason)
|
||||
→ { "allow": true|false, "reason": "..." }
|
||||
```
|
||||
|
||||
The reader never unlocks directly; this extension owns the allow/deny decision
|
||||
and the Home Assistant call. Fails **closed** at every step.
|
||||
|
||||
## Data model
|
||||
|
||||
- **doors** — a resource: `name`, `controller_token` (reader secret),
|
||||
`ha_webhook_url` (local HA webhook), `boltcards_base_url` (blank = this
|
||||
instance), `unlock_timeout_ms`, `enabled`.
|
||||
- **grants** — `external_id → door` permission, `enabled`/`expires_at`.
|
||||
- **logs** — every allow/deny with a reason.
|
||||
|
||||
## Admin API (wallet admin key)
|
||||
|
||||
- `GET/POST /access/api/v1/doors`, `PUT/DELETE /access/api/v1/doors/{id}`
|
||||
- `GET/POST /access/api/v1/grants`, `DELETE /access/api/v1/grants/{id}`
|
||||
- `GET /access/api/v1/logs`
|
||||
|
||||
## Reader API (controller token)
|
||||
|
||||
```http
|
||||
POST /access/api/v1/check
|
||||
X-Controller-Token: <door.controller_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{ "doorId": "front-door", "external_id": "abc123", "p": "<32-hex>", "c": "<16-hex>" }
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install on the on-prem LNbits (same instance as `boltcards`).
|
||||
2. Create a **door**, set its Home Assistant webhook URL, copy its
|
||||
**controller token** to the reader config.
|
||||
3. Issue Bolt Cards in `boltcards` as usual; add a **grant** (the card's
|
||||
`external_id` → the door).
|
||||
4. Point the door reader (Pi) at `/access/api/v1/check` with the token.
|
||||
|
||||
## Notes
|
||||
|
||||
- Card authentication delegates to `boltcards` over loopback HTTP via its
|
||||
**`/api/v1/verify/{external_id}`** endpoint (aiolabs fork ≥ **1.1.1-aio.2**) —
|
||||
a side-effect-light SUN check that returns `{"authenticated": true, …}`
|
||||
without `/scan`'s spend semantics (no withdrawRequest, no daily-limit, no
|
||||
"hit"), while still advancing the SUN counter for replay protection.
|
||||
- Requires boltcards **1.1.1-aio.2+** installed on the same LNbits.
|
||||
- Empty `ha_webhook_url` = authorize + log only (bring-up mode; no unlock).
|
||||
- Keep this on the LAN; use a strong per-door controller token; never expose
|
||||
Home Assistant publicly.
|
||||
Loading…
Add table
Add a link
Reference in a new issue