2026-10-01 14:39:57 +01:00
|
|
|
# YOUreka Tools
|
2021-12-08 14:35:55 +01:00
|
|
|
|
2026-10-01 14:39:57 +01:00
|
|
|
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 `/`.
|
2021-12-08 14:35:55 +01:00
|
|
|
|
2026-09-30 23:12:51 +01:00
|
|
|
## Layout
|
2021-12-08 14:35:55 +01:00
|
|
|
|
2026-09-30 23:12:51 +01:00
|
|
|
```
|
|
|
|
|
tools/
|
2026-10-01 14:39:57 +01:00
|
|
|
├── server.js # Express app: routes, session middleware, caching, proxies
|
2026-09-30 23:12:51 +01:00
|
|
|
├── yrka-auth.js # YOUreka Auth cookie session → PocketBase (authRefresh)
|
2026-10-01 14:39:57 +01:00
|
|
|
├── permit.js # OpenFGA permission checks + 5-minute cache
|
|
|
|
|
├── cropper.js # /cropper router (queue, review, save) — injectable clients
|
2026-09-30 23:12:51 +01:00
|
|
|
├── public/
|
2026-10-01 14:39:57 +01:00
|
|
|
│ ├── index.html # landing page shell (tiles injected per session)
|
2026-10-02 09:53:35 +01:00
|
|
|
│ ├── fpagent/
|
|
|
|
|
│ │ └── install.sh # public /fpagent/install.sh (proxies private releases)
|
2026-09-30 23:12:51 +01:00
|
|
|
│ └── assets/
|
2026-10-01 14:39:57 +01:00
|
|
|
│ ├── 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
|
2026-09-30 23:12:51 +01:00
|
|
|
```
|
|
|
|
|
|
2026-10-01 14:39:57 +01:00
|
|
|
`openspec/` at the repository root holds the design specs and the in-flight
|
|
|
|
|
change proposals (see [Design docs](#design-docs)).
|
|
|
|
|
|
2026-09-30 23:12:51 +01:00
|
|
|
## Running
|
|
|
|
|
|
2026-10-01 14:39:57 +01:00
|
|
|
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.
|
2026-09-30 23:12:51 +01:00
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
cd tools
|
|
|
|
|
npm install
|
2026-10-01 14:39:57 +01:00
|
|
|
npm start # → http://localhost:4173
|
2026-09-30 23:12:51 +01:00
|
|
|
```
|
|
|
|
|
|
2026-10-01 14:39:57 +01:00
|
|
|
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
|
|
|
|
|
```
|
2026-09-30 23:12:51 +01:00
|
|
|
|
|
|
|
|
> fpagent must allow the tools origin. On a dev machine run it with:
|
|
|
|
|
> `fpagent -allowed-origins http://localhost:4173`
|
|
|
|
|
|
2026-10-01 14:39:57 +01:00
|
|
|
## How it works
|
2026-09-30 23:12:51 +01:00
|
|
|
|
2026-10-01 14:39:57 +01:00
|
|
|
### Authentication and sessions
|
2026-09-30 23:12:51 +01:00
|
|
|
|
2026-10-01 14:39:57 +01:00
|
|
|
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.
|
2026-09-30 23:12:51 +01:00
|
|
|
|
2026-10-01 14:39:57 +01:00
|
|
|
### 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`
|
2026-10-02 20:41:43 +01:00
|
|
|
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.
|
2026-09-30 23:12:51 +01:00
|
|
|
|
2026-10-01 01:13:30 +01:00
|
|
|
## The Image Cropper
|
|
|
|
|
|
2026-10-01 14:39:57 +01:00
|
|
|
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.
|
2026-10-01 01:13:30 +01:00
|
|
|
|
2026-10-02 09:53:35 +01:00
|
|
|
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](#installing-the-agent)):
|
2026-10-01 01:13:30 +01:00
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
MINIO_ACCESS_KEY=… MINIO_SECRET_KEY=…
|
|
|
|
|
AZURE_STORAGE_CONNECTION_STRING=…
|
|
|
|
|
NODERED_HTTP_USERNAME=… NODERED_HTTP_PASSWORD=…
|
|
|
|
|
CARVEKIT_API_KEY=…
|
|
|
|
|
```
|
|
|
|
|
|
2026-10-01 14:39:57 +01:00
|
|
|
Without them the rest of the app works and `/cropper` answers `503`. The private
|
|
|
|
|
decryption keys sit in `.env.keys` (gitignored, never commit it).
|
2026-10-01 03:17:52 +01:00
|
|
|
|
|
|
|
|
```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
|
|
|
|
|
```
|
|
|
|
|
|
2026-10-01 14:39:57 +01:00
|
|
|
## The Fingerprint Agent
|
|
|
|
|
|
|
|
|
|
The `/fingerprint` wizard walks the operator through:
|
|
|
|
|
|
2026-10-02 20:41:43 +01:00
|
|
|
1. **Locate profile** — pick a permitted domain, enter the staff reference, and
|
|
|
|
|
look up the record through the a2loft proxy.
|
2026-10-01 14:39:57 +01:00
|
|
|
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.
|
2026-10-02 20:41:43 +01:00
|
|
|
3. **Save** — both templates are posted to a2loft authenticated with the
|
|
|
|
|
session token.
|
2026-10-01 14:39:57 +01:00
|
|
|
|
2026-10-02 15:12:16 +01:00
|
|
|
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.
|
|
|
|
|
|
2026-10-02 09:53:35 +01:00
|
|
|
### Installing the agent
|
|
|
|
|
|
|
|
|
|
`/fpagent/install.sh` is a public installer (also served for `curl | bash`). It
|
2026-10-06 19:19:16 +01:00
|
|
|
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
|
2026-10-02 09:53:35 +01:00
|
|
|
[`fingerprint-agent`](https://sources.yrka.app/YOUreka/fingerprint-agent/releases)
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```sh
|
2026-10-06 19:19:16 +01:00
|
|
|
curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash
|
2026-10-02 09:53:35 +01:00
|
|
|
curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash -s -- --check # report only
|
2026-10-06 19:19:16 +01:00
|
|
|
curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash -s -- --tag v1.0.0
|
2026-10-02 09:53:35 +01:00
|
|
|
```
|
|
|
|
|
|
2026-10-02 11:26:35 +01:00
|
|
|
Rerunning it is the upgrade path: it installs the newest release (or `--tag`
|
2026-10-06 19:19:16 +01:00
|
|
|
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.
|
2026-10-02 11:26:35 +01:00
|
|
|
|
2026-10-02 09:53:35 +01:00
|
|
|
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`.
|
|
|
|
|
|
2026-10-01 14:39:57 +01:00
|
|
|
## Configuration
|
|
|
|
|
|
2026-10-02 09:53:35 +01:00
|
|
|
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.
|
2026-10-01 14:39:57 +01:00
|
|
|
|
|
|
|
|
## 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):
|
2026-10-01 03:21:05 +01:00
|
|
|
|
|
|
|
|
```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.
|
2026-10-01 01:13:30 +01:00
|
|
|
|
2026-09-30 23:12:51 +01:00
|
|
|
Behind a TLS-terminating proxy the app honours `X-Forwarded-Proto`
|
|
|
|
|
(`trust proxy` is enabled), so session cookies get the `Secure` flag
|
|
|
|
|
automatically.
|
|
|
|
|
|
2026-10-01 14:39:57 +01:00
|
|
|
## Tests
|
2021-12-08 14:35:55 +01:00
|
|
|
|
2026-10-01 14:39:57 +01:00
|
|
|
```sh
|
|
|
|
|
cd tools
|
|
|
|
|
npm test # node --test — 48 tests, no network or hardware required
|
|
|
|
|
```
|
2021-12-08 14:35:55 +01:00
|
|
|
|
2026-10-01 14:39:57 +01:00
|
|
|
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.
|