From 4b21aa38874c08746757f7dd6e2bd853dce65e26 Mon Sep 17 00:00:00 2001
From: Cam Proudlock
Date: Wed, 2 Sep 2026 18:11:01 -0400
Subject: [PATCH] Gate both kiosks behind a device token, and make an
unreachable setting a test failure
---
frontend/src/api/index.js | 69 ++++++++++----
.../frontend/views/CollectorSettings.vue | 93 ++++++++++++++++++-
.../frontend/views/GeEnforceSettings.vue | 35 ++++++-
.../frontend/views/PartsKiosk.vue | 24 +----
.../frontend/views/PrintedPartsSettings.vue | 61 +++++++++++-
plugins/printedparts/plugin.py | 6 +-
.../frontend/views/PrinterAlertsSettings.vue | 14 ++-
plugins/usb/api/routes.py | 37 +++++++-
plugins/usb/frontend/views/USBKiosk.vue | 3 +-
plugins/usb/frontend/views/USBSettings.vue | 41 +++++++-
plugins/usb/plugin.py | 12 +++
.../frontend/views/DellWarrantySettings.vue | 17 ++++
tests/test_plugin_settings_reachable.py | 88 ++++++++++++++++++
tests/test_plugins/test_usb_kiosk.py | 64 ++++++++++---
14 files changed, 498 insertions(+), 66 deletions(-)
create mode 100644 tests/test_plugin_settings_reachable.py
diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js
index 46733ca1..7b66a215 100644
--- a/frontend/src/api/index.js
+++ b/frontend/src/api/index.js
@@ -11,6 +11,54 @@ const api = axios.create({
})
// Add auth token and normalize pagination params
+// Kiosk device tokens. The kiosk routes are the only ones that write without a
+// login, so each is additionally gated on a token an administrator provisioned
+// into THAT screen's browser - see the kiosk views, which claim it from a
+// ?token= URL once and strip it from the address bar. A token served inside the
+// page would be readable by anyone who could load the page, so it is per-device
+// and never shipped with the bundle.
+//
+// One entry per kiosk: the two are separate screens in separate rooms and a
+// shared token would mean provisioning one grants the other.
+export const KIOSK_TOKEN_KEYS = {
+ '/printedparts/kiosk/': 'printedparts-kiosk-token',
+ '/usb/kiosk/': 'usb-kiosk-token',
+}
+
+function kioskTokenFor(url) {
+ const path = url || ''
+ for (const [prefix, storageKey] of Object.entries(KIOSK_TOKEN_KEYS)) {
+ if (path.includes(prefix)) {
+ try { return localStorage.getItem(storageKey) || '' } catch { return '' }
+ }
+ }
+ return ''
+}
+
+// Claim a kiosk's device token, once, from the URL an admin used to set the
+// screen up. Kept in THIS browser and stripped from the address bar straight
+// away, so it does not sit there for a shoulder to read or travel in a bookmark
+// or a screenshot.
+export function claimKioskToken(storageKey) {
+ try {
+ const url = new URL(window.location.href)
+ const token = (url.searchParams.get('token') || '').trim()
+ if (!token) return
+ localStorage.setItem(storageKey, token)
+ url.searchParams.delete('token')
+ window.history.replaceState({}, '', url.pathname + url.search + url.hash)
+ } catch {
+ // A browser refusing localStorage cannot be provisioned. Nothing to
+ // recover; the routes 404 and the operator sees the standard error.
+ }
+}
+
+api.interceptors.request.use(config => {
+ const token = kioskTokenFor(config.url)
+ if (token) config.headers['X-Kiosk-Token'] = token
+ return config
+})
+
api.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
@@ -1501,17 +1549,6 @@ export const measuringtoolsApi = {
}
// 3D printed parts (printedparts plugin)
-// Device token for the parts kiosk, provisioned once per screen and kept in
-// that browser. Absent means the routes answer 404, which is what an
-// unprovisioned browser should see.
-export const KIOSK_TOKEN_KEY = 'printedparts-kiosk-token'
-
-function kioskHeaders() {
- let token = ''
- try { token = localStorage.getItem(KIOSK_TOKEN_KEY) || '' } catch { token = '' }
- return token ? { 'X-Kiosk-Token': token } : {}
-}
-
export const printedpartsApi = {
list(params = {}) {
return api.get('/printedparts/items', { params })
@@ -1552,17 +1589,11 @@ export const printedpartsApi = {
adjust(printeditemid, data) {
return api.post(`/printedparts/items/${printeditemid}/adjust`, data)
},
- // The kiosk routes are the only unauthenticated write in the product, so they
- // are gated on a per-DEVICE token rather than on the page. It lives in this
- // browser's localStorage, put there once by an admin opening the kiosk with
- // ?token=... - see PartsKiosk.vue. A token shipped inside the page would be
- // readable by anyone who could load it, which is not a gate.
kioskItem(itemcode) {
- return api.get(`/printedparts/kiosk/item/${encodeURIComponent(itemcode)}`,
- { headers: kioskHeaders() })
+ return api.get(`/printedparts/kiosk/item/${encodeURIComponent(itemcode)}`)
},
kioskTake(data) {
- return api.post('/printedparts/kiosk/take', data, { headers: kioskHeaders() })
+ return api.post('/printedparts/kiosk/take', data)
},
listFiles(printeditemid) {
return api.get(`/printedparts/items/${printeditemid}/files`)
diff --git a/plugins/computers/frontend/views/CollectorSettings.vue b/plugins/computers/frontend/views/CollectorSettings.vue
index c530c76e..46ced119 100644
--- a/plugins/computers/frontend/views/CollectorSettings.vue
+++ b/plugins/computers/frontend/views/CollectorSettings.vue
@@ -10,6 +10,50 @@
is nothing in it to find and edit.
+
+