# 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`](../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, 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](#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. ```sh 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: ```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 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_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 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: ```sh 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). ```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 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 . 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 ```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.