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.
8.6 KiB
YOUreka Tools
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
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, caching, proxies
├── yrka-auth.js # YOUreka Auth cookie session → PocketBase (authRefresh)
├── permit.js # OpenFGA permission checks + 5-minute cache
├── cropper.js # /cropper router (queue, review, save) — injectable clients
├── public/
│ ├── index.html # landing page shell (tiles injected per session)
│ └── assets/
│ ├── 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).
Running
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.
cd tools
npm install
npm start # → http://localhost:4173
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:
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 it works
Authentication and sessions
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.
Permissions
- Fingerprint captures require
capture_biometricper 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 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 only environment variables this app uses.
They live in dotenvx-encrypted files at the repository root — .env for
development, .env.production for deployment:
MINIO_ACCESS_KEY=… MINIO_SECRET_KEY=…
AZURE_STORAGE_CONNECTION_STRING=…
NODERED_HTTP_USERNAME=… NODERED_HTTP_PASSWORD=…
CARVEKIT_API_KEY=…
Without them the rest of the app works and /cropper answers 503. The private
decryption keys sit in .env.keys (gitignored, never commit it).
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 Fingerprint Agent
The /fingerprint wizard walks the operator through:
- Locate profile — pick a permitted domain, enter the staff reference and OTP, and look up the record through the a2loft proxy.
- 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-enrollto fpagent. - 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):
docker build -t yreka-tools .
docker run -d -p 4173:4173 --name yreka-tools \
--env DOTENV_PRIVATE_KEY_PRODUCTION="$(grep -oP '^DOTENV_PRIVATE_KEY_PRODUCTION=\K.*' .env.keys)" \
yreka-tools
dotenvx run --strict makes the container exit rather than start with
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.
Behind a TLS-terminating proxy the app honours X-Forwarded-Proto
(trust proxy is enabled), so session cookies get the Secure flag
automatically.
Tests
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.