105 lines
5.7 KiB
Markdown
105 lines
5.7 KiB
Markdown
# Design
|
||||
|
|
|
|||
|
|
## Context
|
|||
|
|
|
|||
|
|
`app.py` (Flask + python-socketio) serves the UI from `public/{mode}` and talks
|
|||
|
|
to libfprint via PyGObject; fpagent replaces the capture side entirely with a
|
|||
|
|
guarded Socket.IO server on `127.0.0.1:9000` (enroll-only: `fprint-enroll`,
|
|||
|
|
`fprint-enroll-status`, `fprint-enroll-done`, `fprint-enroll-stop`,
|
|||
|
|
`agent-hello`). The YOUreka Auth pattern is proven in `one-school-chat`
|
|||
|
|
(`src/lib/server/yrka-auth.ts` + `routes/login/+page.server.ts`): portal
|
|||
|
|
`POST /api/web/auth` → redirect → `GET /api/web/auth/{sessionId}/cookie` →
|
|||
|
|
store the PB cookie value under an httpOnly `session` cookie → PocketBase SDK
|
|||
|
|
`authRefresh()` when near expiry. fpagent's permit logic
|
|||
|
|
(`internal/permit/permit.go`) defines the OpenFGA check this app replicates for
|
|||
|
|
domain filtering. See proposal.md for motivation and scope.
|
|||
|
|
|
|||
|
|
## Goals / Non-Goals
|
|||
|
|
|
|||
|
|
**Goals:**
|
|||
|
|
- Zero-configuration operation: every URL and the port hardcoded in source.
|
|||
|
|
- Smallest reliable stack: Express + static CDN pages, no build step, no
|
|||
|
|
session store, no template engine.
|
|||
|
|
- Credentials stay server-side where possible: a2loft calls proxied same-origin
|
|||
|
|
with phone injected from the session; only the PocketBase token is handed to
|
|||
|
|
the page (fpagent's contract requires the page to emit it).
|
|||
|
|
|
|||
|
|
**Non-Goals:**
|
|||
|
|
- clockio/identify/verify (fpagent is enroll-only by design).
|
|||
|
|
- TypeScript, bundlers, component frameworks, server-side templating.
|
|||
|
|
- Permission caching or a domain-management UI (catalog is hardcoded).
|
|||
|
|
- Changes to a2loft's API contract or fpagent itself.
|
|||
|
|
|
|||
|
|
## Decisions
|
|||
|
|
|
|||
|
|
1. **Static CDN front-end, no build step.** The existing `public/capture` page
|
|||
|
|
is Vue 2 + Bootstrap 4 + socket.io + axios via CDN; the wizard reuses that
|
|||
|
|
stack unchanged in shape. Alternative (SvelteKit like one-school-chat)
|
|||
|
|
rejected: new toolchain in this repo for one page.
|
|||
|
|
2. **Session = PB cookie value in an httpOnly cookie; no server store.** Same
|
|||
|
|
as one-school-chat; any instance can serve any request, and refresh happens
|
|||
|
|
server-side per request via the PocketBase SDK against `pb.itcass.net`.
|
|||
|
|
Alternative (express-session + store) rejected: nothing server-side needs
|
|||
|
|
more than the cookie already carries.
|
|||
|
|
3. **Token delivery via `GET /auth/session` JSON** rather than templating: the
|
|||
|
|
wizard is a static page; one fetch returns token, profile, agent URL, and
|
|||
|
|
permitted domains. The token must live in the page because fpagent's
|
|||
|
|
`fprint-enroll` requires it — that is the agent's designed flow, and fpagent
|
|||
|
|
re-validates it per request.
|
|||
|
|
4. **Domain filtering server-side, parallel, fail-closed, cached 5 min.**
|
|||
|
|
Five domains × ≤2 sequential verify calls each, run with `Promise.all`
|
|||
|
|
per (re)check; results cached in an in-memory `Map<phone, {allowed,
|
|||
|
|
checkedAt}>` with a 5-minute TTL (one constant, matching the session
|
|||
|
|
refresh buffer). `/auth/session` serves the cached set within the TTL;
|
|||
|
|
a force-refresh endpoint re-checks all catalog domains, bypassing and
|
|||
|
|
then replacing the cache, and is refused while the cache is fresh. The
|
|||
|
|
filter is UX only; fpagent re-enforces at enroll time, so a revoked
|
|||
|
|
permission degrades gracefully (enroll rejected) even mid-session, and
|
|||
|
|
the wizard's refresh control covers the residual staleness window.
|
|||
|
|
5. **a2loft behind same-origin proxies.** The page calls
|
|||
|
|
`POST /api/:domain/staff/:reference` (lookup) and
|
|||
|
|
`POST /api/:domain/staff/:reference/fingerprints` (save); the server
|
|||
|
|
attaches `phone` from the session and forwards the page-supplied `otp` as
|
|||
|
|
query params — a2loft's contract (`?phone=&otp=`, same response shapes) is
|
|||
|
|
untouched. This keeps the operator's a2loft credential flow out of
|
|||
|
|
page-to-third-party calls and lets the phone come from the trusted session.
|
|||
|
|
6. **Hardcoded config incl. port 4173.** Moving an endpoint or the port is a
|
|||
|
|
one-constant source change; env-var plumbing is unused flexibility.
|
|||
|
|
7. **Python retirement in the same change, gated on a parity walkthrough.**
|
|||
|
|
`app.py` + `requirement.txt` + `public/` are deleted only after the new
|
|||
|
|
wizard completes a real capture-and-save round trip; git history preserves
|
|||
|
|
them. Rollback = revert.
|
|||
|
|
|
|||
|
|
## Risks / Trade-offs
|
|||
|
|
|
|||
|
|
- [a2loft auth contract drifts (no longer `?phone=&otp=`)] → only the two
|
|||
|
|
proxy handlers change; page and specs unaffected.
|
|||
|
|
- [Revoked permission lingers in the UI up to 5 minutes] → bounded by the
|
|||
|
|
cache TTL; fpagent re-enforces at enroll, and the wizard's refresh control
|
|||
|
|
(enabled once the cache elapses) re-checks on demand.
|
|||
|
|
- [Port 4173 already in use on a laptop] → deliberate hardcode; change the one
|
|||
|
|
constant (and fpagent's `-allowed-origins`) if a machine conflicts.
|
|||
|
|
- [Browser → `http://127.0.0.1:9000` blocked as mixed content] → loopback is a
|
|||
|
|
potentially trustworthy origin in all current browsers; works from both http
|
|||
|
|
and https pages. Watch for corporate-policy exceptions in the field.
|
|||
|
|
- [CORS misconfig blocks the wizard silently] → README documents the fpagent
|
|||
|
|
`-allowed-origins` requirement (`http://localhost:4173` in dev); wizard shows
|
|||
|
|
connection state from `agent-hello` so an unreachable/blocked agent is
|
|||
|
|
visible.
|
|||
|
|
- [Vue 2 is EOL] → acceptable for an internal tool page already on it; the
|
|||
|
|
page is isolated and small.
|
|||
|
|
|
|||
|
|
## Migration Plan
|
|||
|
|
|
|||
|
|
1. Build `tools/` alongside the untouched python app (different port; no
|
|||
|
|
interference).
|
|||
|
|
2. Parity walkthrough on field hardware: login → domain filter → lookup →
|
|||
|
|
capture ×2 → save; fingerprints verify against a2loft as before.
|
|||
|
|
3. Delete `app.py`, `requirement.txt`, `public/`; update README.
|
|||
|
|
4. Deploy note: fpagent must include the tools origin in `-allowed-origins`.
|
|||
|
|
|
|||
|
|
## Open Questions
|
|||
|
|
|
|||
|
|
None — a2loft's contract (unchanged, `?phone=&otp=`), hardcoded endpoints, and
|
|||
|
|
the domain catalog were all confirmed with the operator.
|