diff --git a/README.md b/README.md index f56c53c..fb323ae 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,125 @@ -# YOUreka Tools — Fingerprint Service +# YOUreka Tools -A small Express app that hosts the YOUreka tools landing page and the -**Fingerprint Agent** enrollment wizard. Fingerprint capture is performed by -the local [`fpagent`](../fingerprint-agent) binary (Socket.IO on -`127.0.0.1:9000`); this app provides the login (YOUreka Auth), the permitted -domain selection, and the a2loft integration that stores the enrolled -fingerprints. +The web front-end for YOUreka ID field operations. A small Express app that +hosts an authentication-aware landing page, an **Image Cropper**, a +**Fingerprint Agent** enrollment wizard, and a public **Help** page. + +Fingerprint captures run through the local [`fpagent`](../fingerprint-agent) +binary (Socket.IO on `127.0.0.1:9000`); this app handles login (YOUreka Auth), +per-domain permissions, and the a2loft integration that reads and stores records. +It is stateless — the operator's PocketBase session lives in an httpOnly cookie, +so the container can run anywhere while fpagent stays on the field laptop. + +## Tools at a glance + +| Page | Path | Access | Purpose | +| --- | --- | --- | --- | +| Landing | `/` | public | Links the tools when signed in; shows a sign-in call to action when not | +| Fingerprint Agent | `/fingerprint` | signed in | Enroll right-thumb and right-index fingerprints via the attached scanner | +| Image Cropper | `/cropper` | signed in + `crop_image` | Review, crop, clean and save identity photos from the a2loft queue | +| Help | `/help` | public | Intro plus a per-tool how-to (Carbon accordion) | + +## Navigation + +Every page shares one Carbon ui-shell header and mobile side nav, rendered +server-side: + +- **Home** → `/` +- **Image Cropper**, **Fingerprint Agent** → shown only when signed in +- a divider, then **Help** +- **Sign in** (anonymous) or **Sign out** (signed in; confirmed via a modal) + +The landing page hides the two service tiles until you sign in, and the sign-in +callback returns you to `/`. ## Layout ``` tools/ -├── server.js # Express app: routes, session middleware, proxies +├── server.js # Express app: routes, session middleware, caching, proxies ├── yrka-auth.js # YOUreka Auth cookie session → PocketBase (authRefresh) -├── permit.js # OpenFGA domain permission checks + 5-minute cache +├── permit.js # OpenFGA permission checks + 5-minute cache +├── cropper.js # /cropper router (queue, review, save) — injectable clients ├── public/ -│ ├── index.html # landing page (Image Cropper + Fingerprint Agent) +│ ├── index.html # landing page shell (tiles injected per session) │ └── assets/ -│ ├── style.css -│ └── fp/ # /fingerprint enrollment wizard (Vue 2 via CDN) -└── test/ # node --test suite (fake portal / PocketBase / a2loft) +│ ├── style.css # shared Carbon-styled layout + g100 theme +│ ├── help.html # /help docs page +│ ├── fp/ # /fingerprint wizard (Vue 2 via CDN) +│ ├── cropper/ # /cropper page (Vue 2 + vue-croppa via CDN) +│ └── youreka-logo.png, youreka-favicon.png +└── test/ # node --test suite (fake portal / PocketBase / a2loft), 48 tests ``` +`openspec/` at the repository root holds the design specs and the in-flight +change proposals (see [Design docs](#design-docs)). + ## Running -Prerequisites: Node.js 22+, `fpagent` installed and running on this laptop -(see the fingerprint-agent repository for installation), the U.are.U 4500 -scanner attached. +Prerequisites: Node.js 22+. For the Fingerprint Agent you also need `fpagent` +installed and running on this laptop (see the fingerprint-agent repository) with +the U.are.U 4500 scanner attached. ```sh cd tools npm install -npm start # → http://localhost:4173 +npm start # → http://localhost:4173 ``` -Open http://localhost:4173 and sign in with your YOUreka account. +Open http://localhost:4173 and sign in with your YOUreka account. Started this +way (no cropper credentials) everything works except `/cropper`, which answers +`503`. + +To run locally **with** the cropper, supply the credentials via dotenvx from the +repository root: + +```sh +dotenvx run -f .env -- node tools/server.js +``` > fpagent must allow the tools origin. On a dev machine run it with: > `fpagent -allowed-origins http://localhost:4173` -## How enrollment works +## How it works -1. **Sign in** — YOUreka Auth (portal web-auth redirect). The PocketBase - session is kept in an httpOnly cookie and refreshed server-side. -2. **Pick a domain** — the hardcoded catalog (UNICAL, Metropolitan Hotel, UCTH, - UNICAL Radio International, YOUreka) is filtered against the same portal - OpenFGA endpoint fpagent uses (`capture_biometric` per domain). Results are - cached for 5 minutes; the **Refresh domains** control unlocks once the - cache expires. fpagent re-authorizes every capture regardless. -3. **Locate the staff profile** — reference + OTP, validated by - `apis.a2loft.net` through a same-origin proxy (the operator's phone comes - from the session). -4. **Capture** — right thumb and right index finger, one capture at a time, - with per-scan progress and cancel; the PocketBase token, domain and - reference travel with every `fprint-enroll` to fpagent. -5. **Save** — both templates are posted to a2loft (with a fresh OTP). +### Authentication and sessions -## Configuration +Sign-in is the YOUreka portal web-auth flow: the app opens a portal auth session +with a local callback URL, redirects the operator to the portal, and on callback +redeems the session id for a PocketBase cookie. That value is stored under the +app's own httpOnly `session` cookie (30-day max age) alongside the operator's +profile. On every request the session is recovered server-side and refreshed +when the token is invalid or expires within 5 minutes; an unrecoverable session +is treated as anonymous and the stale cookie cleared. -Everything is hardcoded in `tools/server.js` by design — port `4173`, portal -`https://portal.yrka.app`, PocketBase `https://pb.itcass.net`, a2loft -`https://apis.a2loft.net`, agent `http://127.0.0.1:9000`, and the domain -catalog. Change them there; there are no environment variables or config -files. +### Permissions + +- **Fingerprint captures** require `capture_biometric` per domain. The hardcoded + catalog (UNICAL, Metropolitan Hotel, UCTH, UNICAL Radio International, + YOUreka) is filtered against the portal's OpenFGA endpoint, cached for 5 + minutes; the **Refresh domains** control unlocks once the cache expires. + fpagent re-authorizes every capture regardless. +- **Image Cropper** actions require `crop_image`. Reads may use the 5-minute + cached verdict; the save always verifies live. + +### a2loft proxy + +Staff lookup and the fingerprint save are proxied to `https://apis.a2loft.net` +under same-origin routes. The proxy attaches the operator's phone from the +server-side session and forwards the OTP supplied by the page; the operator +never sends credentials to a third party directly. The cropper's save call +instead authenticates with the session token (`Bearer`) after a live permission +check. ## The Image Cropper -The `/cropper` page (Smart Photo Editor) lets operators review, crop and save -identity photos. It reuses the YOUreka session for identity, requires the -`crop_image` permission on `domain:idid` for every proxied action (verified -against the portal's OpenFGA endpoint — live on save, 5-minute cached on -reads), and keeps the a2loft OTP requirement only on the save step. +The `/cropper` page (Smart Photo Editor) lets operators work through the a2loft +`id-images` queue: **Load next queued** or **Load by upload ID**, review the +record, crop the photo, optionally **Clean background**, then accept or reject +with reasons. Reads may use the cached `crop_image` verdict; saving re-verifies +live and uploads through the queue. -Its service credentials are the sole environment variables this app uses. +Its service credentials are the **only** environment variables this app uses. They live in dotenvx-encrypted files at the repository root — `.env` for development, `.env.production` for deployment: @@ -83,16 +130,46 @@ NODERED_HTTP_USERNAME=… NODERED_HTTP_PASSWORD=… CARVEKIT_API_KEY=… ``` -Without them the rest of the app works and `/cropper` routes answer `503`. -The private decryption keys sit in `.env.keys` (gitignored, never commit it). +Without them the rest of the app works and `/cropper` answers `503`. The private +decryption keys sit in `.env.keys` (gitignored, never commit it). ```sh dotenvx run -f .env -- node tools/server.js # local, from the repo root dotenvx set MINIO_SECRET_KEY "" -f .env # add/rotate a value ``` -The Docker image contains the encrypted `.env.production` and runs through -`dotenvx`; only the private key is supplied at runtime (never baked in): +## The Fingerprint Agent + +The `/fingerprint` wizard walks the operator through: + +1. **Locate profile** — pick a permitted domain, enter the staff reference and + OTP, and look up the record through the a2loft proxy. +2. **Capture** — right thumb and right index finger, one capture at a time, with + per-scan progress and cancel. The PocketBase token, domain and reference + travel with every `fprint-enroll` to fpagent. +3. **Save** — both templates are posted to a2loft with a fresh OTP. + +## Configuration + +Everything except the cropper credentials is hardcoded in `tools/server.js` by +design — port `4173`, portal `https://portal.yrka.app`, PocketBase +`https://pb.itcass.net`, a2loft `https://apis.a2loft.net`, agent +`http://127.0.0.1:9000`, and the domain catalog. Change them there; there are no +other environment variables or config files. + +## Deployment + +**Cache busting.** Each process start generates a deploy id +(`DEPLOY_VERSION`) that is appended to every `/assets/...` URL in served pages +(`?v=…`), so browsers fetch fresh CSS/JS/images after a deploy. Versioned assets +are cached hard (`max-age=1y, immutable`); the `.html` templates and all page +routes are served `no-store`. + +**Stateless container.** The container only serves the web front-end; fpagent +keeps running on the operator's laptop (the wizard reaches it at +`http://127.0.0.1:9000` from the browser, wherever the container lives). The +image bakes in the encrypted `.env.production` and runs through `dotenvx`; only +the private key is supplied at runtime (never baked in): ```sh docker build -t yreka-tools . @@ -106,32 +183,25 @@ undecryptable credentials. Note that `--env` values are visible to `docker inspect`; on a shared host prefer an env file with restrictive permissions (`--env-file deploy.keys`) or your platform's secret store. -## Tests - -```sh -cd tools -npm test # node --test — 35 tests, no network or hardware required -``` - -## Docker - -## Docker - -The app is stateless, so the container only serves the web front-end; fpagent -keeps running on the operator's laptop (the wizard connects to -`http://127.0.0.1:9000` from the browser, wherever this container lives). - -```sh -docker build -t yreka-tools . -docker run -d -p 4173:4173 --name yreka-tools yreka-tools -``` - Behind a TLS-terminating proxy the app honours `X-Forwarded-Proto` (`trust proxy` is enabled), so session cookies get the `Secure` flag automatically. -Field walkthrough (real portal, PocketBase, a2loft, and U.are.U 4500): login → -permitted-domain filtering → staff lookup → two scanner captures with cancel -path → save → reset, all verified end to end via Playwright. +## Tests +```sh +cd tools +npm test # node --test — 48 tests, no network or hardware required +``` +The suite covers the auth round-trip, session refresh, permission caching, the +cropper router and token auth, and the a2loft proxies against fakes. A full +field walkthrough (real portal, PocketBase, a2loft, and U.are.U 4500) — login → +domain filtering → staff lookup → two captures with cancel path → save → reset — +has been verified end to end via Playwright. + +## Design docs + +`openspec/` holds the source-of-truth specs under `openspec/specs/` +(`tools-app`, `fingerprint-wizard`, `image-cropper`) and change proposals under +`openspec/changes/`. See `openspec/config.yaml` for the workflow.