Two failures from the same root: an administrator cannot see the kiosk token, and a screen that was provisioned wrongly does not say so. The settings API masks every key whose name contains 'token', so the settings page shows '********'. The real value is visible for one moment - after Generate, before Save - and an administrator who copies the field at any other time provisions a screen with the literal mask. The kiosk then answers 404, exactly as it answers a caller it has never heard of, because the route deliberately does not advertise itself. Nothing anywhere says what went wrong. - Each kiosk gains a permission-gated provisioning-link route that returns the real setup URL, token included, to somebody who already holds settings.edit. The mask stays: a token has no business in an ordinary settings list. The copy step just stops depending on catching the one moment it is on screen. Revealing it writes an audit row, since that is when the value left the database. Both settings pages get a "Copy setup link" button, which falls back to showing the link when the clipboard is refused - it is, over plain http and in locked-down browsers. - The USB kiosk asks the server on load whether this screen may be used, and says plainly when it may not. It rendered a keypad and only failed once somebody had typed their SSO, which reads as broken rather than as not set up. It cannot guess: the enabled flag is public and only says the site has a kiosk, and a caller admitted by network holds no token to look for, so the parts kiosk's localStorage check would warn exactly the callers the allowlist exists to serve. The probe is a badge-less session request - the gate runs before any badge handling, so refused is 404 and admitted is the validation error, with no lookup and nothing recorded. A test pins that ordering, because reordering it would start telling admitted screens they are not set up. Verified in a browser, not only in tests: provisioning with the mask lands on the refused screen, a correct token gets the drive list and survives a plain revisit, and a tokenless caller inside the allowlist gets straight in.
USB plugin - CMMC USB check-in/out database contract
The USB plugin tracks removable-media check-in/out for CMMC compliance. Device
and log state live in a separate, read-write MySQL database (cmmc_usb),
reached with parameterized pymysql via cmmc_usb_connection(). Names of people
are resolved from the HR employee directory (see the employees plugin).
The plugin's own reference tables (usbdevicetypes, usbdevices,
usbcheckouts) live in the main app database; only the live check-in/out data
is in cmmc_usb.
The schema is standardized across sites - the
cmmc_usbcheck-in/out solution is the same deployment everywhere, so the tables below match as-is and no schema adaptation is needed. The database name may differ per site, though - setcmmc_usb_db_name(defaultcmmc_usb) to match the local name. The view recipe at the end is only a fallback for a site that somehow differs. (Contrast the employee directory, which genuinely varies per site.)
Connection
Credentials resolve settings-first, then environment, except the password which is env-only (never stored in the app database).
| Field | Setting key (editable in the setup wizard) | Env var (fallback) |
|---|---|---|
| Host | cmmc_usb_db_host |
CMMC_USB_DB_HOST |
| Database | cmmc_usb_db_name |
CMMC_USB_DB_NAME |
| User | cmmc_usb_db_user |
CMMC_USB_DB_USER |
| Password | (not stored) | CMMC_USB_DB_PASSWORD |
Set the password in .env; the setup wizard's Features step shows the exact
line to paste. This database is read-write - the app account needs
SELECT, INSERT, UPDATE. Engine is MySQL/MariaDB (pymysql).
Required schema
The plugin runs raw SQL against these tables (or views - see below).
devices
| Column | Type | Notes |
|---|---|---|
device_id |
VARCHAR | Primary key; the device serial/tag |
device_desc |
VARCHAR | Description |
device_owner |
VARCHAR | Owner |
status |
VARCHAR | Check-in/out state |
locker_location |
VARCHAR | Where the device is stored |
Operations: SELECT (list + by id), INSERT (register device), UPDATE
(edit fields, change status).
checkinoutlog
| Column | Type | Notes |
|---|---|---|
log_id |
INT (PK) | Auto id |
badge_number |
VARCHAR | Person's badge |
device_id |
VARCHAR | FK to devices.device_id |
action |
VARCHAR | check-in / check-out |
timestamp |
DATETIME | When it happened |
scanned_viruses |
(text/int) | Scan result |
locker_location |
VARCHAR | Locker at time of event |
sanitized |
(bool/int) | Sanitization flag |
Operations: SELECT (history per device), INSERT (log an event).
users
| Column | Type | Notes |
|---|---|---|
badge_number |
VARCHAR | Primary key; the scanned badge |
first_name |
VARCHAR | Given name |
last_name |
VARCHAR | Surname |
Operations: SELECT by badge, INSERT (auto-add a badge on first scan).
Employee directory dependency
To turn a scanned badge into a name, the plugin also reads the HR employees
directory (via the employees plugin's connection). A badge shaped 0<digits>BZ
carries a PayNo (the digits); lookups try employees.SSO and employees.PayNo.
See plugins/employees/README.md for that schema and connection.
Adapting a different site schema (recommended: views)
Sites whose USB-tracking database uses different table/column names should
create read-only/updatable views named devices, checkinoutlog, and
users that map local columns to the names above. Example:
CREATE VIEW devices AS
SELECT
asset_tag AS device_id,
description AS device_desc,
owner AS device_owner,
state AS status,
storage_bay AS locker_location
FROM usb_assets;
Because the plugin writes to devices/checkinoutlog/users, either make the
views updatable (single-table views usually are) or expose real tables with
these column names. Grant the app account SELECT, INSERT, UPDATE.
Notes:
device_idandbadge_numberare the natural keys the plugin matches on.- If
cmmc_usbis unreachable, USB endpoints return an error and the rest of the app keeps working (the feature degrades, it does not crash the app).