- Python 100%
| edge_auth | ||
| systemd | ||
| .gitignore | ||
| example.env | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
member_portal-edge_auth
Helper that interfaces between portal access lists and edge/access control hardware.
Requirements
- Linux (comms over socket)
- Python 3.12+
- Network access to a member portal instance
Configuration
An example .env is provided as example.env
| Variable | Required | Description |
|---|---|---|
EDGE_AUTH_API_KEY |
yes | Portal API key (X-API-Key) |
EDGE_AUTH_ACCESS_LIST |
yes | Access list short_name |
EDGE_AUTH_BASE_URL |
no | Default http://localhost:5000 |
EDGE_AUTH_SOCKET_PATH |
no | Default run/member-portal-edge-auth.sock (relative to cwd; parent directory is created) |
EDGE_AUTH_SOUND_CACHE_DIR |
no | Default data/sounds (relative to cwd) |
EDGE_AUTH_CACHE_FILE |
no | JSON file for last successful access list (default data/access_list_cache.json); loaded if startup refresh fails |
EDGE_AUTH_OUTBOX_FILE |
no | JSONL queue for failed portal writes (default data/pending_scans.jsonl) |
EDGE_AUTH_REPLAY_INTERVAL_SECONDS |
no | How often to retry the outbox (default 60) |
EDGE_AUTH_REVISION_POLL_SECONDS |
no | How often to poll GET .../revision (default 30) |
EDGE_AUTH_FULL_REFRESH_SECONDS |
no | Full list refresh interval (default 3600) |
EDGE_AUTH_DEVICE_ID |
no | Label for timeline / denial payloads (default edge) |
EDGE_AUTH_EVENT_TYPE_GRANTED |
no | Timeline event_type for allowed taps (default edge_access_granted) |
Run
From the deploy directory:
uv sync
uv run member-portal-edge-auth
systemd
A sample unit file is in systemd/member-portal-edge-auth.service. It uses uv run member-portal-edge-auth with WorkingDirectory set to the deploy tree (so pyproject.toml, uv.lock, and .env are used the same way as interactive runs). ExecStart must point at the uv binary (e.g. /usr/local/bin/uv); install or symlink it there, or edit the unit. See comments in the unit file for install steps.
Offline behaviour and persistence
- Access list file: After a successful fetch from the portal, the current card map is written to
EDGE_AUTH_CACHE_FILE. If the daemon starts and cannot reach the portal, it loads that file when the in-memory cache is still empty (sameEDGE_AUTH_ACCESS_LISTas when the file was saved). - Outbox: If a timeline grant or access-denial POST fails (network error or non-success HTTP), the request body is appended to
EDGE_AUTH_OUTBOX_FILE(JSON lines). A background task retries on a schedule and after each successful access list refresh. Each line includes an ISO 8601timestampso the portal can record when the scan happened, not only when replay succeeded (POST /api/timeline/eventsandPOST /api/edge/access-denialboth accepttimestamp). - Sound cache: After each successful refresh, newly referenced door sounds are fetched into the cache, and MP3 files no longer referenced by the current list are deleted.
Test socket client
With the edge-auth daemon running, send a card id and print the JSON response (uses EDGE_AUTH_SOCKET_PATH or the same default as the daemon):
uv run edge-auth-test-socket "30:01:02:bb"
# or
uv run edge-auth-test-socket 3137470768
The test client sends card as a JSON string unchanged. That value must match the portal’s stored card_number for the tap to be allowed (quote in the shell when needed, e.g. "00123").
Optional: uv run edge-auth-test-socket --socket-path /path/to/sock 3137470768
Socket protocol
One JSON object per line (newline-terminated), one JSON response per line.
Authorise
{"card":"30:01:02:bb"}
or {"cmd":"authorise","card":"..."}.
Use a JSON string that matches the portal card_number (readers/scanners should perform any format conversion before sending). A JSON number cannot preserve leading zeros and may not match a zero-padded stored card number.
Response (allowed)
{"allowed":true,"name":"Alex","sound_path":"/path/to/deploy/data/sounds/uuid.mp3"}
Response (denied)
{"allowed":false,"portal":{"logged":"unknown_scan"}}
If the portal reports the card is on the access list but the edge cache was stale, portal may include "logged":"skipped","reason":"card_on_access_list"; the helper then refreshes its cache.
Force refresh
{"cmd":"refresh"}