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
**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
@@ -34,45 +66,60 @@ npm install
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 "<value>" -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.