Gate the USB kiosk the same way, and make an unreachable setting a test failure
The USB kiosk had a feature flag and nothing else. A flag is a decision, not a credential: with the kiosk on, anyone who could reach the app could move a drive between checked-out and checked-in and sign it with a badge they guessed. Same reasoning that closed the parts kiosk, smaller blast radius - which is why Darkwing did not raise it and why it was worth asking about anyway. Both kiosks now share one mechanism. The device token moved out of the printedparts api wrapper into a request interceptor that attaches X-Kiosk-Token by URL prefix, with one localStorage key per kiosk - they are separate screens in separate rooms, and a shared token would mean provisioning one grants the other. claimKioskToken() is shared too, so both views take the token from a ?token= URL once and strip it from the address bar identically. Then the part that matters more than either fix. I told you to set the new settings in the UI, and that was impossible: every plugin settings page carries a hardcoded key list, and nothing had added them. printedparts_kiosk_enabled, printedparts_kiosk_token and usb_kiosk_token were declared, seeded and read correctly, and rendered by no page - changeable only by writing SQL. tests/test_plugin_settings_reachable.py now fails when any declared setting appears nowhere in its plugin's frontend. It found seven MORE that were already in that state, all pre-existing: collector_corporate_ranges computers_configpolicies_enabled computers_machinelink_alerts computers_quietreporthours geenforce_reportstaleminutes printers_dashboardpercent warranty_expiringdays All seven now have fields. I first parked them in a KNOWN_GAPS ratchet, then fixed them and deleted the ratchet - a backlog list in a new check is a way of agreeing the check does not apply yet. There is no backlog; the check is absolute. Two of my own mistakes on the way, both caught by tests rather than by reading: the blanket rename of the USB guard also rewrote the call inside _kiosk_open, so it called itself until the stack ran out; and the first version of the reachable check skipped all 17 plugins because it looked for get_settings() when the method is get_settings_defaults() - it passed by testing nothing, which is the exact shape it exists to catch. Suite 2336, frontend 316, build clean. Scope: product
This commit is contained in:
@@ -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`)
|
||||
|
||||
Reference in New Issue
Block a user