adebisi-fa 528eaafb18 docs(openspec): archive cropper-token-auth and sync specs
The cropper upload POSTs with the session token and no OTP query auth, and the
panel shows the profile's domain and picture target. Field pass was verified
earlier (upload 0b90075b, saved with no OTP). Merged the image-cropper delta
into the main spec and archived the change.
2026-10-06 19:29:45 +01:00
2021-12-08 14:35:55 +01:00

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)
│   ├── fpagent/
│   │   └── install.sh      # public /fpagent/install.sh (proxies private releases)
│   └── 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_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 authenticates the operator with the signed-in session's PocketBase token, sent to a2loft as a Bearer credential; the operator never sends credentials to a third party directly. The cropper's save call uses the same token flow 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 live in dotenvx-encrypted files at the repository root — .env for development, .env.production for deployment. The fpagent installer key is read from the same files (see Installing the agent):

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:

  1. Locate profile — pick a permitted domain, enter the staff reference, 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 authenticated with the session token.

The wizard tracks fpagent's scanner availability: the header tag and the capture panel show when the agent is offline or no reader is attached, and capture is disabled until a supported reader is detected. Availability means the reader is attached, not that a capture will succeed; enrolling still fails if the scanner is claimed or a scan does not complete.

Installing the agent

/fpagent/install.sh is a public installer (also served for curl | bash). It fetches the newest bundle for the host architecture — x86_64 or 32-bit i386, preferring the bundle built for the machine's Ubuntu codename — from the private fingerprint-agent forge, verifies its sha256, then runs the bundle's own installer, which handles libfprint, the udev rule, plugdev and the systemd service. On a field laptop:

curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash
curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash -s -- --check   # report only
curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash -s -- --tag v1.0.0

Rerunning it is the upgrade path: it installs the newest release (or --tag for a pinned one), the bundle installer reports the version change, and it restarts a running service so the new binary takes effect. The version is the git hash carried in the asset name, stamped next to the binary (/usr/local/bin/fpagent.version) and compared on the next run. The systemd service and the self-update timer are on by default; pass --no-service or --no-updates to leave them off.

The forge is private, so the operator's laptop needs no credentials: the app reads the release with GITEA_RELEASE_INSTALL_KEY and proxies it through /fpagent/releases/*. Without that key the installer answers 503.

Configuration

Everything except the 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, forge https://sources.yrka.app and the domain catalog. Change them there. The only environment variables are the cropper credentials and the fpagent installer key GITEA_RELEASE_INSTALL_KEY above.

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.

S
Description
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.
Readme
920 KiB
Languages
JavaScript 74.7%
HTML 14.7%
CSS 6.9%
Shell 3%
Dockerfile 0.7%