Files
2026-09-23 01:52:28 +10:00

4.1 KiB
Raw Permalink Blame History

visitor-signin — architecture

Current state of the design. Established 2026-09-23 (first recorded session). When a later session changes something here, edit the relevant section and note the date and reason.

Process layout

One Node process in one container:

  • HTTPS listener on container port 3000 (kiosk, admin, APIs).
  • Plain-HTTP helper on 3001: serves the CA certificate and redirects everything else to HTTPS.
  • Background jobs in-process: photo retention purge, optional nightly auto sign-out, daily certificate renewal check, Google Sheet sync retries.

Data

  • SQLite at /data/visitors.db (bind-mounted ./data), WAL mode.
  • Photos on disk under /data/photos/YYYY/MM/; banners under /data/branding/; certs under /data/certs/.
  • Migrations are additive and idempotent (addColumn in src/db.js) — never destructive.
  • visits denormalises name, contact, host and site name, so history survives deletion of the related records.

Identity and access

  • Admin accounts: email + scrypt password + TOTP (RFC 6238), eight one-shot recovery codes. Roles owner (manages admins and sites) and admin (optionally scoped to one site). Optional email-domain restriction. First account created once from ADMIN_BOOTSTRAP_*.
  • Recurring visitors: mobile number is the username; PIN is 4 digits, stored AES-256-GCM encrypted with a keyed HMAC pin_lookup for uniqueness. Phone, email and PIN are each unique.
  • APP_SECRET keys sessions, PIN encryption and pin_lookup. Changing it breaks every PIN.

Multi-site

  • sites owns: people to visit (hosts), badge settings, printer settings, branding.
  • A kiosk is bound to a site by /?site=<slug> (remembered in the tablet's local storage) or a first-run picker; a single-site install never asks.
  • Sign in, sign out and duplicate detection are scoped per site.

TLS

  • Local CA (10 years) + server certificate (398 days), generated by the container with openssl.
  • Server cert renews inside 30 days and is hot-swapped with setSecureContext; the CA is never replaced except by an explicit --force, so devices trust it once.
  • Reissue is triggered by a change in HTTPS_HOSTNAMES (tracked in certs/.hostnames.json), not by the full SAN list, because the container's own bridge IP changes between restarts.
  • CA distributed as PEM .crt, DER .cer (required by Jamf Pro) and a zip bundle (browsers block bare certificate downloads).

Printing

  • The server renders the badge (src/printer.js, @napi-rs/canvas) at 300 dpi, 696 dots across (QL-820NWB head width on a 62 mm roll), and sends it with brother_ql over TCP 9100.
  • Photos are halftoned in the renderer (Floyd–Steinberg by default) so preview equals print.
  • The loaded roll is an explicit per-site setting (printer_label: 62 or 62red). 62red always adds --red; without it the job is monochrome and the printer refuses it.
  • Sign in waits up to PRINT_SIGNIN_WAIT_MS for the print; on failure the kiosk falls back to a browser print of the HTML badge (src/sites.js).
  • brother_ql's network backend never reads the socket and the QL-820NWB does not answer status requests on 9100, so the server cannot confirm a label came out. Brother Status Monitor on a connected PC is the source of truth for refusals.

Google Sheet

  • Evacuation mirror only: one tab ("On site now"), rewritten in full on every sign in/out and every 15 minutes. No history is sent to Google; history lives in the app (Visit log, CSV).
  • A failed write marks the tab stale and is retried; it never blocks the kiosk.

Front end

  • Three separate documents: kiosk (index.html), login (login.html), console (admin.html).
  • Kiosk theming is per site: four chosen colours, the rest derived with WCAG contrast rules.
  • HTML/JS/CSS served with Cache-Control: no-cache so an upgrade is never masked by a cached console.

Deployment

  • Author on Windows, deploy on an Ubuntu docker host from the deploy branch with git reset --hard origin/deploy (the host is a mirror, never merges).
  • docker-entrypoint.sh fixes ownership of the bind-mounted /data, then drops to node.
  • pull_policy: build — the image is always built locally.