docs: rewrite README for the current project status

Restructure around the four pages, the nav, auth/sessions, permissions,
the a2loft proxy, configuration, deployment (incl. asset cache-busting)
and tests; fix the duplicate Docker heading and the stale test count.
This commit is contained in:
2026-10-01 14:39:57 +01:00
parent a218835a1c
commit 4d006e361f
+141 -71
View File
@@ -1,32 +1,64 @@
# YOUreka Tools — Fingerprint Service # YOUreka Tools
A small Express app that hosts the YOUreka tools landing page and the The web front-end for YOUreka ID field operations. A small Express app that
**Fingerprint Agent** enrollment wizard. Fingerprint capture is performed by hosts an authentication-aware landing page, an **Image Cropper**, a
the local [`fpagent`](../fingerprint-agent) binary (Socket.IO on **Fingerprint Agent** enrollment wizard, and a public **Help** page.
`127.0.0.1:9000`); this app provides the login (YOUreka Auth), the permitted
domain selection, and the a2loft integration that stores the enrolled Fingerprint captures run through the local [`fpagent`](../fingerprint-agent)
fingerprints. 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 ## Layout
``` ```
tools/ 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) ├── 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/ ├── public/
│ ├── index.html # landing page (Image Cropper + Fingerprint Agent) │ ├── index.html # landing page shell (tiles injected per session)
│ └── assets/ │ └── assets/
│ ├── style.css │ ├── style.css # shared Carbon-styled layout + g100 theme
│ └── fp/ # /fingerprint enrollment wizard (Vue 2 via CDN) │ ├── help.html # /help docs page
└── test/ # node --test suite (fake portal / PocketBase / a2loft) │ ├── 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 ## Running
Prerequisites: Node.js 22+, `fpagent` installed and running on this laptop Prerequisites: Node.js 22+. For the Fingerprint Agent you also need `fpagent`
(see the fingerprint-agent repository for installation), the U.are.U 4500 installed and running on this laptop (see the fingerprint-agent repository) with
scanner attached. the U.are.U 4500 scanner attached.
```sh ```sh
cd tools cd tools
@@ -34,45 +66,60 @@ 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 must allow the tools origin. On a dev machine run it with:
> `fpagent -allowed-origins http://localhost:4173` > `fpagent -allowed-origins http://localhost:4173`
## How enrollment works ## How it works
1. **Sign in** — YOUreka Auth (portal web-auth redirect). The PocketBase ### Authentication and sessions
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).
## 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 ### Permissions
`https://portal.yrka.app`, PocketBase `https://pb.itcass.net`, a2loft
`https://apis.a2loft.net`, agent `http://127.0.0.1:9000`, and the domain - **Fingerprint captures** require `capture_biometric` per domain. The hardcoded
catalog. Change them there; there are no environment variables or config catalog (UNICAL, Metropolitan Hotel, UCTH, UNICAL Radio International,
files. 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 Image Cropper
The `/cropper` page (Smart Photo Editor) lets operators review, crop and save The `/cropper` page (Smart Photo Editor) lets operators work through the a2loft
identity photos. It reuses the YOUreka session for identity, requires the `id-images` queue: **Load next queued** or **Load by upload ID**, review the
`crop_image` permission on `domain:idid` for every proxied action (verified record, crop the photo, optionally **Clean background**, then accept or reject
against the portal's OpenFGA endpoint — live on save, 5-minute cached on with reasons. Reads may use the cached `crop_image` verdict; saving re-verifies
reads), and keeps the a2loft OTP requirement only on the save step. 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 They live in dotenvx-encrypted files at the repository root — `.env` for
development, `.env.production` for deployment: development, `.env.production` for deployment:
@@ -83,16 +130,46 @@ NODERED_HTTP_USERNAME=… NODERED_HTTP_PASSWORD=…
CARVEKIT_API_KEY=… CARVEKIT_API_KEY=…
``` ```
Without them the rest of the app works and `/cropper` routes answer `503`. Without them the rest of the app works and `/cropper` answers `503`. The private
The private decryption keys sit in `.env.keys` (gitignored, never commit it). decryption keys sit in `.env.keys` (gitignored, never commit it).
```sh ```sh
dotenvx run -f .env -- node tools/server.js # local, from the repo root dotenvx run -f .env -- node tools/server.js # local, from the repo root
dotenvx set MINIO_SECRET_KEY "<value>" -f .env # add/rotate a value dotenvx set MINIO_SECRET_KEY "<value>" -f .env # add/rotate a value
``` ```
The Docker image contains the encrypted `.env.production` and runs through ## The Fingerprint Agent
`dotenvx`; only the private key is supplied at runtime (never baked in):
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 ```sh
docker build -t yreka-tools . 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 `docker inspect`; on a shared host prefer an env file with restrictive
permissions (`--env-file deploy.keys`) or your platform's secret store. 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` Behind a TLS-terminating proxy the app honours `X-Forwarded-Proto`
(`trust proxy` is enabled), so session cookies get the `Secure` flag (`trust proxy` is enabled), so session cookies get the `Secure` flag
automatically. automatically.
Field walkthrough (real portal, PocketBase, a2loft, and U.are.U 4500): login → ## Tests
permitted-domain filtering → staff lookup → two scanner captures with cancel
path → save → reset, all verified end to end via Playwright.
```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.